/** * 真库集成测试的公共夹具。 * * 这里没有任何 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 { 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 { 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 { 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; } /** * 构造真实 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 = { ...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 { const rows = await prisma.$queryRawUnsafe>( `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 { await prisma.$disconnect(); }