Files
delivery-desk/scripts/test-postgres-runtime.ts
yuzhe b0c498fbb6 feat(auth): 添加认证模块和图片批注功能(项目初始化)
- 实现用户登录、登出、密码修改等认证功能
- 添加会话管理和权限控制中间件
- 创建图片批注组件和相关API路由
- 实现批注的增删改查功能
- 添加Docker和Git忽略配置文件
- 创建系统架构文档和开发约定说明
- 集成认证模块到前端应用路由中
2026-07-21 15:28:55 +08:00

101 lines
10 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.
process.env.NODE_ENV = 'test';
process.env.DATABASE_URL = 'pg-mem://';
process.env.INITIAL_ADMIN_PASSWORD = 'AdminTest123!';
process.env.COS_CONFIG_ENCRYPTION_KEY = 'test-only-encryption-key-at-least-32-characters';
const { default: app } = await import('../api/app.js');
const { closeDatabase } = await import('../api/database.js');
const server = app.listen(0, '127.0.0.1');
await new Promise<void>((resolve) => server.once('listening', resolve));
const address = server.address();
if (!address || typeof address === 'string') throw new Error('测试服务启动失败');
const base = `http://127.0.0.1:${address.port}`;
async function request(path: string, init: RequestInit = {}, cookie?: string) {
const response = await fetch(`${base}${path}`, { ...init, headers: { ...(init.headers || {}), ...(cookie ? { Cookie: cookie } : {}) } });
const body = response.status === 204 ? null : await response.json();
return { response, body };
}
function expectStatus(actual: number, expected: number, label: string, body?: unknown) {
if (actual !== expected) throw new Error(`${label}: 期望 ${expected},实际 ${actual},响应 ${JSON.stringify(body)}`);
}
try {
const health = await request('/api/health'); expectStatus(health.response.status, 200, '健康检查');
if ((health.body as { database?: string }).database !== 'postgres') throw new Error('测试未运行在 PostgreSQL 查询层');
const login = await request('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: 'admin', password: 'AdminTest123!' }) });
expectStatus(login.response.status, 200, '平台管理员登录');
const adminCookie = login.response.headers.get('set-cookie')?.split(';')[0];
if (!adminCookie) throw new Error('登录未返回会话 Cookie');
const group = await request('/api/management/groups', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: '测试运营组', username: 'test_manager', display_name: '测试管理员', password: 'Manager123!' }) }, adminCookie);
expectStatus(group.response.status, 201, '创建运营组');
const groupId = Number((group.body as { id: number }).id);
const duplicateGroupAdmin = await request('/api/management/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ group_id: groupId, username: 'second_manager', display_name: '林溪', password: 'Manager123!', role: 'group_admin' }) }, adminCookie);
expectStatus(duplicateGroupAdmin.response.status, 409, '拒绝第二位组管理员', duplicateGroupAdmin.body);
const firstOperator = await request('/api/management/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ group_id: groupId, username: 'first_operator', display_name: '林溪', password: 'Operator123!', role: 'operator' }) }, adminCookie);
expectStatus(firstOperator.response.status, 201, '创建第一位光影叙事', firstOperator.body);
const secondOperator = await request('/api/management/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ group_id: groupId, username: 'second_operator', display_name: '周宁', password: 'Operator123!', role: 'operator' }) }, adminCookie);
expectStatus(secondOperator.response.status, 201, '创建第二位光影叙事', secondOperator.body);
const secondPlatformAdmin = await request('/api/management/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: 'second_admin', display_name: '平台管理员乙', password: 'AdminTest123!', role: 'platform_admin' }) }, adminCookie);
expectStatus(secondPlatformAdmin.response.status, 201, '创建第二位平台管理员', secondPlatformAdmin.body);
const renameOperator = await request(`/api/management/users/${Number((firstOperator.body as { id: number }).id)}/name`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ display_name: '林清溪' }) }, adminCookie);
expectStatus(renameOperator.response.status, 200, '修改光影叙事用户名', renameOperator.body);
const groupAccounts = await request(`/api/management/users?groupId=${groupId}`, {}, adminCookie);
expectStatus(groupAccounts.response.status, 200, '读取组内账号', groupAccounts.body);
const groupUsers = groupAccounts.body as Array<{ display_name: string; role: string }>;
if (groupUsers.filter((item) => item.role === 'group_admin').length !== 1 || groupUsers.filter((item) => item.role === 'operator').length !== 2) throw new Error('运营组账号数量规则未正确执行');
const allAccounts = await request('/api/management/users', {}, adminCookie);
expectStatus(allAccounts.response.status, 200, '读取平台账号', allAccounts.body);
if ((allAccounts.body as Array<{ role: string }>).filter((item) => item.role === 'platform_admin').length !== 2) throw new Error('多个平台管理员未正确创建');
const replaceAdmin = await request(`/api/management/groups/${groupId}/replace-admin`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: Number((firstOperator.body as { id: number }).id), previous_action: 'demote' }) }, adminCookie);
expectStatus(replaceAdmin.response.status, 200, '更换组管理员', replaceAdmin.body);
const replacedAccounts = await request(`/api/management/users?groupId=${groupId}`, {}, adminCookie);
const replacedUsers = replacedAccounts.body as Array<{ id:number;role:string;last_login_at:string|null }>;
if (replacedUsers.filter((item)=>item.role==='group_admin').length!==1 || replacedUsers.find((item)=>item.role==='group_admin')?.id!==Number((firstOperator.body as {id:number}).id)) throw new Error('组管理员更换未保持唯一身份');
const groupsAfterReplace = await request('/api/management/groups', {}, adminCookie);
expectStatus(groupsAfterReplace.response.status, 200, '读取运营组统计', groupsAfterReplace.body);
const groupStats=(groupsAfterReplace.body as Array<{id:number;user_count:number;operator_count:number;group_admin_name:string|null}>).find((item)=>item.id===groupId);
if(!groupStats||groupStats.user_count!==3||groupStats.operator_count!==2||!groupStats.group_admin_name)throw new Error('运营组统计不正确');
const renameGroup=await request(`/api/management/groups/${groupId}`,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:'已更名运营组'})},adminCookie);
expectStatus(renameGroup.response.status,200,'修改运营组名称',renameGroup.body);
if((renameGroup.body as {name:string}).name!=='已更名运营组')throw new Error('运营组名称未正确更新');
const newAdminLogin=await request('/api/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:'first_operator',password:'Operator123!'})});
expectStatus(newAdminLogin.response.status,200,'新组管理员登录',newAdminLogin.body);
const accountsAfterLogin=await request(`/api/management/users?groupId=${groupId}`,{},adminCookie);
if(!(accountsAfterLogin.body as Array<{username:string;last_login_at:string|null}>).find((item)=>item.username==='first_operator')?.last_login_at)throw new Error('最近登录时间未记录');
const adminLogs=await request('/api/management/audit-logs?userId=1',{},adminCookie);
expectStatus(adminLogs.response.status,200,'按账号筛选审计日志',adminLogs.body);
if(!(adminLogs.body as Array<{action:string}>).some((item)=>item.action==='group.admin_replace'))throw new Error('管理员更换审计日志缺失');
const project = await request('/api/projects', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'PostgreSQL 联调项目', slug: 'postgres-runtime-test', client_description: '运行时验证', groupId }) }, adminCookie);
expectStatus(project.response.status, 201, '创建项目', project.body);
if((project.body as {group_name?:string}).group_name!=='已更名运营组')throw new Error('项目接口未返回所属运营组名称');
const projectId = Number((project.body as { id: number }).id);
const access = await request(`/api/projects/${projectId}/customer-access`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: true, password: 'Review123!' }) }, adminCookie);
expectStatus(access.response.status, 200, '配置客户访问');
const collection = await request(`/api/projects/${projectId}/collections`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: '第一阶段', client_description: '验收阶段' }) }, adminCookie);
expectStatus(collection.response.status, 201, '创建作品交付集');
const reviewLogin = await request('/api/review/postgres-runtime-test/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ reviewer_name: '客户测试', password: 'Review123!' }) });
expectStatus(reviewLogin.response.status, 200, '客户登录');
const reviewCookie = reviewLogin.response.headers.get('set-cookie')?.split(';')[0];
const reviewProject = await request('/api/review/postgres-runtime-test/project', {}, reviewCookie);
expectStatus(reviewProject.response.status, 200, '客户项目读取');
const apiKey = await request('/api/management/api-keys', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: '运行时测试 Key' }) }, adminCookie);
expectStatus(apiKey.response.status, 201, '创建平台 API Key');
const keyId = Number((apiKey.body as { item: { id: number } }).item.id);
const revoke = await request(`/api/management/api-keys/${keyId}`, { method: 'DELETE' }, adminCookie);
expectStatus(revoke.response.status, 204, '吊销平台 API Key');
process.stdout.write('PostgreSQL 运行时验证通过单组管理员、多光影叙事、多平台管理员、管理员更换、活动时间、审计筛选、项目、客户门禁、作品交付集、API Key\n');
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
await closeDatabase();
}