b2-shelf-scope.spec.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. import { test, expect } from '../fixtures/auth';
  2. import { STORAGE_STATE_PATH } from '../fixtures/auth';
  3. import { request as playwrightRequest, type APIRequestContext } from '@playwright/test';
  4. import fs from 'node:fs';
  5. /**
  6. * B2-FIX-1 LocationShelf.Location 作用域校验 e2e 回归。
  7. *
  8. * 安全契约:
  9. * - 所有测试数据使用 TEST_B2_ 前缀;afterAll 严格按 (子→主) 顺序删除。
  10. * - 不修改任何非 TEST_B2_ 数据;A2 delete guard 保留,不绕过。
  11. * - SCOPE_A=(1,1) 用作正向 master;SCOPE_B=(2,2) 用作 ScopeMiss 触发。
  12. * - DTO 范围校验要求 CompanyRefId/FactoryRefId>=1,不能用 0/0。
  13. * - 任何 cleanup 失败立即 hard fail(不吞错)。
  14. */
  15. const RUN_ID = `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
  16. const PREFIX = 'TEST_B2_';
  17. const LOC_CODE = `${PREFIX}LOC_${RUN_ID}`;
  18. const LOC_NOEXIST = `${PREFIX}NOLOC_${RUN_ID}`;
  19. const SHELF_HAPPY = `${PREFIX}SH_OK_${RUN_ID}`;
  20. const SHELF_NOTFOUND = `${PREFIX}SH_NF_${RUN_ID}`;
  21. const SHELF_SCOPEMISS = `${PREFIX}SH_SM_${RUN_ID}`;
  22. const SCOPE_A = { CompanyRefId: 1, FactoryRefId: 1 };
  23. const SCOPE_B = { CompanyRefId: 2, FactoryRefId: 2 };
  24. let api: APIRequestContext;
  25. let createdLocationId: number | null = null;
  26. let createdShelfId: number | null = null;
  27. function tokenFromStorageState(): string {
  28. if (!fs.existsSync(STORAGE_STATE_PATH)) {
  29. throw new Error(`storage-state.json not found at ${STORAGE_STATE_PATH}; global-setup must run first.`);
  30. }
  31. const raw = fs.readFileSync(STORAGE_STATE_PATH, 'utf8');
  32. const state = JSON.parse(raw) as { origins?: Array<{ localStorage?: Array<{ name: string; value: string }> }> };
  33. for (const origin of state.origins ?? []) {
  34. for (const item of origin.localStorage ?? []) {
  35. if (/access-token$/i.test(item.name) && !/x-access-token$/i.test(item.name)) {
  36. try {
  37. const parsed = JSON.parse(item.value);
  38. return typeof parsed === 'string' ? parsed : (parsed?.value ?? item.value);
  39. } catch {
  40. return item.value;
  41. }
  42. }
  43. }
  44. }
  45. throw new Error('access-token not found in storage-state.json');
  46. }
  47. test.beforeAll(async () => {
  48. const baseURL = (process.env.AIDOP_E2E_BASE_URL ?? 'http://localhost:8888').replace(/\/+$/, '');
  49. api = await playwrightRequest.newContext({
  50. baseURL,
  51. extraHTTPHeaders: { Authorization: `Bearer ${tokenFromStorageState()}` },
  52. });
  53. // 创建 SCOPE_A 下的 TEST_B2_ Location 主记录,作为 happy + ScopeMiss 的引用目标
  54. const locResp = await api.post('/api/s0/warehouse/locations', {
  55. data: { ...SCOPE_A, DomainCode: 'TEST', Location: LOC_CODE, Descr: 'B2-TEST-1 fixture', IsActive: true },
  56. });
  57. expect(locResp.status(), `setup: create TEST_B2_ Location failed: ${await locResp.text()}`).toBe(200);
  58. const locBody = await locResp.json();
  59. createdLocationId = locBody?.recId ?? locBody?.RecID ?? locBody?.id ?? null;
  60. expect(createdLocationId, 'setup: TEST_B2_ Location rec id missing').not.toBeNull();
  61. });
  62. test.afterAll(async () => {
  63. const errors: string[] = [];
  64. try {
  65. // 子 → 主 顺序:先删 shelf,再删 location,避免 A2 LocationReferences guard 阻挡
  66. if (createdShelfId !== null) {
  67. const r = await api.delete(`/api/s0/warehouse/location-shelves/${createdShelfId}`);
  68. if (r.status() !== 200) errors.push(`shelf delete ${createdShelfId} -> ${r.status()} ${await r.text()}`);
  69. }
  70. if (createdLocationId !== null) {
  71. const r = await api.delete(`/api/s0/warehouse/locations/${createdLocationId}`);
  72. if (r.status() !== 200) errors.push(`location delete ${createdLocationId} -> ${r.status()} ${await r.text()}`);
  73. }
  74. } finally {
  75. await api.dispose();
  76. }
  77. expect(errors, `cleanup must succeed: ${errors.join('; ')}`).toEqual([]);
  78. });
  79. test('B2-2 happy: shelf 与 location 同 scope → 200', async () => {
  80. const r = await api.post('/api/s0/warehouse/location-shelves', {
  81. data: { ...SCOPE_A, DomainCode: 'TEST', Location: LOC_CODE, InvShelf: SHELF_HAPPY, Descr: 'happy' },
  82. });
  83. expect(r.status(), await r.text()).toBe(200);
  84. const body = await r.json();
  85. createdShelfId = body?.recId ?? body?.RecID ?? body?.id ?? null;
  86. expect(createdShelfId).not.toBeNull();
  87. });
  88. test('B2-2 NotFound: shelf 引用不存在的 location → 400 S01011', async () => {
  89. const r = await api.post('/api/s0/warehouse/location-shelves', {
  90. data: { ...SCOPE_A, DomainCode: 'TEST', Location: LOC_NOEXIST, InvShelf: SHELF_NOTFOUND },
  91. });
  92. expect(r.status()).toBe(400);
  93. const body = await r.json();
  94. expect(body?.code ?? body?.Code).toBe('S01011');
  95. });
  96. test('B2-2 ScopeMiss: shelf scope 与 location scope 不一致 → 400 S01012', async () => {
  97. const r = await api.post('/api/s0/warehouse/location-shelves', {
  98. data: { ...SCOPE_B, DomainCode: 'TEST', Location: LOC_CODE, InvShelf: SHELF_SCOPEMISS },
  99. });
  100. expect(r.status()).toBe(400);
  101. const body = await r.json();
  102. expect(body?.code ?? body?.Code).toBe('S01012');
  103. });
  104. test('B2-2 Update ScopeMiss: 改 happy shelf 到 SCOPE_B → 400 S01012(origin 非 0/0,不降级)', async () => {
  105. expect(createdShelfId, 'happy shelf must exist before update test').not.toBeNull();
  106. const r = await api.put(`/api/s0/warehouse/location-shelves/${createdShelfId}`, {
  107. data: { ...SCOPE_B, DomainCode: 'TEST', Location: LOC_CODE, InvShelf: SHELF_HAPPY, Descr: 'tampered' },
  108. });
  109. expect(r.status()).toBe(400);
  110. const body = await r.json();
  111. expect(body?.code ?? body?.Code).toBe('S01012');
  112. });