import { INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import * as request from 'supertest'; import { AppModule } from '../src/app/app.module'; import { bootstrapApp } from '../src/bootstrap'; import { loginAdmin } from './test-helpers'; describe('Admin permissions and user management', () => { let app: INestApplication; let superAdminToken: string; beforeAll(async () => { const moduleRef = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleRef.createNestApplication(); bootstrapApp(app); await app.init(); superAdminToken = await loginAdmin(app); }); afterAll(async () => { await app.close(); }); it('requires admin authentication for admin APIs', async () => { const response = await request(app.getHttpServer()) .get('/api/admin/v1/users') .expect(401); expect(response.body).toMatchObject({ code: 401, data: {}, message: 'Admin not authenticated', }); }); it('creates permissions, roles, admin users, and enforces permission checks', async () => { const permissions = await request(app.getHttpServer()) .get('/api/admin/v1/permissions') .set('Authorization', `Bearer ${superAdminToken}`) .expect(200); expect(permissions.body.data.list).toEqual( expect.arrayContaining([ expect.objectContaining({ code: 'permission:read', name: '查看权限', }), expect.objectContaining({ code: 'permission:create', name: '创建权限', }), ]), ); const dramaReadPermission = permissions.body.data.list.find( (permission: { code: string }) => permission.code === 'drama:read', ); const role = await request(app.getHttpServer()) .post('/api/admin/v1/roles') .set('Authorization', `Bearer ${superAdminToken}`) .send({ code: 'drama_viewer', name: 'Drama Viewer', description: 'Can only view dramas', permissionIds: [dramaReadPermission.id], }) .expect(201); const admin = await request(app.getHttpServer()) .post('/api/admin/v1/admin-users') .set('Authorization', `Bearer ${superAdminToken}`) .send({ username: 'viewer', password: 'viewer123', nickname: 'Viewer', email: 'viewer@example.com', roleIds: [role.body.data.id], }) .expect(201); expect(admin.body.data).toMatchObject({ username: 'viewer', nickname: 'Viewer', email: 'viewer@example.com', status: 'active', roles: [expect.objectContaining({ code: 'drama_viewer' })], }); expect(admin.body.data.passwordHash).toBeUndefined(); const viewerLogin = await request(app.getHttpServer()) .post('/api/admin/v1/auth/login') .send({ username: 'viewer', password: 'viewer123' }) .expect(201); await request(app.getHttpServer()) .get('/api/admin/v1/dramas') .set('Authorization', `Bearer ${viewerLogin.body.data.accessToken}`) .expect(200); const forbidden = await request(app.getHttpServer()) .post('/api/admin/v1/dramas') .set('Authorization', `Bearer ${viewerLogin.body.data.accessToken}`) .send({ title: 'Forbidden Create', coverUrl: 'https://cdn.example.com/forbidden.jpg', type: 'test', status: 'draft', }) .expect(403); expect(forbidden.body).toMatchObject({ code: 403, data: {}, message: 'No permission', }); }); it('updates role permissions from the role permission assignment endpoint', async () => { const permissions = await request(app.getHttpServer()) .get('/api/admin/v1/permissions') .set('Authorization', `Bearer ${superAdminToken}`) .expect(200); const dramaReadPermission = permissions.body.data.list.find( (permission: { code: string }) => permission.code === 'drama:read', ); const role = await request(app.getHttpServer()) .post('/api/admin/v1/roles') .set('Authorization', `Bearer ${superAdminToken}`) .send({ code: 'assignable_role', name: 'Assignable Role', description: 'Role permissions can be assigned later', permissionIds: [], }) .expect(201); const updatedRole = await request(app.getHttpServer()) .patch(`/api/admin/v1/roles/${role.body.data.id}/permissions`) .set('Authorization', `Bearer ${superAdminToken}`) .send({ permissionIds: [dramaReadPermission.id] }) .expect(200); expect(updatedRole.body.data).toMatchObject({ id: role.body.data.id, permissionIds: [dramaReadPermission.id], permissions: [expect.objectContaining({ code: 'drama:read' })], }); }); it('creates roles with assigned permissions and rejects duplicate role codes', async () => { const permissions = await request(app.getHttpServer()) .get('/api/admin/v1/permissions') .set('Authorization', `Bearer ${superAdminToken}`) .expect(200); const roleReadPermission = permissions.body.data.list.find( (permission: { code: string }) => permission.code === 'role:read', ); const roleCreatePermission = permissions.body.data.list.find( (permission: { code: string }) => permission.code === 'role:create', ); const createdRole = await request(app.getHttpServer()) .post('/api/admin/v1/roles') .set('Authorization', `Bearer ${superAdminToken}`) .send({ code: 'system_operator', name: 'System Operator', description: 'Can manage system roles', permissionIds: [roleReadPermission.id, roleCreatePermission.id], }) .expect(201); expect(createdRole.body.data).toMatchObject({ code: 'system_operator', name: 'System Operator', permissionIds: [roleReadPermission.id, roleCreatePermission.id], permissions: [ expect.objectContaining({ code: 'role:read' }), expect.objectContaining({ code: 'role:create' }), ], }); const duplicate = await request(app.getHttpServer()) .post('/api/admin/v1/roles') .set('Authorization', `Bearer ${superAdminToken}`) .send({ code: 'system_operator', name: 'Duplicated System Operator', }) .expect(409); expect(duplicate.body).toMatchObject({ code: 409, data: {}, message: 'Role code already exists', }); }); it('allows the current admin to change password with the old password', async () => { const admin = await request(app.getHttpServer()) .post('/api/admin/v1/admin-users') .set('Authorization', `Bearer ${superAdminToken}`) .send({ username: 'password-user', password: 'oldpass123', nickname: 'Password User', email: 'password-user@example.com', }) .expect(201); expect(admin.body.data.passwordHash).toBeUndefined(); const login = await request(app.getHttpServer()) .post('/api/admin/v1/auth/login') .send({ username: 'password-user', password: 'oldpass123' }) .expect(201); await request(app.getHttpServer()) .patch('/api/admin/v1/auth/password') .set('Authorization', `Bearer ${login.body.data.accessToken}`) .send({ oldPassword: 'wrongpass', newPassword: 'newpass123' }) .expect(422); const changed = await request(app.getHttpServer()) .patch('/api/admin/v1/auth/password') .set('Authorization', `Bearer ${login.body.data.accessToken}`) .send({ oldPassword: 'oldpass123', newPassword: 'newpass123' }) .expect(200); expect(changed.body.data).toEqual({}); await request(app.getHttpServer()) .post('/api/admin/v1/auth/login') .send({ username: 'password-user', password: 'oldpass123' }) .expect(401); await request(app.getHttpServer()) .post('/api/admin/v1/auth/login') .send({ username: 'password-user', password: 'newpass123' }) .expect(201); }); it('allows the current admin to view and update profile information', async () => { await request(app.getHttpServer()) .post('/api/admin/v1/admin-users') .set('Authorization', `Bearer ${superAdminToken}`) .send({ username: 'profile-user', password: 'profile123', nickname: 'Profile User', email: 'profile-user@example.com', }) .expect(201); const login = await request(app.getHttpServer()) .post('/api/admin/v1/auth/login') .send({ username: 'profile-user', password: 'profile123' }) .expect(201); const updated = await request(app.getHttpServer()) .patch('/api/admin/v1/auth/profile') .set('Authorization', `Bearer ${login.body.data.accessToken}`) .send({ nickname: 'Updated Profile User', email: 'updated-profile-user@example.com', }) .expect(200); expect(updated.body.data).toMatchObject({ username: 'profile-user', nickname: 'Updated Profile User', email: 'updated-profile-user@example.com', }); expect(updated.body.data.passwordHash).toBeUndefined(); const profile = await request(app.getHttpServer()) .get('/api/admin/v1/auth/profile') .set('Authorization', `Bearer ${login.body.data.accessToken}`) .expect(200); expect(profile.body.data).toMatchObject({ username: 'profile-user', nickname: 'Updated Profile User', email: 'updated-profile-user@example.com', }); }); it('assigns roles to existing admin users', async () => { const permissions = await request(app.getHttpServer()) .get('/api/admin/v1/permissions') .set('Authorization', `Bearer ${superAdminToken}`) .expect(200); const userReadPermission = permissions.body.data.list.find( (permission: { code: string }) => permission.code === 'user:read', ); const role = await request(app.getHttpServer()) .post('/api/admin/v1/roles') .set('Authorization', `Bearer ${superAdminToken}`) .send({ code: 'user_reader', name: 'User Reader', permissionIds: [userReadPermission.id], }) .expect(201); const admin = await request(app.getHttpServer()) .post('/api/admin/v1/admin-users') .set('Authorization', `Bearer ${superAdminToken}`) .send({ username: 'role-assignee', password: 'assign123', nickname: 'Role Assignee', email: 'role-assignee@example.com', roleIds: [], }) .expect(201); const assigned = await request(app.getHttpServer()) .patch(`/api/admin/v1/admin-users/${admin.body.data.id}/roles`) .set('Authorization', `Bearer ${superAdminToken}`) .send({ roleIds: [role.body.data.id] }) .expect(200); expect(assigned.body.data).toMatchObject({ id: admin.body.data.id, roles: [expect.objectContaining({ code: 'user_reader' })], }); }); it('lists and updates app users from admin APIs', async () => { await request(app.getHttpServer()) .post('/api/app/v1/auth/tiktok-login') .send({ code: 'user-management-code', mockOpenId: 'open-user-management', nickname: 'Managed User', }) .expect(201); const users = await request(app.getHttpServer()) .get('/api/admin/v1/users') .set('Authorization', `Bearer ${superAdminToken}`) .expect(200); expect(users.body.data.list).toEqual( expect.arrayContaining([ expect.objectContaining({ tiktokOpenId: 'open-user-management', nickname: 'Managed User', status: 'active', }), ]), ); const managedUser = users.body.data.list.find( (user: { tiktokOpenId: string }) => user.tiktokOpenId === 'open-user-management', ); const disabled = await request(app.getHttpServer()) .patch(`/api/admin/v1/users/${managedUser.id}/status`) .set('Authorization', `Bearer ${superAdminToken}`) .send({ status: 'disabled' }) .expect(200); expect(disabled.body.data).toMatchObject({ id: managedUser.id, status: 'disabled', }); }); });