|
|
@@ -0,0 +1,74 @@
|
|
|
+/**
|
|
|
+ * ================================================================
|
|
|
+ * 商城价格排序 · 接口级契约 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);
|
|
|
+ }
|
|
|
+ });
|
|
|
+});
|