286 lines
9.5 KiB
TypeScript
286 lines
9.5 KiB
TypeScript
import { UpsertExternalGameRatingUseCase } from './UpsertExternalGameRatingUseCase';
|
|
import { ExternalGameRatingRepository } from '../../domain/repositories/ExternalGameRatingRepository';
|
|
import { ExternalGameRatingProfile } from '../../domain/entities/ExternalGameRatingProfile';
|
|
import { UserId } from '../../domain/value-objects/UserId';
|
|
import { GameKey } from '../../domain/value-objects/GameKey';
|
|
import { ExternalRating } from '../../domain/value-objects/ExternalRating';
|
|
import { ExternalRatingProvenance } from '../../domain/value-objects/ExternalRatingProvenance';
|
|
import { UpsertExternalGameRatingInput } from '../dtos/UpsertExternalGameRatingDto';
|
|
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
|
|
|
describe('UpsertExternalGameRatingUseCase', () => {
|
|
let useCase: UpsertExternalGameRatingUseCase;
|
|
let mockRepository: ExternalGameRatingRepository;
|
|
|
|
beforeEach(() => {
|
|
mockRepository = {
|
|
findByUserIdAndGameKey: vi.fn(),
|
|
findByUserId: vi.fn(),
|
|
findByGameKey: vi.fn(),
|
|
save: vi.fn(),
|
|
saveMany: vi.fn(),
|
|
delete: vi.fn(),
|
|
exists: vi.fn(),
|
|
} as any;
|
|
|
|
useCase = new UpsertExternalGameRatingUseCase(mockRepository);
|
|
});
|
|
|
|
describe('execute', () => {
|
|
it('should create a new profile when it does not exist', async () => {
|
|
const input: UpsertExternalGameRatingInput = {
|
|
userId: 'user-123',
|
|
gameKey: 'iracing',
|
|
ratings: [
|
|
{ type: 'safety', value: 85.5 },
|
|
{ type: 'skill', value: 92.0 },
|
|
],
|
|
provenance: {
|
|
source: 'iracing',
|
|
lastSyncedAt: '2024-01-01T00:00:00Z',
|
|
verified: true,
|
|
},
|
|
};
|
|
|
|
(mockRepository.findByUserIdAndGameKey as any).mockResolvedValue(null);
|
|
(mockRepository.save as any).mockImplementation(async (profile: any) => profile);
|
|
|
|
const result = await useCase.execute(input);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.action).toBe('created');
|
|
expect(result.profile.userId).toBe('user-123');
|
|
expect(result.profile.gameKey).toBe('iracing');
|
|
expect(result.profile.ratingCount).toBe(2);
|
|
expect(result.profile.ratingTypes).toEqual(['safety', 'skill']);
|
|
expect(result.profile.verified).toBe(true);
|
|
|
|
expect(mockRepository.findByUserIdAndGameKey).toHaveBeenCalledWith('user-123', 'iracing');
|
|
expect(mockRepository.save).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should update existing profile', async () => {
|
|
const existingProfile = createTestProfile('user-123', 'iracing');
|
|
(mockRepository.findByUserIdAndGameKey as any).mockResolvedValue(existingProfile);
|
|
(mockRepository.save as any).mockImplementation(async (profile: any) => profile);
|
|
|
|
const input: UpsertExternalGameRatingInput = {
|
|
userId: 'user-123',
|
|
gameKey: 'iracing',
|
|
ratings: [
|
|
{ type: 'safety', value: 90.0 },
|
|
{ type: 'newType', value: 88.0 },
|
|
],
|
|
provenance: {
|
|
source: 'iracing',
|
|
lastSyncedAt: '2024-01-02T00:00:00Z',
|
|
verified: false,
|
|
},
|
|
};
|
|
|
|
const result = await useCase.execute(input);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.action).toBe('updated');
|
|
expect(result.profile.ratingCount).toBe(2);
|
|
expect(result.profile.ratingTypes).toEqual(['safety', 'newType']);
|
|
expect(result.profile.verified).toBe(false);
|
|
|
|
expect(mockRepository.findByUserIdAndGameKey).toHaveBeenCalledWith('user-123', 'iracing');
|
|
expect(mockRepository.save).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should handle validation errors - missing userId', async () => {
|
|
const input: UpsertExternalGameRatingInput = {
|
|
userId: '',
|
|
gameKey: 'iracing',
|
|
ratings: [{ type: 'safety', value: 85.5 }],
|
|
provenance: {
|
|
source: 'iracing',
|
|
lastSyncedAt: '2024-01-01T00:00:00Z',
|
|
},
|
|
};
|
|
|
|
const result = await useCase.execute(input);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.errors).toContain('User ID is required');
|
|
expect(mockRepository.save).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle validation errors - missing gameKey', async () => {
|
|
const input: UpsertExternalGameRatingInput = {
|
|
userId: 'user-123',
|
|
gameKey: '',
|
|
ratings: [{ type: 'safety', value: 85.5 }],
|
|
provenance: {
|
|
source: 'iracing',
|
|
lastSyncedAt: '2024-01-01T00:00:00Z',
|
|
},
|
|
};
|
|
|
|
const result = await useCase.execute(input);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.errors).toContain('Game key is required');
|
|
expect(mockRepository.save).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle validation errors - empty ratings', async () => {
|
|
const input: UpsertExternalGameRatingInput = {
|
|
userId: 'user-123',
|
|
gameKey: 'iracing',
|
|
ratings: [],
|
|
provenance: {
|
|
source: 'iracing',
|
|
lastSyncedAt: '2024-01-01T00:00:00Z',
|
|
},
|
|
};
|
|
|
|
const result = await useCase.execute(input);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.errors).toContain('Ratings are required');
|
|
expect(mockRepository.save).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle validation errors - invalid rating value', async () => {
|
|
const input: UpsertExternalGameRatingInput = {
|
|
userId: 'user-123',
|
|
gameKey: 'iracing',
|
|
ratings: [
|
|
{ type: 'safety', value: NaN },
|
|
],
|
|
provenance: {
|
|
source: 'iracing',
|
|
lastSyncedAt: '2024-01-01T00:00:00Z',
|
|
},
|
|
};
|
|
|
|
const result = await useCase.execute(input);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.errors).toContain('Rating value for type safety must be a valid number');
|
|
expect(mockRepository.save).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle validation errors - missing provenance source', async () => {
|
|
const input: UpsertExternalGameRatingInput = {
|
|
userId: 'user-123',
|
|
gameKey: 'iracing',
|
|
ratings: [{ type: 'safety', value: 85.5 }],
|
|
provenance: {
|
|
source: '',
|
|
lastSyncedAt: '2024-01-01T00:00:00Z',
|
|
},
|
|
};
|
|
|
|
const result = await useCase.execute(input);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.errors).toContain('Provenance source is required');
|
|
expect(mockRepository.save).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle validation errors - invalid date', async () => {
|
|
const input: UpsertExternalGameRatingInput = {
|
|
userId: 'user-123',
|
|
gameKey: 'iracing',
|
|
ratings: [{ type: 'safety', value: 85.5 }],
|
|
provenance: {
|
|
source: 'iracing',
|
|
lastSyncedAt: 'invalid-date',
|
|
},
|
|
};
|
|
|
|
const result = await useCase.execute(input);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.errors).toContain('Provenance lastSyncedAt must be a valid date');
|
|
expect(mockRepository.save).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle repository errors gracefully', async () => {
|
|
const input: UpsertExternalGameRatingInput = {
|
|
userId: 'user-123',
|
|
gameKey: 'iracing',
|
|
ratings: [{ type: 'safety', value: 85.5 }],
|
|
provenance: {
|
|
source: 'iracing',
|
|
lastSyncedAt: '2024-01-01T00:00:00Z',
|
|
},
|
|
};
|
|
|
|
(mockRepository.findByUserIdAndGameKey as any).mockRejectedValue(new Error('Database connection failed'));
|
|
|
|
const result = await useCase.execute(input);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.errors).toContain('Database connection failed');
|
|
expect(mockRepository.save).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should default verified to false when not provided', async () => {
|
|
const input: UpsertExternalGameRatingInput = {
|
|
userId: 'user-123',
|
|
gameKey: 'iracing',
|
|
ratings: [{ type: 'safety', value: 85.5 }],
|
|
provenance: {
|
|
source: 'iracing',
|
|
lastSyncedAt: '2024-01-01T00:00:00Z',
|
|
// verified not provided
|
|
},
|
|
};
|
|
|
|
(mockRepository.findByUserIdAndGameKey as any).mockResolvedValue(null);
|
|
(mockRepository.save as any).mockImplementation(async (profile: any) => profile);
|
|
|
|
const result = await useCase.execute(input);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.profile.verified).toBe(false);
|
|
});
|
|
|
|
it('should trim rating types', async () => {
|
|
const input: UpsertExternalGameRatingInput = {
|
|
userId: 'user-123',
|
|
gameKey: 'iracing',
|
|
ratings: [
|
|
{ type: ' safety ', value: 85.5 },
|
|
],
|
|
provenance: {
|
|
source: 'iracing',
|
|
lastSyncedAt: '2024-01-01T00:00:00Z',
|
|
},
|
|
};
|
|
|
|
(mockRepository.findByUserIdAndGameKey as any).mockResolvedValue(null);
|
|
(mockRepository.save as any).mockImplementation(async (profile: any) => profile);
|
|
|
|
const result = await useCase.execute(input);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.profile.ratingTypes).toEqual(['safety']);
|
|
});
|
|
});
|
|
|
|
function createTestProfile(userId: string, gameKey: string): ExternalGameRatingProfile {
|
|
const user = UserId.fromString(userId);
|
|
const game = GameKey.create(gameKey);
|
|
const ratings = new Map([
|
|
['safety', ExternalRating.create(game, 'safety', 85.5)],
|
|
]);
|
|
const provenance = ExternalRatingProvenance.create({
|
|
source: gameKey,
|
|
lastSyncedAt: new Date('2024-01-01'),
|
|
verified: false,
|
|
});
|
|
|
|
return ExternalGameRatingProfile.create({
|
|
userId: user,
|
|
gameKey: game,
|
|
ratings,
|
|
provenance,
|
|
});
|
|
}
|
|
}); |