shop-sort-api.spec.js 3.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
  1. /**
  2. * ================================================================
  3. * 商城价格排序 · 接口级契约 E2E · TC-S-U01 ~ TC-S-U04
  4. * ================================================================
  5. * 需求来源:docs/需求分析/2026-11-07-商城价格排序需求变更.md(变更 S1)
  6. * - 后端能力已完备(SortInterceptor 读 body.sort 数组 + SortUtil 白名单注入)
  7. * - 本次为纯前端改动(排序栏 UI + buildSort() 传参)
  8. * → 故先锁定「后端排序契约」:前端 buildSort() 的三种取值必须被后端正确消费。
  9. *
  10. * 契约映射(对应 buildSort 输出):
  11. * price_asc → [{column:'price',order:'asc'}]
  12. * price_desc → [{column:'price',order:'desc'}]
  13. * default → [](不传,走后端默认 sort_order asc + created_at desc)
  14. *
  15. * 只读:仅查询商品列表,不写任何数据。
  16. * ================================================================
  17. */
  18. const { test, expect } = require('@playwright/test');
  19. const API = process.env.LIVE_API_BASE || 'http://cfc.iwintrue.com';
  20. async function listProducts(request, body) {
  21. const resp = await request.post(`${API}/api/product/list`, { data: body, timeout: 30000 });
  22. expect(resp.status(), '商品列表接口应 200').toBe(200);
  23. const json = await resp.json();
  24. expect(json.code, `业务码应为 200,实际 ${json.code} ${json.message || ''}`).toBe(200);
  25. const data = json.data || {};
  26. const rows = data.records || data.list || data.rows || (Array.isArray(data) ? data : []);
  27. return rows;
  28. }
  29. function prices(rows) {
  30. return rows.map((r) => Number(r.price)).filter((n) => !Number.isNaN(n));
  31. }
  32. test.describe('商城价格排序 · 后端排序契约(需求变更 S1)', () => {
  33. test('TC-S-U01 price asc 生效(对应 buildSort → price_asc)', async ({ request }) => {
  34. const rows = await listProducts(request, { page: 1, size: 8, sort: [{ column: 'price', order: 'asc' }] });
  35. expect(rows.length, '应返回商品').toBeGreaterThan(1);
  36. const p = prices(rows);
  37. const sorted = [...p].sort((a, b) => a - b);
  38. expect(p, `价格应非降序,实际 ${p.join(',')}`).toEqual(sorted);
  39. });
  40. test('TC-S-U02 price desc 生效(对应 buildSort → price_desc)', async ({ request }) => {
  41. const rows = await listProducts(request, { page: 1, size: 8, sort: [{ column: 'price', order: 'desc' }] });
  42. expect(rows.length).toBeGreaterThan(1);
  43. const p = prices(rows);
  44. const sorted = [...p].sort((a, b) => b - a);
  45. expect(p, `价格应非升序,实际 ${p.join(',')}`).toEqual(sorted);
  46. });
  47. test('TC-S-U03 默认排序不变(对应 buildSort → default,不传 sort)', async ({ request }) => {
  48. const rows = await listProducts(request, { page: 1, size: 8 });
  49. expect(rows.length, '默认查询应返回商品').toBeGreaterThan(0);
  50. // 默认口径:sort_order asc, created_at desc —— 不做值断言,只锁定「结构不回归」
  51. expect(rows[0]).toHaveProperty('id');
  52. expect(rows[0]).toHaveProperty('price');
  53. });
  54. test('TC-S-U04 非法排序列被白名单拦截(不 5xx、不注入)', async ({ request }) => {
  55. const resp = await request.post(`${API}/api/product/list`, {
  56. data: { page: 1, size: 5, sort: [{ column: 'price;drop table product', order: 'asc' }] },
  57. timeout: 30000,
  58. });
  59. expect(resp.status(), '非法列名不应导致 5xx').toBeLessThan(500);
  60. const json = await resp.json();
  61. expect([200, 400], `应返回 200(忽略非法列)或 400(显式拒绝),实际 ${json.code}`).toContain(json.code);
  62. if (json.code === 200) {
  63. const rows = (json.data && (json.data.records || json.data.list)) || [];
  64. expect(rows.length, '合法查询仍应返回数据(证明未抛异常)').toBeGreaterThan(0);
  65. }
  66. });
  67. });