chore: 初始化TikTok短剧后端项目基础架构
创建了完整的NestJS项目结构,包含配置文件、核心模块、工具类、拦截器、过滤器等基础代码,实现了健康检查、用户认证、管理员RBAC、剧集管理、订单支付、媒体管理等核心功能的基础框架,配置了ESLint、TypeScript、Jest等开发工具链。
This commit is contained in:
375
test/admin-permissions-users.e2e-spec.ts
Normal file
375
test/admin-permissions-users.e2e-spec.ts
Normal file
@@ -0,0 +1,375 @@
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import * as request from 'supertest';
|
||||
import { AppModule } from '../src/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',
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user