b2-employee-scope.spec.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  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 Employee.Department 作用域校验 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. * - 任何 cleanup 失败立即 hard fail(不吞错)。
  13. */
  14. const RUN_ID = `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
  15. const PREFIX = 'TEST_B2_';
  16. const DEPT_CODE = `${PREFIX}DEPT_${RUN_ID}`;
  17. const DEPT_NOEXIST = `${PREFIX}NODEPT_${RUN_ID}`;
  18. const EMP_HAPPY = `${PREFIX}EMP_OK_${RUN_ID}`;
  19. const EMP_NOTFOUND = `${PREFIX}EMP_NF_${RUN_ID}`;
  20. const EMP_SCOPEMISS = `${PREFIX}EMP_SM_${RUN_ID}`;
  21. const SCOPE_A = { CompanyRefId: 1, FactoryRefId: 1 };
  22. const SCOPE_B = { CompanyRefId: 2, FactoryRefId: 2 };
  23. let api: APIRequestContext;
  24. let createdDeptId: number | null = null;
  25. let createdEmpId: number | null = null;
  26. function tokenFromStorageState(): string {
  27. if (!fs.existsSync(STORAGE_STATE_PATH)) {
  28. throw new Error(`storage-state.json not found at ${STORAGE_STATE_PATH}; global-setup must run first.`);
  29. }
  30. const raw = fs.readFileSync(STORAGE_STATE_PATH, 'utf8');
  31. const state = JSON.parse(raw) as { origins?: Array<{ localStorage?: Array<{ name: string; value: string }> }> };
  32. for (const origin of state.origins ?? []) {
  33. for (const item of origin.localStorage ?? []) {
  34. if (/access-token$/i.test(item.name) && !/x-access-token$/i.test(item.name)) {
  35. try {
  36. const parsed = JSON.parse(item.value);
  37. return typeof parsed === 'string' ? parsed : (parsed?.value ?? item.value);
  38. } catch {
  39. return item.value;
  40. }
  41. }
  42. }
  43. }
  44. throw new Error('access-token not found in storage-state.json');
  45. }
  46. test.beforeAll(async () => {
  47. const baseURL = (process.env.AIDOP_E2E_BASE_URL ?? 'http://localhost:8888').replace(/\/+$/, '');
  48. api = await playwrightRequest.newContext({
  49. baseURL,
  50. extraHTTPHeaders: { Authorization: `Bearer ${tokenFromStorageState()}` },
  51. });
  52. const deptResp = await api.post('/api/s0/warehouse/departments', {
  53. data: { ...SCOPE_A, DomainCode: 'TEST', Department: DEPT_CODE, Descr: 'B2-TEST-1 fixture', IsActive: true },
  54. });
  55. expect(deptResp.status(), `setup: create TEST_B2_ Department failed: ${await deptResp.text()}`).toBe(200);
  56. const deptBody = await deptResp.json();
  57. createdDeptId = deptBody?.recID ?? deptBody?.RecID ?? deptBody?.id ?? null;
  58. expect(createdDeptId, 'setup: TEST_B2_ Department rec id missing').not.toBeNull();
  59. });
  60. test.afterAll(async () => {
  61. const errors: string[] = [];
  62. try {
  63. if (createdEmpId !== null) {
  64. const r = await api.delete(`/api/s0/warehouse/employees/${createdEmpId}`);
  65. if (r.status() !== 200) errors.push(`employee delete ${createdEmpId} -> ${r.status()} ${await r.text()}`);
  66. }
  67. if (createdDeptId !== null) {
  68. const r = await api.delete(`/api/s0/warehouse/departments/${createdDeptId}`);
  69. if (r.status() !== 200) errors.push(`department delete ${createdDeptId} -> ${r.status()} ${await r.text()}`);
  70. }
  71. } finally {
  72. await api.dispose();
  73. }
  74. expect(errors, `cleanup must succeed: ${errors.join('; ')}`).toEqual([]);
  75. });
  76. test('B2-3 happy: employee 与 department 同 scope → 200', async () => {
  77. const r = await api.post('/api/s0/warehouse/employees', {
  78. data: { ...SCOPE_A, DomainCode: 'TEST', Employee: EMP_HAPPY, Name: 'TestB2', Department: DEPT_CODE, IsActive: true },
  79. });
  80. expect(r.status(), await r.text()).toBe(200);
  81. const body = await r.json();
  82. createdEmpId = body?.recID ?? body?.RecID ?? body?.id ?? null;
  83. expect(createdEmpId).not.toBeNull();
  84. });
  85. test('B2-3 NotFound: employee 引用不存在的 department → 400 S01011', async () => {
  86. const r = await api.post('/api/s0/warehouse/employees', {
  87. data: { ...SCOPE_A, DomainCode: 'TEST', Employee: EMP_NOTFOUND, Name: 'TestB2', Department: DEPT_NOEXIST, IsActive: true },
  88. });
  89. expect(r.status()).toBe(400);
  90. const body = await r.json();
  91. expect(body?.code ?? body?.Code).toBe('S01011');
  92. });
  93. test('B2-3 ScopeMiss: employee scope 与 department scope 不一致 → 400 S01012', async () => {
  94. const r = await api.post('/api/s0/warehouse/employees', {
  95. data: { ...SCOPE_B, DomainCode: 'TEST', Employee: EMP_SCOPEMISS, Name: 'TestB2', Department: DEPT_CODE, IsActive: true },
  96. });
  97. expect(r.status()).toBe(400);
  98. const body = await r.json();
  99. expect(body?.code ?? body?.Code).toBe('S01012');
  100. });
  101. test('B2-3 Update ScopeMiss: 改 happy employee 到 SCOPE_B → 400 S01012(origin 非 0/0,不降级)', async () => {
  102. expect(createdEmpId, 'happy employee must exist before update test').not.toBeNull();
  103. const r = await api.put(`/api/s0/warehouse/employees/${createdEmpId}`, {
  104. data: { ...SCOPE_B, DomainCode: 'TEST', Employee: EMP_HAPPY, Name: 'TestB2-updated', Department: DEPT_CODE, IsActive: true },
  105. });
  106. expect(r.status()).toBe(400);
  107. const body = await r.json();
  108. expect(body?.code ?? body?.Code).toBe('S01012');
  109. });