Files
novalon-website/tests-integration/helpers/harness.ts
T
zhangxiang 6bb7c557ee 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
  配置收敛。
2026-09-28 10:48:08 +08:00

136 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 真库集成测试的公共夹具。
*
* 这里没有任何 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();
}