/** * ================================================================ * 商城价格排序 · 接口级契约 E2E · TC-S-U01 ~ TC-S-U04 * ================================================================ * 需求来源:docs/需求分析/2026-11-07-商城价格排序需求变更.md(变更 S1) * - 后端能力已完备(SortInterceptor 读 body.sort 数组 + SortUtil 白名单注入) * - 本次为纯前端改动(排序栏 UI + buildSort() 传参) * → 故先锁定「后端排序契约」:前端 buildSort() 的三种取值必须被后端正确消费。 * * 契约映射(对应 buildSort 输出): * price_asc → [{column:'price',order:'asc'}] * price_desc → [{column:'price',order:'desc'}] * default → [](不传,走后端默认 sort_order asc + created_at desc) * * 只读:仅查询商品列表,不写任何数据。 * ================================================================ */ const { test, expect } = require('@playwright/test'); const API = process.env.LIVE_API_BASE || 'http://cfc.iwintrue.com'; async function listProducts(request, body) { const resp = await request.post(`${API}/api/product/list`, { data: body, timeout: 30000 }); expect(resp.status(), '商品列表接口应 200').toBe(200); const json = await resp.json(); expect(json.code, `业务码应为 200,实际 ${json.code} ${json.message || ''}`).toBe(200); const data = json.data || {}; const rows = data.records || data.list || data.rows || (Array.isArray(data) ? data : []); return rows; } function prices(rows) { return rows.map((r) => Number(r.price)).filter((n) => !Number.isNaN(n)); } test.describe('商城价格排序 · 后端排序契约(需求变更 S1)', () => { test('TC-S-U01 price asc 生效(对应 buildSort → price_asc)', async ({ request }) => { const rows = await listProducts(request, { page: 1, size: 8, sort: [{ column: 'price', order: 'asc' }] }); expect(rows.length, '应返回商品').toBeGreaterThan(1); const p = prices(rows); const sorted = [...p].sort((a, b) => a - b); expect(p, `价格应非降序,实际 ${p.join(',')}`).toEqual(sorted); }); test('TC-S-U02 price desc 生效(对应 buildSort → price_desc)', async ({ request }) => { const rows = await listProducts(request, { page: 1, size: 8, sort: [{ column: 'price', order: 'desc' }] }); expect(rows.length).toBeGreaterThan(1); const p = prices(rows); const sorted = [...p].sort((a, b) => b - a); expect(p, `价格应非升序,实际 ${p.join(',')}`).toEqual(sorted); }); test('TC-S-U03 默认排序不变(对应 buildSort → default,不传 sort)', async ({ request }) => { const rows = await listProducts(request, { page: 1, size: 8 }); expect(rows.length, '默认查询应返回商品').toBeGreaterThan(0); // 默认口径:sort_order asc, created_at desc —— 不做值断言,只锁定「结构不回归」 expect(rows[0]).toHaveProperty('id'); expect(rows[0]).toHaveProperty('price'); }); test('TC-S-U04 非法排序列被白名单拦截(不 5xx、不注入)', async ({ request }) => { const resp = await request.post(`${API}/api/product/list`, { data: { page: 1, size: 5, sort: [{ column: 'price;drop table product', order: 'asc' }] }, timeout: 30000, }); expect(resp.status(), '非法列名不应导致 5xx').toBeLessThan(500); const json = await resp.json(); expect([200, 400], `应返回 200(忽略非法列)或 400(显式拒绝),实际 ${json.code}`).toContain(json.code); if (json.code === 200) { const rows = (json.data && (json.data.records || json.data.list)) || []; expect(rows.length, '合法查询仍应返回数据(证明未抛异常)').toBeGreaterThan(0); } }); });