| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121 |
- import { test, expect } from '../fixtures/auth';
- import type { Page } from '@playwright/test';
- /**
- * A2 实测:S0 四类主数据 DELETE 是否在存在下游引用时拒绝。
- * 不修改任何数据:每条用例先用 list 找到一条"应当被引用"的记录,仅尝试 DELETE,
- * 期望 4xx + 业务错误码;若意外返回 2xx,则真的存在 BUG。
- */
- async function token(page: Page) {
- return page.evaluate(() => {
- for (let i = 0; i < localStorage.length; i++) {
- const k = localStorage.key(i)!;
- if (/access-token$/i.test(k) && !/x-access-token$/i.test(k)) {
- const raw = localStorage.getItem(k)!;
- try {
- const v = JSON.parse(raw);
- return typeof v === 'string' ? v : v?.value ?? raw;
- } catch {
- return raw;
- }
- }
- }
- return null;
- });
- }
- async function get(page: Page, url: string) {
- const t = await token(page);
- const r = await page.request.get(`${new URL(page.url()).origin}${url}`, {
- headers: { Authorization: `Bearer ${t!}` },
- });
- return { status: r.status(), json: r.ok() ? ((await r.json()) as any) : null };
- }
- async function del(page: Page, url: string) {
- const t = await token(page);
- const r = await page.request.delete(`${new URL(page.url()).origin}${url}`, {
- headers: { Authorization: `Bearer ${t!}` },
- });
- let body: any = null;
- try { body = await r.json(); } catch { body = await r.text().catch(() => null); }
- return { status: r.status(), body };
- }
- function rowsOf(j: any): any[] {
- return j?.list ?? j?.data?.list ?? j?.data?.items ?? j?.data ?? j?.items ?? [];
- }
- /** 取主表与子表全量,按 keyField 求交集,返回主表中 key 出现在子表的第一条。 */
- async function pickReferenced(
- page: Page,
- parentUrl: string,
- childUrl: string,
- parentField: string,
- childField: string,
- ): Promise<{ id: number; key: string; childCount: number } | null> {
- const parents = rowsOf((await get(page, parentUrl)).json);
- const children = rowsOf((await get(page, childUrl)).json);
- const childCounts = new Map<string, number>();
- for (const c of children) {
- const k = c[childField];
- if (k) childCounts.set(String(k), (childCounts.get(String(k)) ?? 0) + 1);
- }
- for (const p of parents) {
- const key = String(p[parentField] ?? '');
- if (key && childCounts.has(key)) {
- return { id: Number(p.id), key, childCount: childCounts.get(key)! };
- }
- }
- return null;
- }
- test.describe('A2 DELETE 引用检查实测', () => {
- test.beforeEach(async ({ authedPage }) => {
- await authedPage.goto('/#/dashboard/home', { waitUntil: 'domcontentloaded' });
- });
- test('Department 删除带 Employee 的部门 → 应 4xx', async ({ authedPage }) => {
- const target = await pickReferenced(
- authedPage,
- '/api/s0/warehouse/departments?page=1&pageSize=200&tenantId=1&factoryId=1',
- '/api/s0/warehouse/employees?page=1&pageSize=500&tenantId=1&factoryId=1',
- 'department',
- 'department',
- );
- test.skip(!target, '无可用"被员工引用的部门"样本');
- const resp = await del(
- authedPage,
- `/api/s0/warehouse/departments/${target!.id}?tenantId=1&factoryId=1`,
- );
- test.info().annotations.push({
- type: 'department',
- description: `target=${JSON.stringify(target)} resp=${resp.status} body=${JSON.stringify(resp.body).slice(0, 200)}`,
- });
- expect(resp.status, '应返回 4xx 拒绝').toBeGreaterThanOrEqual(400);
- });
- test('Location 删除带 Shelf 的库位 → 应 4xx', async ({ authedPage }) => {
- // 库位下游覆盖 LocationShelfMaster + ItemMaster.Location;前者无 S0 列表 API,
- // 改用 materials.location 求交集(命中其一即触发拒绝)
- const target = await pickReferenced(
- authedPage,
- '/api/s0/warehouse/locations?page=1&pageSize=200&tenantId=1&factoryId=1',
- '/api/s0/sales/materials?page=1&pageSize=2000&tenantId=1&factoryId=1',
- 'location',
- 'location',
- );
- test.skip(!target, '无可用"被物料/货架引用的库位"样本');
- const resp = await del(
- authedPage,
- `/api/s0/warehouse/locations/${target!.id}?tenantId=1&factoryId=1`,
- );
- test.info().annotations.push({
- type: 'location',
- description: `target=${JSON.stringify(target)} resp=${resp.status} body=${JSON.stringify(resp.body).slice(0, 200)}`,
- });
- expect(resp.status, '应返回 4xx 拒绝').toBeGreaterThanOrEqual(400);
- });
- // Material / Supplier 的运行态 DELETE 验证:未在 S0 路由下挂 SRM/Routing 子表 API,
- // 无法稳妥挑选"必有引用"的样本而不踩到孤儿。本轮仅做代码审查,不做运行态删除。
- });
|