Эх сурвалжийг харах

test(s0): cover shelf and employee reference scope guards

YY968XX 5 сар өмнө
parent
commit
b031e135

+ 120 - 0
Web/tests/e2e/s0/b2-employee-scope.spec.ts

@@ -0,0 +1,120 @@
+import { test, expect } from '../fixtures/auth';
+import { STORAGE_STATE_PATH } from '../fixtures/auth';
+import { request as playwrightRequest, type APIRequestContext } from '@playwright/test';
+import fs from 'node:fs';
+
+/**
+ * B2-FIX-1 Employee.Department 作用域校验 e2e 回归。
+ *
+ * 安全契约:
+ *  - 所有测试数据使用 TEST_B2_ 前缀;afterAll 严格按 (子→主) 顺序删除。
+ *  - 不修改任何非 TEST_B2_ 数据;A2 delete guard 保留,不绕过。
+ *  - SCOPE_A=(1,1) 用作正向 master;SCOPE_B=(2,2) 用作 ScopeMiss 触发。
+ *  - 任何 cleanup 失败立即 hard fail(不吞错)。
+ */
+
+const RUN_ID = `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
+const PREFIX = 'TEST_B2_';
+const DEPT_CODE = `${PREFIX}DEPT_${RUN_ID}`;
+const DEPT_NOEXIST = `${PREFIX}NODEPT_${RUN_ID}`;
+const EMP_HAPPY = `${PREFIX}EMP_OK_${RUN_ID}`;
+const EMP_NOTFOUND = `${PREFIX}EMP_NF_${RUN_ID}`;
+const EMP_SCOPEMISS = `${PREFIX}EMP_SM_${RUN_ID}`;
+const SCOPE_A = { CompanyRefId: 1, FactoryRefId: 1 };
+const SCOPE_B = { CompanyRefId: 2, FactoryRefId: 2 };
+
+let api: APIRequestContext;
+let createdDeptId: number | null = null;
+let createdEmpId: number | null = null;
+
+function tokenFromStorageState(): string {
+	if (!fs.existsSync(STORAGE_STATE_PATH)) {
+		throw new Error(`storage-state.json not found at ${STORAGE_STATE_PATH}; global-setup must run first.`);
+	}
+	const raw = fs.readFileSync(STORAGE_STATE_PATH, 'utf8');
+	const state = JSON.parse(raw) as { origins?: Array<{ localStorage?: Array<{ name: string; value: string }> }> };
+	for (const origin of state.origins ?? []) {
+		for (const item of origin.localStorage ?? []) {
+			if (/access-token$/i.test(item.name) && !/x-access-token$/i.test(item.name)) {
+				try {
+					const parsed = JSON.parse(item.value);
+					return typeof parsed === 'string' ? parsed : (parsed?.value ?? item.value);
+				} catch {
+					return item.value;
+				}
+			}
+		}
+	}
+	throw new Error('access-token not found in storage-state.json');
+}
+
+test.beforeAll(async () => {
+	const baseURL = (process.env.AIDOP_E2E_BASE_URL ?? 'http://localhost:8888').replace(/\/+$/, '');
+	api = await playwrightRequest.newContext({
+		baseURL,
+		extraHTTPHeaders: { Authorization: `Bearer ${tokenFromStorageState()}` },
+	});
+
+	const deptResp = await api.post('/api/s0/warehouse/departments', {
+		data: { ...SCOPE_A, DomainCode: 'TEST', Department: DEPT_CODE, Descr: 'B2-TEST-1 fixture', IsActive: true },
+	});
+	expect(deptResp.status(), `setup: create TEST_B2_ Department failed: ${await deptResp.text()}`).toBe(200);
+	const deptBody = await deptResp.json();
+	createdDeptId = deptBody?.recID ?? deptBody?.RecID ?? deptBody?.id ?? null;
+	expect(createdDeptId, 'setup: TEST_B2_ Department rec id missing').not.toBeNull();
+});
+
+test.afterAll(async () => {
+	const errors: string[] = [];
+	try {
+		if (createdEmpId !== null) {
+			const r = await api.delete(`/api/s0/warehouse/employees/${createdEmpId}`);
+			if (r.status() !== 200) errors.push(`employee delete ${createdEmpId} -> ${r.status()} ${await r.text()}`);
+		}
+		if (createdDeptId !== null) {
+			const r = await api.delete(`/api/s0/warehouse/departments/${createdDeptId}`);
+			if (r.status() !== 200) errors.push(`department delete ${createdDeptId} -> ${r.status()} ${await r.text()}`);
+		}
+	} finally {
+		await api.dispose();
+	}
+	expect(errors, `cleanup must succeed: ${errors.join('; ')}`).toEqual([]);
+});
+
+test('B2-3 happy: employee 与 department 同 scope → 200', async () => {
+	const r = await api.post('/api/s0/warehouse/employees', {
+		data: { ...SCOPE_A, DomainCode: 'TEST', Employee: EMP_HAPPY, Name: 'TestB2', Department: DEPT_CODE, IsActive: true },
+	});
+	expect(r.status(), await r.text()).toBe(200);
+	const body = await r.json();
+	createdEmpId = body?.recID ?? body?.RecID ?? body?.id ?? null;
+	expect(createdEmpId).not.toBeNull();
+});
+
+test('B2-3 NotFound: employee 引用不存在的 department → 400 S01011', async () => {
+	const r = await api.post('/api/s0/warehouse/employees', {
+		data: { ...SCOPE_A, DomainCode: 'TEST', Employee: EMP_NOTFOUND, Name: 'TestB2', Department: DEPT_NOEXIST, IsActive: true },
+	});
+	expect(r.status()).toBe(400);
+	const body = await r.json();
+	expect(body?.code ?? body?.Code).toBe('S01011');
+});
+
+test('B2-3 ScopeMiss: employee scope 与 department scope 不一致 → 400 S01012', async () => {
+	const r = await api.post('/api/s0/warehouse/employees', {
+		data: { ...SCOPE_B, DomainCode: 'TEST', Employee: EMP_SCOPEMISS, Name: 'TestB2', Department: DEPT_CODE, IsActive: true },
+	});
+	expect(r.status()).toBe(400);
+	const body = await r.json();
+	expect(body?.code ?? body?.Code).toBe('S01012');
+});
+
+test('B2-3 Update ScopeMiss: 改 happy employee 到 SCOPE_B → 400 S01012(origin 非 0/0,不降级)', async () => {
+	expect(createdEmpId, 'happy employee must exist before update test').not.toBeNull();
+	const r = await api.put(`/api/s0/warehouse/employees/${createdEmpId}`, {
+		data: { ...SCOPE_B, DomainCode: 'TEST', Employee: EMP_HAPPY, Name: 'TestB2-updated', Department: DEPT_CODE, IsActive: true },
+	});
+	expect(r.status()).toBe(400);
+	const body = await r.json();
+	expect(body?.code ?? body?.Code).toBe('S01012');
+});

+ 123 - 0
Web/tests/e2e/s0/b2-shelf-scope.spec.ts

@@ -0,0 +1,123 @@
+import { test, expect } from '../fixtures/auth';
+import { STORAGE_STATE_PATH } from '../fixtures/auth';
+import { request as playwrightRequest, type APIRequestContext } from '@playwright/test';
+import fs from 'node:fs';
+
+/**
+ * B2-FIX-1 LocationShelf.Location 作用域校验 e2e 回归。
+ *
+ * 安全契约:
+ *  - 所有测试数据使用 TEST_B2_ 前缀;afterAll 严格按 (子→主) 顺序删除。
+ *  - 不修改任何非 TEST_B2_ 数据;A2 delete guard 保留,不绕过。
+ *  - SCOPE_A=(1,1) 用作正向 master;SCOPE_B=(2,2) 用作 ScopeMiss 触发。
+ *  - DTO 范围校验要求 CompanyRefId/FactoryRefId>=1,不能用 0/0。
+ *  - 任何 cleanup 失败立即 hard fail(不吞错)。
+ */
+
+const RUN_ID = `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
+const PREFIX = 'TEST_B2_';
+const LOC_CODE = `${PREFIX}LOC_${RUN_ID}`;
+const LOC_NOEXIST = `${PREFIX}NOLOC_${RUN_ID}`;
+const SHELF_HAPPY = `${PREFIX}SH_OK_${RUN_ID}`;
+const SHELF_NOTFOUND = `${PREFIX}SH_NF_${RUN_ID}`;
+const SHELF_SCOPEMISS = `${PREFIX}SH_SM_${RUN_ID}`;
+const SCOPE_A = { CompanyRefId: 1, FactoryRefId: 1 };
+const SCOPE_B = { CompanyRefId: 2, FactoryRefId: 2 };
+
+let api: APIRequestContext;
+let createdLocationId: number | null = null;
+let createdShelfId: number | null = null;
+
+function tokenFromStorageState(): string {
+	if (!fs.existsSync(STORAGE_STATE_PATH)) {
+		throw new Error(`storage-state.json not found at ${STORAGE_STATE_PATH}; global-setup must run first.`);
+	}
+	const raw = fs.readFileSync(STORAGE_STATE_PATH, 'utf8');
+	const state = JSON.parse(raw) as { origins?: Array<{ localStorage?: Array<{ name: string; value: string }> }> };
+	for (const origin of state.origins ?? []) {
+		for (const item of origin.localStorage ?? []) {
+			if (/access-token$/i.test(item.name) && !/x-access-token$/i.test(item.name)) {
+				try {
+					const parsed = JSON.parse(item.value);
+					return typeof parsed === 'string' ? parsed : (parsed?.value ?? item.value);
+				} catch {
+					return item.value;
+				}
+			}
+		}
+	}
+	throw new Error('access-token not found in storage-state.json');
+}
+
+test.beforeAll(async () => {
+	const baseURL = (process.env.AIDOP_E2E_BASE_URL ?? 'http://localhost:8888').replace(/\/+$/, '');
+	api = await playwrightRequest.newContext({
+		baseURL,
+		extraHTTPHeaders: { Authorization: `Bearer ${tokenFromStorageState()}` },
+	});
+
+	// 创建 SCOPE_A 下的 TEST_B2_ Location 主记录,作为 happy + ScopeMiss 的引用目标
+	const locResp = await api.post('/api/s0/warehouse/locations', {
+		data: { ...SCOPE_A, DomainCode: 'TEST', Location: LOC_CODE, Descr: 'B2-TEST-1 fixture', IsActive: true },
+	});
+	expect(locResp.status(), `setup: create TEST_B2_ Location failed: ${await locResp.text()}`).toBe(200);
+	const locBody = await locResp.json();
+	createdLocationId = locBody?.recId ?? locBody?.RecID ?? locBody?.id ?? null;
+	expect(createdLocationId, 'setup: TEST_B2_ Location rec id missing').not.toBeNull();
+});
+
+test.afterAll(async () => {
+	const errors: string[] = [];
+	try {
+		// 子 → 主 顺序:先删 shelf,再删 location,避免 A2 LocationReferences guard 阻挡
+		if (createdShelfId !== null) {
+			const r = await api.delete(`/api/s0/warehouse/location-shelves/${createdShelfId}`);
+			if (r.status() !== 200) errors.push(`shelf delete ${createdShelfId} -> ${r.status()} ${await r.text()}`);
+		}
+		if (createdLocationId !== null) {
+			const r = await api.delete(`/api/s0/warehouse/locations/${createdLocationId}`);
+			if (r.status() !== 200) errors.push(`location delete ${createdLocationId} -> ${r.status()} ${await r.text()}`);
+		}
+	} finally {
+		await api.dispose();
+	}
+	expect(errors, `cleanup must succeed: ${errors.join('; ')}`).toEqual([]);
+});
+
+test('B2-2 happy: shelf 与 location 同 scope → 200', async () => {
+	const r = await api.post('/api/s0/warehouse/location-shelves', {
+		data: { ...SCOPE_A, DomainCode: 'TEST', Location: LOC_CODE, InvShelf: SHELF_HAPPY, Descr: 'happy' },
+	});
+	expect(r.status(), await r.text()).toBe(200);
+	const body = await r.json();
+	createdShelfId = body?.recId ?? body?.RecID ?? body?.id ?? null;
+	expect(createdShelfId).not.toBeNull();
+});
+
+test('B2-2 NotFound: shelf 引用不存在的 location → 400 S01011', async () => {
+	const r = await api.post('/api/s0/warehouse/location-shelves', {
+		data: { ...SCOPE_A, DomainCode: 'TEST', Location: LOC_NOEXIST, InvShelf: SHELF_NOTFOUND },
+	});
+	expect(r.status()).toBe(400);
+	const body = await r.json();
+	expect(body?.code ?? body?.Code).toBe('S01011');
+});
+
+test('B2-2 ScopeMiss: shelf scope 与 location scope 不一致 → 400 S01012', async () => {
+	const r = await api.post('/api/s0/warehouse/location-shelves', {
+		data: { ...SCOPE_B, DomainCode: 'TEST', Location: LOC_CODE, InvShelf: SHELF_SCOPEMISS },
+	});
+	expect(r.status()).toBe(400);
+	const body = await r.json();
+	expect(body?.code ?? body?.Code).toBe('S01012');
+});
+
+test('B2-2 Update ScopeMiss: 改 happy shelf 到 SCOPE_B → 400 S01012(origin 非 0/0,不降级)', async () => {
+	expect(createdShelfId, 'happy shelf must exist before update test').not.toBeNull();
+	const r = await api.put(`/api/s0/warehouse/location-shelves/${createdShelfId}`, {
+		data: { ...SCOPE_B, DomainCode: 'TEST', Location: LOC_CODE, InvShelf: SHELF_HAPPY, Descr: 'tampered' },
+	});
+	expect(r.status()).toBe(400);
+	const body = await r.json();
+	expect(body?.code ?? body?.Code).toBe('S01012');
+});