feat: 支持单元测试
This commit is contained in:
120
src/modules/auth/auth.controller.spec.ts
Normal file
120
src/modules/auth/auth.controller.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
293
src/modules/auth/auth.service.spec.ts
Normal file
293
src/modules/auth/auth.service.spec.ts
Normal 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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user