feat: 支持单元测试
This commit is contained in:
409
src/modules/share/share.service.spec.ts
Normal file
409
src/modules/share/share.service.spec.ts
Normal file
@@ -0,0 +1,409 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import {
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { ShareService } from './share.service';
|
||||
import { ShareConfigRepository } from './repositories/share-config.repository';
|
||||
import { ShareParticipantRepository } from './repositories/share-participant.repository';
|
||||
import { ShareLevelProgressRepository } from './repositories/share-level-progress.repository';
|
||||
import { LevelRepository } from '../wechat-game/repositories/level.repository';
|
||||
import { Level } from '../wechat-game/entities/level.entity';
|
||||
import { ShareConfig } from './entities/share-config.entity';
|
||||
import { ShareParticipant } from './entities/share-participant.entity';
|
||||
import { ShareLevelProgress } from './entities/share-level-progress.entity';
|
||||
|
||||
// Mock nanoid to return predictable values
|
||||
jest.mock('nanoid', () => ({
|
||||
nanoid: jest.fn(() => 'ABCD1234'),
|
||||
}));
|
||||
|
||||
describe('ShareService', () => {
|
||||
let service: ShareService;
|
||||
|
||||
const mockLevels: Level[] = Array.from({ length: 6 }, (_, i) => ({
|
||||
id: `level-${i + 1}`,
|
||||
imageUrl: `https://example.com/meme${i + 1}.jpg`,
|
||||
answer: `答案${i + 1}`,
|
||||
hint1: `提示${i + 1}`,
|
||||
hint2: null,
|
||||
hint3: null,
|
||||
sortOrder: i,
|
||||
timeLimit: i === 0 ? 60 : null,
|
||||
createdAt: new Date('2026-01-01'),
|
||||
updatedAt: new Date('2026-01-01'),
|
||||
}));
|
||||
|
||||
const mockShareConfig: ShareConfig = {
|
||||
id: 'share-uuid-1',
|
||||
shareCode: 'ABCD1234',
|
||||
title: '我的挑战',
|
||||
sharerId: 'user-uuid-1',
|
||||
levelIds: mockLevels.map((l) => l.id),
|
||||
sharer: {} as any,
|
||||
participants: [],
|
||||
createdAt: new Date('2026-01-01'),
|
||||
updatedAt: new Date('2026-01-01'),
|
||||
};
|
||||
|
||||
const mockParticipant: ShareParticipant = {
|
||||
id: 'participant-uuid-1',
|
||||
shareConfigId: 'share-uuid-1',
|
||||
participantId: 'user-uuid-2',
|
||||
shareConfig: {} as any,
|
||||
participant: {} as any,
|
||||
createdAt: new Date('2026-01-01'),
|
||||
};
|
||||
|
||||
const mockShareConfigRepository = {
|
||||
create: jest.fn(),
|
||||
findByShareCode: jest.fn(),
|
||||
};
|
||||
|
||||
const mockShareParticipantRepository = {
|
||||
addParticipant: jest.fn(),
|
||||
countByShareConfigId: jest.fn(),
|
||||
findByShareConfigAndParticipant: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
};
|
||||
|
||||
const mockShareLevelProgressRepository = {
|
||||
findByParticipantAndLevel: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
};
|
||||
|
||||
const mockLevelRepository = {
|
||||
findByIds: jest.fn(),
|
||||
findById: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ShareService,
|
||||
{ provide: ShareConfigRepository, useValue: mockShareConfigRepository },
|
||||
{
|
||||
provide: ShareParticipantRepository,
|
||||
useValue: mockShareParticipantRepository,
|
||||
},
|
||||
{
|
||||
provide: ShareLevelProgressRepository,
|
||||
useValue: mockShareLevelProgressRepository,
|
||||
},
|
||||
{ provide: LevelRepository, useValue: mockLevelRepository },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ShareService>(ShareService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('createShare', () => {
|
||||
const createDto = {
|
||||
title: '我的挑战',
|
||||
levelIds: ['level-1', 'level-2', 'level-3', 'level-4', 'level-5', 'level-6'],
|
||||
};
|
||||
|
||||
it('should create a share with valid 6 unique levels', async () => {
|
||||
mockLevelRepository.findByIds.mockResolvedValue(mockLevels);
|
||||
mockShareConfigRepository.findByShareCode.mockResolvedValue(null);
|
||||
mockShareConfigRepository.create.mockResolvedValue(mockShareConfig);
|
||||
|
||||
const result = await service.createShare('user-uuid-1', createDto);
|
||||
|
||||
expect(result.shareCode).toBe('ABCD1234');
|
||||
expect(result.title).toBe('我的挑战');
|
||||
expect(result.levelCount).toBe(6);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException when level IDs have duplicates', async () => {
|
||||
const duplicateDto = {
|
||||
title: '测试',
|
||||
levelIds: ['level-1', 'level-1', 'level-2', 'level-3', 'level-4', 'level-5'],
|
||||
};
|
||||
|
||||
await expect(
|
||||
service.createShare('user-uuid-1', duplicateDto),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when some levels do not exist', async () => {
|
||||
mockLevelRepository.findByIds.mockResolvedValue(mockLevels.slice(0, 4));
|
||||
|
||||
await expect(
|
||||
service.createShare('user-uuid-1', createDto),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException when share code generation fails', async () => {
|
||||
mockLevelRepository.findByIds.mockResolvedValue(mockLevels);
|
||||
// All 3 attempts find existing codes
|
||||
mockShareConfigRepository.findByShareCode.mockResolvedValue(
|
||||
mockShareConfig,
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.createShare('user-uuid-1', createDto),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('joinShare', () => {
|
||||
it('should return share details with ordered levels for a participant', async () => {
|
||||
mockShareConfigRepository.findByShareCode.mockResolvedValue(
|
||||
mockShareConfig,
|
||||
);
|
||||
mockShareParticipantRepository.addParticipant.mockResolvedValue(undefined);
|
||||
mockLevelRepository.findByIds.mockResolvedValue(mockLevels);
|
||||
|
||||
const result = await service.joinShare('user-uuid-2', 'ABCD1234');
|
||||
|
||||
expect(result.shareCode).toBe('ABCD1234');
|
||||
expect(result.title).toBe('我的挑战');
|
||||
expect(result.levels).toHaveLength(6);
|
||||
expect(result.levels[0].level).toBe(1);
|
||||
expect(result.levels[0].id).toBe('level-1');
|
||||
expect(mockShareParticipantRepository.addParticipant).toHaveBeenCalledWith(
|
||||
'share-uuid-1',
|
||||
'user-uuid-2',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not add participant when user is the sharer', async () => {
|
||||
mockShareConfigRepository.findByShareCode.mockResolvedValue(
|
||||
mockShareConfig,
|
||||
);
|
||||
mockLevelRepository.findByIds.mockResolvedValue(mockLevels);
|
||||
|
||||
const result = await service.joinShare('user-uuid-1', 'ABCD1234');
|
||||
|
||||
expect(result.shareCode).toBe('ABCD1234');
|
||||
expect(
|
||||
mockShareParticipantRepository.addParticipant,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when share code not found', async () => {
|
||||
mockShareConfigRepository.findByShareCode.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.joinShare('user-uuid-2', 'INVALID'),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reportLevelProgress', () => {
|
||||
const reportDto = {
|
||||
shareCode: 'ABCD1234',
|
||||
levelId: 'level-1',
|
||||
passed: true,
|
||||
timeSpent: 30,
|
||||
};
|
||||
|
||||
it('should create new progress record for first attempt', async () => {
|
||||
const levelWithTimeLimit = { ...mockLevels[0], timeLimit: 60 };
|
||||
mockShareConfigRepository.findByShareCode.mockResolvedValue(
|
||||
mockShareConfig,
|
||||
);
|
||||
mockLevelRepository.findById.mockResolvedValue(levelWithTimeLimit);
|
||||
mockShareParticipantRepository.findByShareConfigAndParticipant.mockResolvedValue(
|
||||
mockParticipant,
|
||||
);
|
||||
mockShareLevelProgressRepository.findByParticipantAndLevel.mockResolvedValue(
|
||||
null,
|
||||
);
|
||||
const newProgress: Partial<ShareLevelProgress> = {
|
||||
participantId: 'participant-uuid-1',
|
||||
levelId: 'level-1',
|
||||
passed: true,
|
||||
timeSpent: 30,
|
||||
};
|
||||
mockShareLevelProgressRepository.create.mockReturnValue(newProgress);
|
||||
mockShareLevelProgressRepository.save.mockResolvedValue(newProgress);
|
||||
|
||||
const result = await service.reportLevelProgress(
|
||||
'user-uuid-2',
|
||||
reportDto,
|
||||
);
|
||||
|
||||
expect(result.passed).toBe(true);
|
||||
expect(result.timeLimit).toBe(60);
|
||||
expect(result.withinTimeLimit).toBe(true);
|
||||
});
|
||||
|
||||
it('should return existing result when already passed (idempotent)', async () => {
|
||||
const levelWithTimeLimit = { ...mockLevels[0], timeLimit: 60 };
|
||||
mockShareConfigRepository.findByShareCode.mockResolvedValue(
|
||||
mockShareConfig,
|
||||
);
|
||||
mockLevelRepository.findById.mockResolvedValue(levelWithTimeLimit);
|
||||
mockShareParticipantRepository.findByShareConfigAndParticipant.mockResolvedValue(
|
||||
mockParticipant,
|
||||
);
|
||||
mockShareLevelProgressRepository.findByParticipantAndLevel.mockResolvedValue(
|
||||
{
|
||||
id: 'progress-uuid-1',
|
||||
participantId: 'participant-uuid-1',
|
||||
levelId: 'level-1',
|
||||
passed: true,
|
||||
timeSpent: 25,
|
||||
completedAt: new Date(),
|
||||
} as ShareLevelProgress,
|
||||
);
|
||||
|
||||
const result = await service.reportLevelProgress(
|
||||
'user-uuid-2',
|
||||
reportDto,
|
||||
);
|
||||
|
||||
expect(result.passed).toBe(true);
|
||||
expect(result.withinTimeLimit).toBe(true);
|
||||
expect(mockShareLevelProgressRepository.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should report withinTimeLimit=false when time exceeds limit', async () => {
|
||||
const levelWithTimeLimit = { ...mockLevels[0], timeLimit: 20 };
|
||||
mockShareConfigRepository.findByShareCode.mockResolvedValue(
|
||||
mockShareConfig,
|
||||
);
|
||||
mockLevelRepository.findById.mockResolvedValue(levelWithTimeLimit);
|
||||
mockShareParticipantRepository.findByShareConfigAndParticipant.mockResolvedValue(
|
||||
mockParticipant,
|
||||
);
|
||||
mockShareLevelProgressRepository.findByParticipantAndLevel.mockResolvedValue(
|
||||
null,
|
||||
);
|
||||
const newProgress: Partial<ShareLevelProgress> = {
|
||||
participantId: 'participant-uuid-1',
|
||||
levelId: 'level-1',
|
||||
passed: true,
|
||||
timeSpent: 30,
|
||||
};
|
||||
mockShareLevelProgressRepository.create.mockReturnValue(newProgress);
|
||||
mockShareLevelProgressRepository.save.mockResolvedValue(newProgress);
|
||||
|
||||
const result = await service.reportLevelProgress('user-uuid-2', {
|
||||
...reportDto,
|
||||
timeSpent: 30,
|
||||
});
|
||||
|
||||
expect(result.passed).toBe(true);
|
||||
expect(result.withinTimeLimit).toBe(false);
|
||||
});
|
||||
|
||||
it('should report withinTimeLimit=true when level has no time limit', async () => {
|
||||
const levelNoTimeLimit = { ...mockLevels[1], timeLimit: null };
|
||||
mockShareConfigRepository.findByShareCode.mockResolvedValue(
|
||||
mockShareConfig,
|
||||
);
|
||||
mockLevelRepository.findById.mockResolvedValue(levelNoTimeLimit);
|
||||
mockShareParticipantRepository.findByShareConfigAndParticipant.mockResolvedValue(
|
||||
mockParticipant,
|
||||
);
|
||||
mockShareLevelProgressRepository.findByParticipantAndLevel.mockResolvedValue(
|
||||
null,
|
||||
);
|
||||
const newProgress: Partial<ShareLevelProgress> = {
|
||||
participantId: 'participant-uuid-1',
|
||||
levelId: 'level-2',
|
||||
passed: true,
|
||||
timeSpent: 999,
|
||||
};
|
||||
mockShareLevelProgressRepository.create.mockReturnValue(newProgress);
|
||||
mockShareLevelProgressRepository.save.mockResolvedValue(newProgress);
|
||||
|
||||
const result = await service.reportLevelProgress('user-uuid-2', {
|
||||
shareCode: 'ABCD1234',
|
||||
levelId: 'level-2',
|
||||
passed: true,
|
||||
timeSpent: 999,
|
||||
});
|
||||
|
||||
expect(result.withinTimeLimit).toBe(true);
|
||||
});
|
||||
|
||||
it('should create participant if not exists when reporting progress', async () => {
|
||||
mockShareConfigRepository.findByShareCode.mockResolvedValue(
|
||||
mockShareConfig,
|
||||
);
|
||||
mockLevelRepository.findById.mockResolvedValue(mockLevels[0]);
|
||||
mockShareParticipantRepository.findByShareConfigAndParticipant.mockResolvedValue(
|
||||
null,
|
||||
);
|
||||
const createdParticipant = { ...mockParticipant };
|
||||
mockShareParticipantRepository.create.mockReturnValue(createdParticipant);
|
||||
mockShareParticipantRepository.save.mockResolvedValue(createdParticipant);
|
||||
mockShareLevelProgressRepository.findByParticipantAndLevel.mockResolvedValue(
|
||||
null,
|
||||
);
|
||||
mockShareLevelProgressRepository.create.mockReturnValue({} as any);
|
||||
mockShareLevelProgressRepository.save.mockResolvedValue({} as any);
|
||||
|
||||
await service.reportLevelProgress('user-uuid-2', reportDto);
|
||||
|
||||
expect(mockShareParticipantRepository.create).toHaveBeenCalledWith({
|
||||
shareConfigId: 'share-uuid-1',
|
||||
participantId: 'user-uuid-2',
|
||||
});
|
||||
expect(mockShareParticipantRepository.save).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when share not found', async () => {
|
||||
mockShareConfigRepository.findByShareCode.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.reportLevelProgress('user-uuid-2', reportDto),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when level not found', async () => {
|
||||
mockShareConfigRepository.findByShareCode.mockResolvedValue(
|
||||
mockShareConfig,
|
||||
);
|
||||
mockLevelRepository.findById.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.reportLevelProgress('user-uuid-2', reportDto),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('should update existing progress when not yet passed', async () => {
|
||||
mockShareConfigRepository.findByShareCode.mockResolvedValue(
|
||||
mockShareConfig,
|
||||
);
|
||||
mockLevelRepository.findById.mockResolvedValue(mockLevels[0]);
|
||||
mockShareParticipantRepository.findByShareConfigAndParticipant.mockResolvedValue(
|
||||
mockParticipant,
|
||||
);
|
||||
const existingProgress = {
|
||||
id: 'progress-uuid-1',
|
||||
participantId: 'participant-uuid-1',
|
||||
levelId: 'level-1',
|
||||
passed: false,
|
||||
timeSpent: 15,
|
||||
completedAt: new Date('2026-01-01'),
|
||||
} as ShareLevelProgress;
|
||||
mockShareLevelProgressRepository.findByParticipantAndLevel.mockResolvedValue(
|
||||
existingProgress,
|
||||
);
|
||||
mockShareLevelProgressRepository.save.mockResolvedValue({
|
||||
...existingProgress,
|
||||
passed: true,
|
||||
timeSpent: 30,
|
||||
});
|
||||
|
||||
const result = await service.reportLevelProgress(
|
||||
'user-uuid-2',
|
||||
reportDto,
|
||||
);
|
||||
|
||||
expect(result.passed).toBe(true);
|
||||
expect(mockShareLevelProgressRepository.save).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user