test(qa): e2e 装配修正 + 集成/门禁测试与工具链重构
- e2e:修死选择器与蜜罐误命中、补断言、GA4/security-headers 用例诚实标注 skip 边界;新增 assert-helpers/fixtures/hydrated/primary-nav/touch-targets 等复用桩。 - 集成层:config/test/jest.integration.config.js + tests-integration/ 真库 一次性 SQLite 用例,teardown 守卫开发库指纹。 - 门禁工具链:jest.setup 归位到根、scripts/accessibility 全站 axe 节点计数、 check-brand-text-token/check-motion-constraints 机械守卫、stryker/lighthouse 配置收敛。
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* 真库集成测试的公共夹具。
|
||||
*
|
||||
* 这里没有任何 jest.mock —— 被测代码与 SQLite 之间不存在替身:
|
||||
* `@/lib/db` 走的是 src/generated/prisma 的真实 PrismaClient,
|
||||
* `@/lib/auth` 走的是真实 JWT 签名/校验(密钥由 config/test/itest/setup-files.js 注入,
|
||||
* 刻意不读 .env*)。
|
||||
*/
|
||||
import { NextRequest } from 'next/server';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { generateTokens, hashPassword } from '@/lib/auth';
|
||||
import type { FieldDefinition } from '@/lib/cms/types';
|
||||
|
||||
export { prisma };
|
||||
|
||||
let counter = 0;
|
||||
/** 同一 worker 内共享一个库文件,唯一名避免用例间互相污染 */
|
||||
export function unique(prefix: string): string {
|
||||
counter += 1;
|
||||
return `${prefix}-${Date.now().toString(36)}-${counter}`;
|
||||
}
|
||||
|
||||
/** 按 FK 依赖顺序清空全部业务表(保留表结构) */
|
||||
export async function resetDb(): Promise<void> {
|
||||
await prisma.auditLog.deleteMany();
|
||||
await prisma.notification.deleteMany();
|
||||
await prisma.contentItem.deleteMany();
|
||||
await prisma.contentZone.deleteMany();
|
||||
await prisma.mediaAsset.deleteMany();
|
||||
await prisma.permission.deleteMany();
|
||||
await prisma.userRole.deleteMany();
|
||||
await prisma.role.deleteMany();
|
||||
await prisma.contentModel.deleteMany();
|
||||
await prisma.user.deleteMany();
|
||||
}
|
||||
|
||||
export async function seedRole(code: string, permissions: Array<{ modelCode: string; action: string }> = []): Promise<void> {
|
||||
await prisma.role.create({ data: { code, name: code } });
|
||||
for (const p of permissions) {
|
||||
await prisma.permission.create({ data: { roleCode: code, modelCode: p.modelCode, action: p.action } });
|
||||
}
|
||||
}
|
||||
|
||||
export interface SeededUser {
|
||||
id: string;
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export async function seedUser(
|
||||
username: string,
|
||||
roleCodes: string[],
|
||||
options: { password?: string; status?: number } = {},
|
||||
): Promise<SeededUser> {
|
||||
const password = options.password ?? 'Integration-test-2026';
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
username,
|
||||
// 真实 bcrypt 哈希:集成层不 mock @/lib/auth,登录/刷新走的是真校验
|
||||
password: await hashPassword(password),
|
||||
status: options.status ?? 1,
|
||||
role: roleCodes[0] ?? 'readonly',
|
||||
},
|
||||
});
|
||||
for (const roleCode of roleCodes) {
|
||||
await prisma.userRole.create({ data: { userId: user.id, roleCode } });
|
||||
}
|
||||
return { id: user.id, username: user.username, password };
|
||||
}
|
||||
|
||||
/** 真实签名的 access token(与 src/lib/auth 的 verifyAccessToken 对称) */
|
||||
export function accessTokenFor(user: SeededUser, role = 'admin'): string {
|
||||
return generateTokens({ userId: user.id, username: user.username, role }).accessToken;
|
||||
}
|
||||
|
||||
export function refreshTokenFor(user: SeededUser, role = 'admin'): string {
|
||||
return generateTokens({ userId: user.id, username: user.username, role }).refreshToken;
|
||||
}
|
||||
|
||||
export interface RequestOptions {
|
||||
url: string;
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||
body?: unknown;
|
||||
token?: string;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造真实 NextRequest —— 与仓库里既有的 route.test.ts 同一入口形态,
|
||||
* 但这里不替换 headers/url/json,NextRequest 自己解析 cookie 与 searchParams。
|
||||
*/
|
||||
export function makeRequest(options: RequestOptions): NextRequest {
|
||||
const method = options.method ?? 'GET';
|
||||
const withBody = options.body !== undefined && method !== 'GET';
|
||||
const headers: Record<string, string> = { ...options.headers };
|
||||
if (options.token) headers.authorization = `Bearer ${options.token}`;
|
||||
if (withBody) headers['content-type'] = 'application/json';
|
||||
|
||||
// 字面量直接内联,让 init 按 next/server 自己的 RequestInit 形状做上下文推断
|
||||
return new NextRequest(options.url, {
|
||||
method,
|
||||
headers,
|
||||
...(withBody ? { body: JSON.stringify(options.body) } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
export const API = {
|
||||
items: 'http://localhost:3000/api/admin/items',
|
||||
users: 'http://localhost:3000/api/admin/users',
|
||||
roles: 'http://localhost:3000/api/admin/roles',
|
||||
refresh: 'http://localhost:3000/api/auth/refresh',
|
||||
workflow: (id: string): string =>
|
||||
`http://localhost:3000/api/admin/items/${id}/workflow`,
|
||||
};
|
||||
|
||||
/** 读取 ContentItem.data 的**原始列值**(绕过任何解析,证明落库内容) */
|
||||
export async function rawDataColumn(id: string): Promise<string> {
|
||||
const rows = await prisma.$queryRawUnsafe<Array<{ data: string }>>(
|
||||
`SELECT data FROM "ContentItem" WHERE id = ?`,
|
||||
id,
|
||||
);
|
||||
return rows[0]?.data ?? '';
|
||||
}
|
||||
|
||||
/** 以给定字段声明建一个内容模型,返回 { id, code } */
|
||||
export async function seedModel(code: string, fields: FieldDefinition[]): Promise<{ id: string; code: string }> {
|
||||
const model = await prisma.contentModel.create({
|
||||
data: { code, name: code, fields: JSON.stringify(fields) },
|
||||
});
|
||||
return { id: model.id, code: model.code };
|
||||
}
|
||||
|
||||
export async function disconnect(): Promise<void> {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
Reference in New Issue
Block a user