feat: Phase 1-3 全部完成 — 沙盒增强、学情分析、学习路径
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
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: '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' },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user