302 lines
10 KiB
JavaScript
302 lines
10 KiB
JavaScript
const { test, expect, request } = require('@playwright/test');
|
||
const { cleanDatabase, seedExistingUser, seedAdmin, disconnect, prisma } = require('./fixtures/database');
|
||
|
||
test.describe('管理员审核 + Hermes 同步流程', () => {
|
||
let adminToken;
|
||
let userToken;
|
||
let roleId;
|
||
|
||
test.beforeAll(async () => {
|
||
await cleanDatabase();
|
||
await seedExistingUser();
|
||
await seedAdmin();
|
||
|
||
// 管理员登录
|
||
const adminContext = await request.newContext();
|
||
const adminRes = await adminContext.post('/api/admin-auth/login', {
|
||
data: { account: 'admin', password: 'admin123' },
|
||
});
|
||
const adminData = await adminRes.json();
|
||
adminToken = adminData.token;
|
||
|
||
// 用户登录
|
||
const userContext = await request.newContext();
|
||
const userRes = await userContext.post('/api/auth/login', {
|
||
data: { account: 'e2e_existing', password: 'Test123456' },
|
||
});
|
||
const userData = await userRes.json();
|
||
userToken = userData.token;
|
||
|
||
// 创建角色(待审核)
|
||
const roleRes = await userContext.post('/api/roles', {
|
||
headers: { Authorization: `Bearer ${userToken}` },
|
||
data: {
|
||
displayName: '测试角色',
|
||
personality: '温柔体贴',
|
||
background: '来自未来',
|
||
speechStyle: '轻声细语',
|
||
greeting: '你好呀',
|
||
soulMd: '# SOUL\n这是测试角色的灵魂文件',
|
||
},
|
||
});
|
||
const roleData = await roleRes.json();
|
||
roleId = roleData.role.id;
|
||
});
|
||
|
||
test.afterAll(async () => {
|
||
await cleanDatabase();
|
||
await disconnect();
|
||
});
|
||
|
||
test('管理员登录成功', async () => {
|
||
expect(adminToken).toBeTruthy();
|
||
});
|
||
|
||
test('管理员登录密码错误返回 401', async () => {
|
||
const context = await request.newContext();
|
||
const res = await context.post('/api/admin-auth/login', {
|
||
data: { account: 'admin', password: 'wrong' },
|
||
});
|
||
expect(res.status()).toBe(401);
|
||
});
|
||
|
||
test('用户 JWT 不能访问管理员接口', async () => {
|
||
const context = await request.newContext();
|
||
const res = await context.get('/api/admin/reviews', {
|
||
headers: { Authorization: `Bearer ${userToken}` },
|
||
});
|
||
expect(res.status()).toBe(401);
|
||
});
|
||
|
||
test('无 token 不能访问管理员接口', async () => {
|
||
const context = await request.newContext();
|
||
const res = await context.get('/api/admin/reviews');
|
||
expect(res.status()).toBe(401);
|
||
});
|
||
|
||
test('创建角色后状态为 pending_review', async () => {
|
||
const role = await prisma.role.findUnique({ where: { id: roleId } });
|
||
expect(role.reviewStatus).toBe('pending_review');
|
||
});
|
||
|
||
test('管理员获取待审核列表', async () => {
|
||
const context = await request.newContext();
|
||
const res = await context.get('/api/admin/reviews', {
|
||
headers: { Authorization: `Bearer ${adminToken}` },
|
||
});
|
||
expect(res.status()).toBe(200);
|
||
const data = await res.json();
|
||
expect(data.roles.length).toBeGreaterThan(0);
|
||
expect(data.roles[0].reviewStatus).toBe('pending_review');
|
||
});
|
||
|
||
test('管理员获取角色详情', async () => {
|
||
const context = await request.newContext();
|
||
const res = await context.get(`/api/admin/reviews/${roleId}`, {
|
||
headers: { Authorization: `Bearer ${adminToken}` },
|
||
});
|
||
expect(res.status()).toBe(200);
|
||
const data = await res.json();
|
||
expect(data.role.displayName).toBe('测试角色');
|
||
});
|
||
|
||
test('管理员通过审核', async () => {
|
||
const context = await request.newContext();
|
||
const res = await context.post(`/api/admin/reviews/${roleId}/approve`, {
|
||
headers: { Authorization: `Bearer ${adminToken}` },
|
||
});
|
||
expect(res.status()).toBe(200);
|
||
const data = await res.json();
|
||
expect(data.role.reviewStatus).toBe('approved');
|
||
});
|
||
|
||
test('角色库不显示未同步的角色', async () => {
|
||
const context = await request.newContext();
|
||
const res = await context.get('/api/roles');
|
||
const data = await res.json();
|
||
const found = data.roles.find((r) => r.id === roleId);
|
||
expect(found).toBeUndefined();
|
||
});
|
||
|
||
test('对非 approved 角色发起同步返回 400', async () => {
|
||
// 先创建一个新角色(pending_review)
|
||
const userContext = await request.newContext();
|
||
await userContext.post('/api/roles', {
|
||
headers: { Authorization: `Bearer ${userToken}` },
|
||
data: {
|
||
displayName: '未审核角色',
|
||
personality: '测试',
|
||
background: '测试',
|
||
speechStyle: '测试',
|
||
greeting: '测试',
|
||
soulMd: '# test',
|
||
},
|
||
});
|
||
|
||
const roles = await prisma.role.findMany({ where: { displayName: '未审核角色' } });
|
||
const pendingRoleId = roles[0].id;
|
||
|
||
const adminContext = await request.newContext();
|
||
const res = await adminContext.post(`/api/admin/sync/${pendingRoleId}`, {
|
||
headers: { Authorization: `Bearer ${adminToken}` },
|
||
data: { profileName: 'test-profile' },
|
||
});
|
||
expect(res.status()).toBe(400);
|
||
});
|
||
|
||
test('未配置 Hermes webhook URL 时同步返回 400', async () => {
|
||
const context = await request.newContext();
|
||
const res = await context.post(`/api/admin/sync/${roleId}`, {
|
||
headers: { Authorization: `Bearer ${adminToken}` },
|
||
data: { profileName: 'test-profile' },
|
||
});
|
||
expect(res.status()).toBe(400);
|
||
});
|
||
|
||
test('配置 Hermes webhook URL 指向 mock', async () => {
|
||
const context = await request.newContext();
|
||
const res = await context.put('/api/admin/config/HERMES_WEBHOOK_URL', {
|
||
headers: { Authorization: `Bearer ${adminToken}` },
|
||
data: { value: 'http://localhost:3001/api/mock-hermes/sync' },
|
||
});
|
||
expect(res.status()).toBe(200);
|
||
});
|
||
|
||
test('管理员发起同步 → mock Hermes 回调拉取文件 → 返回二维码', async () => {
|
||
const context = await request.newContext();
|
||
const res = await context.post(`/api/admin/sync/${roleId}`, {
|
||
headers: { Authorization: `Bearer ${adminToken}` },
|
||
data: {
|
||
profileName: 'test-profile',
|
||
modelKey: 'sk-test-key',
|
||
provider: 'openrouter',
|
||
multimediaModelKey: 'sk-multi-key',
|
||
multimediaProvider: 'openrouter',
|
||
enableSchedule: false,
|
||
},
|
||
});
|
||
|
||
expect(res.status()).toBe(200);
|
||
const data = await res.json();
|
||
expect(data.role.reviewStatus).toBe('synced');
|
||
expect(data.role.qrCodeUrl).toContain('mock.hermes.local');
|
||
expect(data.profileId).toContain('mock-profile');
|
||
});
|
||
|
||
test('同步成功后角色出现在角色库', async () => {
|
||
const context = await request.newContext();
|
||
const res = await context.get('/api/roles');
|
||
const data = await res.json();
|
||
const found = data.roles.find((r) => r.id === roleId);
|
||
expect(found).toBeDefined();
|
||
});
|
||
|
||
test('用户查看自己的角色列表包含审核状态和二维码', async () => {
|
||
const context = await request.newContext();
|
||
const res = await context.get('/api/roles/my/roles', {
|
||
headers: { Authorization: `Bearer ${userToken}` },
|
||
});
|
||
const data = await res.json();
|
||
const role = data.roles.find((r) => r.id === roleId);
|
||
expect(role).toBeDefined();
|
||
expect(role.reviewStatus).toBe('synced');
|
||
expect(role.qrCodeUrl).toBeTruthy();
|
||
});
|
||
|
||
test('sync_token 拉取文件 — SOUL.md', async () => {
|
||
// 生成新的 sync_token(通过管理员发起同步流程会自动生成)
|
||
// 这里直接用 API 测试:先通过审核另一个角色,再同步
|
||
const userContext = await request.newContext();
|
||
|
||
// 创建并审核新角色
|
||
const roleRes = await userContext.post('/api/roles', {
|
||
headers: { Authorization: `Bearer ${userToken}` },
|
||
data: {
|
||
displayName: '同步测试角色',
|
||
personality: '测试',
|
||
background: '测试',
|
||
speechStyle: '测试',
|
||
greeting: '测试',
|
||
soulMd: '# Test SOUL\n测试内容',
|
||
},
|
||
});
|
||
const newRoleId = (await roleRes.json()).role.id;
|
||
|
||
// 管理员审核通过
|
||
const adminContext = await request.newContext();
|
||
await adminContext.post(`/api/admin/reviews/${newRoleId}/approve`, {
|
||
headers: { Authorization: `Bearer ${adminToken}` },
|
||
});
|
||
|
||
// 发起同步(mock Hermes 会回调拉取文件)
|
||
const syncRes = await adminContext.post(`/api/admin/sync/${newRoleId}`, {
|
||
headers: { Authorization: `Bearer ${adminToken}` },
|
||
data: {
|
||
profileName: 'test-profile-2',
|
||
modelKey: 'sk-test',
|
||
provider: 'openrouter',
|
||
},
|
||
});
|
||
|
||
expect(syncRes.status()).toBe(200);
|
||
const syncData = await syncRes.json();
|
||
expect(syncData.role.reviewStatus).toBe('synced');
|
||
});
|
||
|
||
test('驳回审核流程', async () => {
|
||
const userContext = await request.newContext();
|
||
const roleRes = await userContext.post('/api/roles', {
|
||
headers: { Authorization: `Bearer ${userToken}` },
|
||
data: {
|
||
displayName: '待驳回角色',
|
||
personality: '测试',
|
||
background: '测试',
|
||
speechStyle: '测试',
|
||
greeting: '测试',
|
||
soulMd: '# test',
|
||
},
|
||
});
|
||
const rejectRoleId = (await roleRes.json()).role.id;
|
||
|
||
const adminContext = await request.newContext();
|
||
const res = await adminContext.post(`/api/admin/reviews/${rejectRoleId}/reject`, {
|
||
headers: { Authorization: `Bearer ${adminToken}` },
|
||
data: { reviewNote: '内容不符合要求' },
|
||
});
|
||
|
||
expect(res.status()).toBe(200);
|
||
const data = await res.json();
|
||
expect(data.role.reviewStatus).toBe('rejected');
|
||
expect(data.role.reviewNote).toBe('内容不符合要求');
|
||
});
|
||
|
||
test('向后兼容:API Key 拉取仍正常工作', async () => {
|
||
// 生成 API Key
|
||
const userContext = await request.newContext();
|
||
const keyRes = await userContext.post('/api/apikeys', {
|
||
headers: { Authorization: `Bearer ${userToken}` },
|
||
data: { name: 'test-key' },
|
||
});
|
||
const apiKey = (await keyRes.json()).apiKey.key;
|
||
|
||
// 用 API Key 拉取 SOUL.md
|
||
const soulRes = await userContext.get(`/api/hermes/roles/${roleId}/SOUL.md`, {
|
||
headers: { Authorization: `Bearer ${apiKey}` },
|
||
});
|
||
expect(soulRes.status()).toBe(200);
|
||
const soulText = await soulRes.text();
|
||
expect(soulText).toContain('测试角色的灵魂文件');
|
||
});
|
||
|
||
test('管理员获取同步状态列表', async () => {
|
||
const context = await request.newContext();
|
||
const res = await context.get('/api/admin/sync-status', {
|
||
headers: { Authorization: `Bearer ${adminToken}` },
|
||
});
|
||
expect(res.status()).toBe(200);
|
||
const data = await res.json();
|
||
expect(data.roles.length).toBeGreaterThan(0);
|
||
expect(data.roles.some((r) => r.reviewStatus === 'synced')).toBe(true);
|
||
});
|
||
});
|