Files
ai-learning-platform/backend/src/modules/admin/tests/admin.service.spec.ts
T
yuzhiran-dev 538de50bb1 注册支持用户名/手机号/邮箱 + 镜像站部署脚本 + AI 助手 Tool Calling 重构
- Prisma User 模型新增 username 字段(唯一索引)
- 注册先查重复再创建,返回友好中文提示(非 500)
- 登录支持用户名/手机号/邮箱三种方式
- 前端注册表单增加用户名输入框,预校验 2-20 位格式
- 新增 scripts/deploy.sh:一键构建并部署主站+镜像站+重启后端+重载 Nginx
- 镜像站 www.yuzhiran.com.cn Nginx 配置与主站同步
- AI 助手架构升级:用户端/管理后台均采用完整 Tool Calling 架构
- 新增 UserAiAssistantService(18 工具)+ AiAssistantController
- admin 助手新增 search + mark-all-notifications-read 工具
- 修复注册 500 错误:catch Prisma P2002 → BadRequestException
- Baidu Analytics Script 注入 root layout
2026-06-01 23:14:42 +08:00

173 lines
5.6 KiB
TypeScript
Executable File

import { Test, TestingModule } from '@nestjs/testing';
import { UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcryptjs';
import { AdminService } from '../admin.service';
import { PrismaService } from '../../../prisma/prisma.service';
// Mock bcrypt
jest.mock('bcryptjs', () => ({
compare: jest.fn(),
hash: jest.fn(),
}));
describe('AdminService', () => {
let service: AdminService;
let prisma: PrismaService;
const mockPrisma = {
adminUser: {
findUnique: jest.fn(),
update: jest.fn(),
},
user: { count: jest.fn() },
course: { count: jest.fn() },
content: { count: jest.fn() },
prompt: { count: jest.fn() },
order: { count: jest.fn() },
adminLog: { create: jest.fn() },
};
const mockJwtService = {
sign: jest.fn().mockReturnValue('mock-jwt-token'),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AdminService,
{ provide: PrismaService, useValue: mockPrisma },
{ provide: JwtService, useValue: mockJwtService },
],
}).compile();
service = module.get<AdminService>(AdminService);
prisma = module.get<PrismaService>(PrismaService);
jest.clearAllMocks();
});
describe('login', () => {
const mockAdmin = {
id: 1,
username: 'admin',
passwordHash: '$2a$10$hashed',
role: { name: 'superadmin' },
status: 'ACTIVE',
};
it('should login successfully', async () => {
mockPrisma.adminUser.findUnique.mockResolvedValue(mockAdmin);
jest.spyOn(bcrypt, 'compare').mockResolvedValue(true as never);
mockPrisma.adminUser.update.mockResolvedValue(mockAdmin);
const result = await service.login('admin', 'admin123456');
expect(result).toEqual(expect.objectContaining({ id: 1, username: 'admin', role: 'superadmin' }));
expect(mockPrisma.adminUser.update).toHaveBeenCalledWith(
expect.objectContaining({
where: { id: 1 },
data: { lastLoginAt: expect.any(Date) },
})
);
});
it('should throw on wrong password', async () => {
mockPrisma.adminUser.findUnique.mockResolvedValue(mockAdmin);
jest.spyOn(bcrypt, 'compare').mockResolvedValue(false as never);
await expect(service.login('admin', 'wrongpass')).rejects.toThrow(UnauthorizedException);
});
it('should throw when admin not found', async () => {
mockPrisma.adminUser.findUnique.mockResolvedValue(null);
await expect(service.login('unknown', 'pass')).rejects.toThrow(UnauthorizedException);
});
it('should throw when admin is inactive', async () => {
mockPrisma.adminUser.findUnique.mockResolvedValue({ ...mockAdmin, status: 'INACTIVE' });
await expect(service.login('admin', 'pass')).rejects.toThrow(UnauthorizedException);
});
});
describe('getDashboard', () => {
it('should return all stats', async () => {
mockPrisma.user.count.mockResolvedValue(100);
mockPrisma.course.count.mockResolvedValue(20);
mockPrisma.content.count.mockResolvedValue(45);
mockPrisma.prompt.count.mockResolvedValue(30);
mockPrisma.order.count.mockResolvedValue(15);
const result = await service.getDashboard();
expect(result).toEqual({
stats: {
userCount: 100,
courseCount: 20,
contentCount: 45,
promptCount: 30,
orderCount: 15,
},
});
});
it('should return zeroes when no data', async () => {
mockPrisma.user.count.mockResolvedValue(0);
mockPrisma.course.count.mockResolvedValue(0);
mockPrisma.content.count.mockResolvedValue(0);
mockPrisma.prompt.count.mockResolvedValue(0);
mockPrisma.order.count.mockResolvedValue(0);
const result = await service.getDashboard();
expect(result).toEqual({
stats: {
userCount: 0, courseCount: 0, contentCount: 0,
promptCount: 0, orderCount: 0,
},
});
});
it('should filter deleted users and courses', async () => {
mockPrisma.user.count.mockResolvedValue(50);
mockPrisma.course.count.mockResolvedValue(10);
mockPrisma.content.count.mockResolvedValue(5);
mockPrisma.prompt.count.mockResolvedValue(3);
mockPrisma.order.count.mockResolvedValue(2);
await service.getDashboard();
expect(mockPrisma.user.count).toHaveBeenCalledWith({ where: { deletedAt: null } });
expect(mockPrisma.course.count).toHaveBeenCalledWith({ where: { deletedAt: null } });
expect(mockPrisma.content.count).toHaveBeenCalledWith({
where: { deletedAt: null, status: 'PUBLISHED' },
});
});
});
describe('logAction', () => {
it('should create an admin log entry', async () => {
const mockLog = { id: 1, adminId: 1, action: 'login', target: null, detail: null, ip: null };
mockPrisma.adminLog.create.mockResolvedValue(mockLog);
const result = await service.logAction(1, 'login');
expect(result).toEqual(mockLog);
expect(mockPrisma.adminLog.create).toHaveBeenCalledWith({
data: { adminId: 1, action: 'login', target: undefined, detail: undefined, ip: undefined },
});
});
it('should create log with full details', async () => {
mockPrisma.adminLog.create.mockResolvedValue({ id: 2 });
await service.logAction(1, 'update', 'courses/1', '修改课程标题', '127.0.0.1');
expect(mockPrisma.adminLog.create).toHaveBeenCalledWith({
data: { adminId: 1, action: 'update', target: 'courses/1', detail: '修改课程标题', ip: '127.0.0.1' },
});
});
});
});