feat: 支持单元测试

This commit is contained in:
richarjiang
2026-04-08 16:02:19 +08:00
parent 2d0fee8a9a
commit df05b7280c
18 changed files with 2462 additions and 17 deletions

View File

@@ -0,0 +1,120 @@
import { Test, TestingModule } from '@nestjs/testing';
import { JwtService } from '@nestjs/jwt';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { ApiResponseDto } from '../../common/dto/api-response.dto';
import type { JwtPayload } from '../../common/guards/jwt-auth.guard';
describe('AuthController', () => {
let controller: AuthController;
const mockUser: JwtPayload = {
sub: 'user-uuid-1',
openid: 'wx-openid-123',
};
const mockAuthService = {
wxLogin: jest.fn(),
getUserAssets: jest.fn(),
consumePoint: jest.fn(),
earnPoint: jest.fn(),
getGameData: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
providers: [
{ provide: AuthService, useValue: mockAuthService },
{ provide: JwtService, useValue: { verifyAsync: jest.fn() } },
],
}).compile();
controller = module.get<AuthController>(AuthController);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('wxLogin', () => {
it('should return success response with token and user info', async () => {
const loginResponse = {
token: 'jwt-token',
user: { id: 'user-uuid-1', nickname: 'Test', points: 10 },
};
mockAuthService.wxLogin.mockResolvedValue(loginResponse);
const result = await controller.wxLogin({ code: 'wx-code-123' });
expect(result).toBeInstanceOf(ApiResponseDto);
expect(result.success).toBe(true);
expect(result.data).toEqual(loginResponse);
expect(mockAuthService.wxLogin).toHaveBeenCalledWith('wx-code-123');
});
});
describe('getUserAssets', () => {
it('should return success response with user points', async () => {
mockAuthService.getUserAssets.mockResolvedValue({ points: 10 });
const result = await controller.getUserAssets(mockUser);
expect(result.success).toBe(true);
expect(result.data).toEqual({ points: 10 });
expect(mockAuthService.getUserAssets).toHaveBeenCalledWith('user-uuid-1');
});
});
describe('consumePoint', () => {
it('should return success response with updated points', async () => {
mockAuthService.consumePoint.mockResolvedValue({ points: 9 });
const dto = { reason: 'hint_unlock' as const, levelId: 'level-1', hintIndex: 2 };
const result = await controller.consumePoint(mockUser, dto);
expect(result.success).toBe(true);
expect(result.data).toEqual({ points: 9 });
expect(mockAuthService.consumePoint).toHaveBeenCalledWith(
'user-uuid-1',
dto,
);
});
});
describe('earnPoint', () => {
it('should return success response with updated points', async () => {
mockAuthService.earnPoint.mockResolvedValue({ points: 11 });
const dto = {
reason: 'level_complete' as const,
levelId: 'level-1',
timeSpent: 30,
};
const result = await controller.earnPoint(mockUser, dto);
expect(result.success).toBe(true);
expect(result.data).toEqual({ points: 11 });
expect(mockAuthService.earnPoint).toHaveBeenCalledWith(
'user-uuid-1',
dto,
);
});
});
describe('getGameData', () => {
it('should return success response with game data', async () => {
const gameData = {
user: { id: 'user-uuid-1', points: 10 },
completedLevelIds: ['level-1', 'level-2'],
};
mockAuthService.getGameData.mockResolvedValue(gameData);
const result = await controller.getGameData(mockUser);
expect(result.success).toBe(true);
expect(result.data).toEqual(gameData);
expect(mockAuthService.getGameData).toHaveBeenCalledWith('user-uuid-1');
});
});
});

View File

@@ -0,0 +1,293 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import {
UnauthorizedException,
BadRequestException,
} from '@nestjs/common';
import axios from 'axios';
import { AuthService } from './auth.service';
import { UserRepository } from './repositories/user.repository';
import { UserLevelProgressRepository } from './repositories/user-level-progress.repository';
import { User } from './entities/user.entity';
import { UserLevelProgress } from './entities/user-level-progress.entity';
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;
describe('AuthService', () => {
let service: AuthService;
const mockUser: User = {
id: 'user-uuid-1',
openid: 'wx-openid-123',
sessionKey: 'session-key-abc',
nickname: 'TestUser',
avatarUrl: null,
points: 10,
createdAt: new Date('2026-01-01'),
updatedAt: new Date('2026-01-01'),
};
const mockLevelProgress: UserLevelProgress = {
id: 'progress-uuid-1',
userId: 'user-uuid-1',
levelId: 'level-1',
user: mockUser,
timeSpent: 30,
completedAt: new Date('2026-01-02'),
};
const mockUserRepository = {
findById: jest.fn(),
findByOpenid: jest.fn(),
create: jest.fn(),
save: jest.fn(),
};
const mockUserLevelProgressRepository = {
findByUserId: jest.fn(),
findByUserAndLevel: jest.fn(),
create: jest.fn(),
save: jest.fn(),
};
const mockJwtService = {
signAsync: jest.fn(),
};
const mockConfigService = {
get: jest.fn((key: string, defaultValue?: string) => {
const config: Record<string, string> = {
WX_APPID: 'test-appid',
WX_SECRET: 'test-secret',
};
return config[key] ?? defaultValue ?? '';
}),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AuthService,
{ provide: ConfigService, useValue: mockConfigService },
{ provide: JwtService, useValue: mockJwtService },
{ provide: UserRepository, useValue: mockUserRepository },
{
provide: UserLevelProgressRepository,
useValue: mockUserLevelProgressRepository,
},
],
}).compile();
service = module.get<AuthService>(AuthService);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('wxLogin', () => {
it('should create a new user and return JWT token on first login', async () => {
const newUser = { ...mockUser, points: 10 };
mockedAxios.get.mockResolvedValue({
data: { openid: 'wx-openid-123', session_key: 'session-key-abc' },
});
mockUserRepository.findByOpenid.mockResolvedValue(null);
mockUserRepository.create.mockReturnValue(newUser);
mockUserRepository.save.mockResolvedValue(newUser);
mockJwtService.signAsync.mockResolvedValue('jwt-token-xyz');
const result = await service.wxLogin('wx-code-123');
expect(result.token).toBe('jwt-token-xyz');
expect(result.user.id).toBe('user-uuid-1');
expect(result.user.points).toBe(10);
expect(mockUserRepository.create).toHaveBeenCalledWith({
openid: 'wx-openid-123',
sessionKey: 'session-key-abc',
points: 10,
});
expect(mockJwtService.signAsync).toHaveBeenCalledWith({
sub: 'user-uuid-1',
openid: 'wx-openid-123',
});
});
it('should return existing user and update session_key on repeat login', async () => {
const existingUser = { ...mockUser };
const updatedUser = { ...existingUser, sessionKey: 'new-session-key' };
mockedAxios.get.mockResolvedValue({
data: { openid: 'wx-openid-123', session_key: 'new-session-key' },
});
mockUserRepository.findByOpenid.mockResolvedValue(existingUser);
mockUserRepository.save.mockResolvedValue(updatedUser);
mockJwtService.signAsync.mockResolvedValue('jwt-token-abc');
const result = await service.wxLogin('wx-code-456');
expect(result.token).toBe('jwt-token-abc');
expect(result.user.id).toBe('user-uuid-1');
expect(mockUserRepository.create).not.toHaveBeenCalled();
expect(mockUserRepository.save).toHaveBeenCalled();
});
it('should throw UnauthorizedException when WeChat API returns error', async () => {
mockedAxios.get.mockResolvedValue({
data: { errcode: 40029, errmsg: 'invalid code' },
});
await expect(service.wxLogin('invalid-code')).rejects.toThrow(
UnauthorizedException,
);
});
it('should throw UnauthorizedException when WeChat API call fails', async () => {
mockedAxios.get.mockRejectedValue(new Error('Network error'));
await expect(service.wxLogin('any-code')).rejects.toThrow(
UnauthorizedException,
);
});
});
describe('getUserAssets', () => {
it('should return user points', async () => {
mockUserRepository.findById.mockResolvedValue(mockUser);
const result = await service.getUserAssets('user-uuid-1');
expect(result.points).toBe(10);
expect(mockUserRepository.findById).toHaveBeenCalledWith('user-uuid-1');
});
it('should throw UnauthorizedException when user not found', async () => {
mockUserRepository.findById.mockResolvedValue(null);
await expect(service.getUserAssets('nonexistent')).rejects.toThrow(
UnauthorizedException,
);
});
});
describe('consumePoint', () => {
it('should deduct 1 point and return updated points', async () => {
const user = { ...mockUser, points: 5 };
const savedUser = { ...user, points: 4 };
mockUserRepository.findById.mockResolvedValue(user);
mockUserRepository.save.mockResolvedValue(savedUser);
const result = await service.consumePoint('user-uuid-1', {
reason: 'hint_unlock',
levelId: 'level-1',
hintIndex: 2,
});
expect(result.points).toBe(4);
expect(mockUserRepository.save).toHaveBeenCalled();
});
it('should throw BadRequestException when points are 0', async () => {
mockUserRepository.findById.mockResolvedValue({
...mockUser,
points: 0,
});
await expect(
service.consumePoint('user-uuid-1', {
reason: 'hint_unlock',
}),
).rejects.toThrow(BadRequestException);
});
it('should throw UnauthorizedException when user not found', async () => {
mockUserRepository.findById.mockResolvedValue(null);
await expect(
service.consumePoint('nonexistent', { reason: 'hint_unlock' }),
).rejects.toThrow(UnauthorizedException);
});
});
describe('earnPoint', () => {
const earnDto = {
reason: 'level_complete' as const,
levelId: 'level-1',
timeSpent: 30,
};
it('should award 1 point for first-time level completion', async () => {
const user = { ...mockUser, points: 10 };
mockUserRepository.findById.mockResolvedValue(user);
mockUserLevelProgressRepository.findByUserAndLevel.mockResolvedValue(null);
mockUserLevelProgressRepository.create.mockReturnValue(mockLevelProgress);
mockUserLevelProgressRepository.save.mockResolvedValue(mockLevelProgress);
mockUserRepository.save.mockResolvedValue({ ...user, points: 11 });
const result = await service.earnPoint('user-uuid-1', earnDto);
expect(result.points).toBe(11);
expect(mockUserLevelProgressRepository.create).toHaveBeenCalledWith({
userId: 'user-uuid-1',
levelId: 'level-1',
timeSpent: 30,
});
});
it('should not award duplicate points for already completed level', async () => {
const user = { ...mockUser, points: 10 };
mockUserRepository.findById.mockResolvedValue(user);
mockUserLevelProgressRepository.findByUserAndLevel.mockResolvedValue(
mockLevelProgress,
);
const result = await service.earnPoint('user-uuid-1', earnDto);
expect(result.points).toBe(10);
expect(mockUserLevelProgressRepository.create).not.toHaveBeenCalled();
expect(mockUserRepository.save).not.toHaveBeenCalled();
});
it('should throw UnauthorizedException when user not found', async () => {
mockUserRepository.findById.mockResolvedValue(null);
await expect(
service.earnPoint('nonexistent', earnDto),
).rejects.toThrow(UnauthorizedException);
});
});
describe('getGameData', () => {
it('should return user info and completed level IDs', async () => {
mockUserRepository.findById.mockResolvedValue(mockUser);
mockUserLevelProgressRepository.findByUserId.mockResolvedValue([
{ ...mockLevelProgress, levelId: 'level-1' },
{ ...mockLevelProgress, levelId: 'level-2' },
]);
const result = await service.getGameData('user-uuid-1');
expect(result.user.id).toBe('user-uuid-1');
expect(result.user.points).toBe(10);
expect(result.completedLevelIds).toEqual(['level-1', 'level-2']);
});
it('should return empty completedLevelIds when no progress', async () => {
mockUserRepository.findById.mockResolvedValue(mockUser);
mockUserLevelProgressRepository.findByUserId.mockResolvedValue([]);
const result = await service.getGameData('user-uuid-1');
expect(result.completedLevelIds).toEqual([]);
});
it('should throw UnauthorizedException when user not found', async () => {
mockUserRepository.findById.mockResolvedValue(null);
mockUserLevelProgressRepository.findByUserId.mockResolvedValue([]);
await expect(service.getGameData('nonexistent')).rejects.toThrow(
UnauthorizedException,
);
});
});
});