fix adapters

This commit is contained in:
2025-12-23 20:43:57 +01:00
parent b5431355ca
commit 16cd572c63
28 changed files with 1357 additions and 15 deletions

View File

@@ -0,0 +1,105 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Logger } from '@core/shared/application';
import { AnalyticsSnapshot } from '@core/analytics';
import { InMemoryAnalyticsSnapshotRepository } from './InMemoryAnalyticsSnapshotRepository';
describe('InMemoryAnalyticsSnapshotRepository', () => {
let repository: InMemoryAnalyticsSnapshotRepository;
let mockLogger: Logger;
beforeEach(() => {
mockLogger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
} as unknown as Logger;
repository = new InMemoryAnalyticsSnapshotRepository(mockLogger);
repository.clear();
});
it('initializes with logger', () => {
expect(repository).toBeDefined();
expect(mockLogger.info).toHaveBeenCalledWith('InMemoryAnalyticsSnapshotRepository initialized.');
});
it('saves and finds by id', async () => {
const snapshot = AnalyticsSnapshot.createEmpty(
'snap-1',
'league',
'league-1',
'monthly',
new Date('2025-01-01T00:00:00.000Z'),
new Date('2025-02-01T00:00:00.000Z'),
);
await repository.save(snapshot);
const found = await repository.findById('snap-1');
expect(found).toBe(snapshot);
});
it('finds by entity and latest', async () => {
const a = AnalyticsSnapshot.createEmpty(
'snap-a',
'league',
'league-1',
'monthly',
new Date('2025-01-01T00:00:00.000Z'),
new Date('2025-02-01T00:00:00.000Z'),
);
const b = AnalyticsSnapshot.createEmpty(
'snap-b',
'league',
'league-1',
'monthly',
new Date('2025-02-01T00:00:00.000Z'),
new Date('2025-03-01T00:00:00.000Z'),
);
const other = AnalyticsSnapshot.createEmpty(
'snap-other',
'team',
'team-1',
'monthly',
new Date('2025-01-01T00:00:00.000Z'),
new Date('2025-02-01T00:00:00.000Z'),
);
await repository.save(a);
await repository.save(b);
await repository.save(other);
const byEntity = await repository.findByEntity('league', 'league-1');
expect(byEntity.map(s => s.id).sort()).toEqual(['snap-a', 'snap-b']);
const latest = await repository.findLatest('league', 'league-1', 'monthly');
expect(latest?.id).toBe('snap-b');
const history = await repository.getHistoricalSnapshots('league', 'league-1', 'monthly', 1);
expect(history.map(s => s.id)).toEqual(['snap-b']);
});
it('finds by period range', async () => {
const snapshot = AnalyticsSnapshot.createEmpty(
'snap-period',
'league',
'league-1',
'monthly',
new Date('2025-01-01T00:00:00.000Z'),
new Date('2025-02-01T00:00:00.000Z'),
);
await repository.save(snapshot);
const found = await repository.findByPeriod(
'league',
'league-1',
'monthly',
new Date('2024-12-15T00:00:00.000Z'),
new Date('2025-03-15T00:00:00.000Z'),
);
expect(found?.id).toBe('snap-period');
});
});

View File

@@ -0,0 +1,83 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Logger } from '@core/shared/application';
import { EngagementEvent } from '@core/analytics';
import { InMemoryEngagementRepository } from './InMemoryEngagementRepository';
describe('InMemoryEngagementRepository', () => {
let repository: InMemoryEngagementRepository;
let mockLogger: Logger;
beforeEach(() => {
mockLogger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
} as unknown as Logger;
repository = new InMemoryEngagementRepository(mockLogger);
repository.clear();
});
it('initializes with a logger', () => {
expect(repository).toBeDefined();
expect(mockLogger.info).toHaveBeenCalledWith('InMemoryEngagementRepository initialized.');
});
it('saves and queries engagement events', async () => {
const now = new Date('2025-01-01T12:00:00.000Z');
const event1 = EngagementEvent.create({
id: 'e1',
action: 'click_sponsor_logo',
entityType: 'league',
entityId: 'league-1',
actorType: 'driver',
actorId: 'driver-1',
sessionId: 's1',
timestamp: now,
});
const event2 = EngagementEvent.create({
id: 'e2',
action: 'view_schedule',
entityType: 'league',
entityId: 'league-1',
actorType: 'anonymous',
sessionId: 's2',
timestamp: new Date(now.getTime() + 1000),
});
await repository.save(event1);
await repository.save(event2);
expect((await repository.findById('e1'))?.id).toBe('e1');
expect((await repository.findByEntityId('league', 'league-1')).length).toBe(2);
expect((await repository.findByAction('view_schedule')).map(e => e.id)).toEqual(['e2']);
const inRange = await repository.findByDateRange(
new Date('2025-01-01T00:00:00.000Z'),
new Date('2025-01-01T23:59:59.999Z'),
);
expect(inRange.length).toBe(2);
const clicks = await repository.getSponsorClicksForEntity('league-1');
expect(clicks).toBe(1);
const counted = await repository.countByAction('view_schedule', 'league-1');
expect(counted).toBe(1);
});
it('seeds events', async () => {
const e = EngagementEvent.create({
id: 'seed-1',
action: 'view_schedule',
entityType: 'league',
entityId: 'league-2',
actorType: 'anonymous',
sessionId: 's3',
});
repository.seed([e]);
expect((await repository.findById('seed-1'))?.id).toBe('seed-1');
});
});

View File

@@ -0,0 +1,91 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Logger } from '@core/shared/application';
import { PageView } from '@core/analytics';
import { InMemoryPageViewRepository } from './InMemoryPageViewRepository';
describe('InMemoryPageViewRepository', () => {
let repository: InMemoryPageViewRepository;
let mockLogger: Logger;
beforeEach(() => {
mockLogger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
} as unknown as Logger;
repository = new InMemoryPageViewRepository(mockLogger);
repository.clear();
});
it('initializes with a logger', () => {
expect(repository).toBeDefined();
expect(mockLogger.info).toHaveBeenCalledWith('InMemoryPageViewRepository initialized.');
});
it('saves and queries page views', async () => {
const pv1 = PageView.create({
id: 'pv-1',
entityType: 'league',
entityId: 'league-1',
visitorType: 'anonymous',
sessionId: 'sess-1',
visitorId: 'visitor-1',
timestamp: new Date('2025-01-01T10:00:00.000Z'),
});
const pv2 = PageView.create({
id: 'pv-2',
entityType: 'league',
entityId: 'league-1',
visitorType: 'anonymous',
sessionId: 'sess-2',
visitorId: 'visitor-1',
timestamp: new Date('2025-01-02T10:00:00.000Z'),
});
const pv3 = PageView.create({
id: 'pv-3',
entityType: 'league',
entityId: 'league-1',
visitorType: 'anonymous',
sessionId: 'sess-3',
// no visitorId; should fall back to sessionId for uniqueness
timestamp: new Date('2025-01-02T11:00:00.000Z'),
});
await repository.save(pv1);
await repository.save(pv2);
await repository.save(pv3);
expect((await repository.findById('pv-1'))?.id).toBe('pv-1');
expect((await repository.findByEntityId('league', 'league-1')).length).toBe(3);
const range = await repository.findByDateRange(
new Date('2025-01-02T00:00:00.000Z'),
new Date('2025-01-02T23:59:59.999Z'),
);
expect(range.map(p => p.id).sort()).toEqual(['pv-2', 'pv-3']);
expect((await repository.findBySession('sess-2')).map(p => p.id)).toEqual(['pv-2']);
const count = await repository.countByEntityId('league', 'league-1');
expect(count).toBe(3);
const unique = await repository.countUniqueVisitors('league', 'league-1');
// visitor-1 + sess-3
expect(unique).toBe(2);
});
it('seeds page views', async () => {
const pv = PageView.create({
id: 'seed-pv',
entityType: 'team',
entityId: 'team-1',
visitorType: 'anonymous',
sessionId: 'sess-seed',
});
repository.seed([pv]);
expect((await repository.findById('seed-pv'))?.id).toBe('seed-pv');
});
});

View File

@@ -1,10 +1,12 @@
import { Game } from '@core/racing/domain/entities/Game';
import { Season } from '@core/racing/domain/entities/season/Season';
import type { LeagueScoringConfig } from '@core/racing/domain/entities/LeagueScoringConfig';
import { InMemoryGameRepository } from '../racing/persistence/inmemory/InMemoryScoringRepositories';
import { InMemorySeasonRepository } from '../racing/persistence/inmemory/InMemoryScoringRepositories';
import { InMemoryLeagueScoringConfigRepository } from '../racing/persistence/inmemory/InMemoryScoringRepositories';
import { InMemoryChampionshipStandingRepository } from '../racing/persistence/inmemory/InMemoryScoringRepositories';
import {
InMemoryChampionshipStandingRepository,
InMemoryGameRepository,
InMemoryLeagueScoringConfigRepository,
InMemorySeasonRepository,
} from '../racing/persistence/inmemory/InMemoryScoringRepositories';
import type { Logger } from '@core/shared/application';
import { getLeagueScoringPresetById } from './LeagueScoringPresets';

View File

@@ -0,0 +1,56 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Logger } from '@core/shared/application';
import type { UserAchievement } from '@core/identity';
import { InMemoryAchievementRepository } from './InMemoryAchievementRepository';
describe('InMemoryAchievementRepository (identity)', () => {
let repository: InMemoryAchievementRepository;
let logger: Logger;
beforeEach(() => {
logger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
} as unknown as Logger;
repository = new InMemoryAchievementRepository(logger);
});
it('seeds predefined achievements', async () => {
const all = await repository.findAllAchievements();
expect(all.length).toBeGreaterThan(0);
const first = all[0]!;
const found = await repository.findAchievementById(first.id);
expect(found?.id).toBe(first.id);
});
it('creates and queries user achievements and stats', async () => {
const all = await repository.findAllAchievements();
const achievement = all[0]!;
const userId = 'user-1';
const ua: UserAchievement = {
id: 'ua-1',
userId,
achievementId: achievement.id,
isComplete: () => true,
} as unknown as UserAchievement;
await repository.createUserAchievement(ua);
expect(await repository.hasUserEarnedAchievement(userId, achievement.id)).toBe(true);
const leaderboard = await repository.getAchievementLeaderboard(10);
expect(leaderboard.length).toBe(1);
expect(leaderboard[0]?.userId).toBe(userId);
expect(leaderboard[0]?.count).toBe(1);
const stats = await repository.getUserAchievementStats(userId);
expect(stats.total).toBe(1);
expect(stats.points).toBeGreaterThan(0);
expect(Object.values(stats.byCategory).reduce((a, b) => a + b, 0)).toBe(1);
});
});

View File

@@ -0,0 +1,69 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Logger } from '@core/shared/application';
import { UserId } from '@core/identity';
import { User } from '@core/identity/domain/entities/User';
import { InMemoryUserRepository } from './InMemoryUserRepository';
import { InMemoryAuthRepository } from './InMemoryAuthRepository';
import { InMemoryPasswordHashingService } from '../../services/InMemoryPasswordHashingService';
describe('InMemoryAuthRepository', () => {
let mockLogger: Logger;
beforeEach(() => {
mockLogger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
} as unknown as Logger;
});
it('creates and verifies a user password', async () => {
const userRepo = new InMemoryUserRepository(mockLogger);
const passwordService = new InMemoryPasswordHashingService();
const authRepo = new InMemoryAuthRepository(userRepo, passwordService, mockLogger);
const user = User.create({
id: UserId.fromString('user-1'),
displayName: 'Test User',
email: 'test@example.com',
});
const created = await authRepo.create(user, 'password123');
expect(created.getEmail()).toBe('test@example.com');
expect(await authRepo.userExists('test@example.com')).toBe(true);
const ok = await authRepo.verifyPassword('test@example.com', 'password123');
expect(ok).not.toBeNull();
expect(ok?.getId().value).toBe('user-1');
const bad = await authRepo.verifyPassword('test@example.com', 'wrong');
expect(bad).toBeNull();
});
it('save updates existing user', async () => {
const userRepo = new InMemoryUserRepository(mockLogger);
const passwordService = new InMemoryPasswordHashingService();
const authRepo = new InMemoryAuthRepository(userRepo, passwordService, mockLogger);
const user = User.create({
id: UserId.fromString('user-2'),
displayName: 'User Two',
email: 'two@example.com',
});
await authRepo.create(user, 'pw');
const updated = User.create({
id: UserId.fromString('user-2'),
displayName: 'User Two Updated',
email: 'two@example.com',
});
await authRepo.save(updated);
const stored = await userRepo.findById('user-2');
expect(stored?.displayName).toBe('User Two Updated');
});
});

View File

@@ -0,0 +1,54 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Logger } from '@core/shared/application';
import { UserId, type SponsorAccount } from '@core/identity';
import { InMemorySponsorAccountRepository } from './InMemorySponsorAccountRepository';
describe('InMemorySponsorAccountRepository', () => {
let repository: InMemorySponsorAccountRepository;
let logger: Logger;
const makeAccount = (id: string, sponsorId: string, email: string): SponsorAccount => {
return {
getId: () => ({ value: id }),
getSponsorId: () => sponsorId,
getEmail: () => email,
} as unknown as SponsorAccount;
};
beforeEach(() => {
logger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
} as unknown as Logger;
repository = new InMemorySponsorAccountRepository(logger);
repository.clear();
});
it('saves and finds sponsor accounts by id / sponsorId / email', async () => {
const account = makeAccount('user-1', 'sponsor-1', 'Sponsor@Example.com');
await repository.save(account);
expect(await repository.findById(UserId.fromString('user-1'))).toBe(account);
expect(await repository.findBySponsorId('sponsor-1')).toBe(account);
expect(await repository.findByEmail('sponsor@example.com')).toBe(account);
});
it('deletes sponsor accounts', async () => {
const account = makeAccount('user-del', 'sponsor-del', 'del@example.com');
await repository.save(account);
await repository.delete(UserId.fromString('user-del'));
expect(await repository.findById(UserId.fromString('user-del'))).toBeNull();
});
it('seeds via constructor', async () => {
const seeded = makeAccount('user-seed', 'sponsor-seed', 'seed@example.com');
repository = new InMemorySponsorAccountRepository(logger, [seeded]);
expect(await repository.findBySponsorId('sponsor-seed')).toBe(seeded);
});
});

View File

@@ -0,0 +1,57 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Logger } from '@core/shared/application';
import type { UserRating } from '@core/identity';
import { InMemoryUserRatingRepository } from './InMemoryUserRatingRepository';
describe('InMemoryUserRatingRepository', () => {
let repository: InMemoryUserRatingRepository;
let mockLogger: Logger;
const rating = (userId: string, driverValue: number, trustValue: number, canSteward: boolean): UserRating => {
return {
userId,
driver: { sampleSize: 10, value: driverValue },
trust: { sampleSize: 5, value: trustValue },
canBeSteward: () => canSteward,
getDriverTier: () => (driverValue >= 2400 ? 'elite' : 'rookie'),
} as unknown as UserRating;
};
beforeEach(() => {
mockLogger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
} as unknown as Logger;
repository = new InMemoryUserRatingRepository(mockLogger);
repository.clear();
});
it('saves and finds ratings', async () => {
await repository.save(rating('u1', 2300, 50, false));
await repository.save(rating('u2', 2500, 70, true));
expect((await repository.findByUserId('u1'))?.userId).toBe('u1');
expect((await repository.findByUserIds(['u1', 'u2'])).length).toBe(2);
const topDrivers = await repository.getTopDrivers(1);
expect(topDrivers[0]?.userId).toBe('u2');
const topTrusted = await repository.getTopTrusted(1);
expect(topTrusted[0]?.userId).toBe('u2');
const stewards = await repository.getEligibleStewards();
expect(stewards.map(r => r.userId)).toEqual(['u2']);
const elite = await repository.findByDriverTier('elite');
expect(elite.map(r => r.userId)).toEqual(['u2']);
});
it('deletes ratings', async () => {
await repository.save(rating('u3', 2200, 10, false));
await repository.delete('u3');
expect(await repository.findByUserId('u3')).toBeNull();
});
});

View File

@@ -0,0 +1,69 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Logger } from '@core/shared/application';
import type { StoredUser } from '@core/identity/domain/repositories/IUserRepository';
import { InMemoryUserRepository } from './InMemoryUserRepository';
describe('InMemoryUserRepository', () => {
let repository: InMemoryUserRepository;
let mockLogger: Logger;
beforeEach(() => {
mockLogger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
} as unknown as Logger;
const seeded: StoredUser[] = [
{
id: 'u1',
email: 'seed@example.com',
displayName: 'Seed',
passwordHash: 'hash',
salt: 'salt',
createdAt: new Date('2025-01-01T00:00:00.000Z'),
},
];
repository = new InMemoryUserRepository(mockLogger, seeded);
});
it('finds by email and id', async () => {
expect((await repository.findByEmail('seed@example.com'))?.id).toBe('u1');
expect((await repository.findById('u1'))?.email).toBe('seed@example.com');
expect(await repository.findByEmail('missing@example.com')).toBeNull();
});
it('creates and rejects duplicate emails', async () => {
const user: StoredUser = {
id: 'u2',
email: 'new@example.com',
displayName: 'New',
passwordHash: 'hash2',
salt: 'salt2',
createdAt: new Date(),
};
await repository.create(user);
expect(await repository.emailExists('new@example.com')).toBe(true);
await expect(repository.create({ ...user, id: 'u3' })).rejects.toThrow('Email already exists');
});
it('updates and maintains email index', async () => {
const existing = await repository.findById('u1');
expect(existing).not.toBeNull();
const updated: StoredUser = {
...(existing as StoredUser),
email: 'changed@example.com',
displayName: 'Changed',
};
await repository.update(updated);
expect(await repository.findByEmail('seed@example.com')).toBeNull();
expect((await repository.findByEmail('changed@example.com'))?.displayName).toBe('Changed');
});
});

View File

@@ -0,0 +1,14 @@
import { describe, expect, it } from 'vitest';
import { InMemoryPasswordHashingService } from './InMemoryPasswordHashingService';
describe('InMemoryPasswordHashingService', () => {
it('hashes and verifies deterministically', async () => {
const service = new InMemoryPasswordHashingService();
const hash = await service.hash('secret');
expect(hash).toBe('demo_salt_terces');
expect(await service.verify('secret', hash)).toBe(true);
expect(await service.verify('wrong', hash)).toBe(false);
});
});

View File

@@ -23,37 +23,47 @@ describe('ConsoleLogger', () => {
consoleErrorSpy.mockRestore();
});
it('should call console.debug with the correct arguments when debug is called', () => {
it('should call console.debug with a formatted message when debug is called', () => {
const message = 'Debug message';
const context = { key: 'value' };
logger.debug(message, context);
expect(consoleDebugSpy).toHaveBeenCalledTimes(1);
expect(consoleDebugSpy).toHaveBeenCalledWith(message, context);
expect(consoleDebugSpy).toHaveBeenCalledWith(
expect.stringContaining('DEBUG: Debug message | {"key":"value"}'),
);
});
it('should call console.info with the correct arguments when info is called', () => {
it('should call console.info with a formatted message when info is called', () => {
const message = 'Info message';
const context = { key: 'value' };
logger.info(message, context);
expect(consoleInfoSpy).toHaveBeenCalledTimes(1);
expect(consoleInfoSpy).toHaveBeenCalledWith(message, context);
expect(consoleInfoSpy).toHaveBeenCalledWith(
expect.stringContaining('INFO: Info message | {"key":"value"}'),
);
});
it('should call console.warn with the correct arguments when warn is called', () => {
it('should call console.warn with a formatted message when warn is called', () => {
const message = 'Warn message';
const context = { key: 'value' };
logger.warn(message, context);
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
expect(consoleWarnSpy).toHaveBeenCalledWith(message, context);
expect(consoleWarnSpy).toHaveBeenCalledWith(
expect.stringContaining('WARN: Warn message | {"key":"value"}'),
);
});
it('should call console.error with the correct arguments when error is called', () => {
it('should call console.error with a formatted message when error is called', () => {
const message = 'Error message';
const error = new Error('Something went wrong');
const context = { key: 'value' };
logger.error(message, error, context);
expect(consoleErrorSpy).toHaveBeenCalledTimes(1);
expect(consoleErrorSpy).toHaveBeenCalledWith(message, error, context);
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining(
'ERROR: Error message | {"key":"value"} | Error: Something went wrong',
),
);
});
});

View File

@@ -0,0 +1,69 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Logger } from '@core/shared/application';
import { AvatarGenerationRequest } from '@core/media/domain/entities/AvatarGenerationRequest';
import { InMemoryAvatarGenerationRepository } from './InMemoryAvatarGenerationRepository';
describe('InMemoryAvatarGenerationRepository', () => {
let repository: InMemoryAvatarGenerationRepository;
let mockLogger: Logger;
beforeEach(() => {
mockLogger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
} as unknown as Logger;
repository = new InMemoryAvatarGenerationRepository(mockLogger);
});
it('saves and finds latest by user', async () => {
const base1 = AvatarGenerationRequest.create({
id: 'req-1',
userId: 'user-1',
facePhotoUrl: 'https://example.com/face1.png',
suitColor: 'red',
});
const base2 = AvatarGenerationRequest.create({
id: 'req-2',
userId: 'user-1',
facePhotoUrl: 'https://example.com/face2.png',
suitColor: 'blue',
});
const r1Props = base1.toProps();
const r2Props = base2.toProps();
const r1 = AvatarGenerationRequest.reconstitute(r1Props);
const r2 = AvatarGenerationRequest.reconstitute({
...r2Props,
createdAt: new Date(r1Props.createdAt.getTime() + 1000),
updatedAt: new Date(r1Props.updatedAt.getTime() + 1000),
});
await repository.save(r1);
await repository.save(r2);
expect((await repository.findById('req-1'))?.id).toBe('req-1');
expect((await repository.findByUserId('user-1')).length).toBe(2);
const latest = await repository.findLatestByUserId('user-1');
expect(latest?.id).toBe('req-2');
});
it('deletes requests', async () => {
const r = AvatarGenerationRequest.create({
id: 'req-del',
userId: 'user-del',
facePhotoUrl: 'https://example.com/face.png',
suitColor: 'green',
});
await repository.save(r);
await repository.delete('req-del');
expect(await repository.findById('req-del')).toBeNull();
});
});

View File

@@ -0,0 +1,21 @@
import { describe, expect, it, vi } from 'vitest';
import type { Logger } from '@core/shared/application';
import { InMemoryFaceValidationAdapter } from './InMemoryFaceValidationAdapter';
describe('InMemoryFaceValidationAdapter', () => {
it('validates face photos as valid (mock)', async () => {
const logger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
} as unknown as Logger;
const adapter = new InMemoryFaceValidationAdapter(logger);
const result = await adapter.validateFacePhoto('data');
expect(result.isValid).toBe(true);
expect(result.hasFace).toBe(true);
expect(result.faceCount).toBe(1);
});
});

View File

@@ -0,0 +1,21 @@
import { describe, expect, it, vi } from 'vitest';
import type { Logger } from '@core/shared/application';
import { InMemoryImageServiceAdapter } from './InMemoryImageServiceAdapter';
describe('InMemoryImageServiceAdapter', () => {
it('returns mock urls', () => {
const logger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
} as unknown as Logger;
const adapter = new InMemoryImageServiceAdapter(logger);
expect(adapter.getDriverAvatar('driver-1')).toContain('/avatars/driver-1.png');
expect(adapter.getTeamLogo('team-1')).toContain('/logos/team-team-1.png');
expect(adapter.getLeagueCover('league-1')).toContain('/covers/league-league-1.png');
expect(adapter.getLeagueLogo('league-1')).toContain('/logos/league-league-1.png');
});
});

View File

@@ -0,0 +1,39 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Logger } from '@core/shared/application';
import { NotificationPreference } from '@core/notifications/domain/entities/NotificationPreference';
import { InMemoryNotificationPreferenceRepository } from './InMemoryNotificationPreferenceRepository';
describe('InMemoryNotificationPreferenceRepository', () => {
let repository: InMemoryNotificationPreferenceRepository;
let logger: Logger;
beforeEach(() => {
logger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
} as unknown as Logger;
repository = new InMemoryNotificationPreferenceRepository(logger);
});
it('returns default preferences when missing', async () => {
const pref = await repository.getOrCreateDefault('driver-1');
expect(pref).toBeInstanceOf(NotificationPreference);
expect(pref.driverId).toBe('driver-1');
const found = await repository.findByDriverId('driver-1');
expect(found?.driverId).toBe('driver-1');
});
it('saves and deletes', async () => {
const pref = NotificationPreference.createDefault('driver-2');
await repository.save(pref);
expect((await repository.findByDriverId('driver-2'))?.driverId).toBe('driver-2');
await repository.delete('driver-2');
expect(await repository.findByDriverId('driver-2')).toBeNull();
});
});

View File

@@ -0,0 +1,80 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Logger } from '@core/shared/application';
import { Notification } from '@core/notifications/domain/entities/Notification';
import { InMemoryNotificationRepository } from './InMemoryNotificationRepository';
describe('InMemoryNotificationRepository', () => {
let repository: InMemoryNotificationRepository;
let logger: Logger;
beforeEach(() => {
logger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
} as unknown as Logger;
repository = new InMemoryNotificationRepository(logger);
});
it('creates, finds, counts and marks as read', async () => {
const n1 = Notification.create({
id: 'n1',
recipientId: 'driver-1',
type: 'system_announcement',
title: 'T1',
body: 'B1',
channel: 'in_app',
status: 'unread',
});
const n2 = Notification.create({
id: 'n2',
recipientId: 'driver-1',
type: 'system_announcement',
title: 'T2',
body: 'B2',
channel: 'in_app',
status: 'unread',
});
const n3 = Notification.create({
id: 'n3',
recipientId: 'driver-2',
type: 'system_announcement',
title: 'T3',
body: 'B3',
channel: 'in_app',
status: 'unread',
});
await repository.create(n1);
await repository.create(n2);
await repository.create(n3);
expect((await repository.findById('n1'))?.id).toBe('n1');
expect((await repository.findByRecipientId('driver-1')).length).toBe(2);
expect(await repository.countUnreadByRecipientId('driver-1')).toBe(2);
await repository.markAllAsReadByRecipientId('driver-1');
expect(await repository.countUnreadByRecipientId('driver-1')).toBe(0);
const unread = await repository.findUnreadByRecipientId('driver-1');
expect(unread).toEqual([]);
});
it('deletes all by recipient', async () => {
const n = Notification.create({
id: 'n-del',
recipientId: 'driver-del',
type: 'system_announcement',
title: 'T',
body: 'B',
channel: 'in_app',
});
await repository.create(n);
await repository.deleteAllByRecipientId('driver-del');
expect(await repository.findById('n-del')).toBeNull();
});
});

View File

@@ -0,0 +1,102 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Logger } from '@core/shared/application/Logger';
import type { MembershipFee } from '@core/payments/domain/entities/MembershipFee';
import { MembershipFeeType } from '@core/payments/domain/entities/MembershipFee';
import type { MemberPayment } from '@core/payments/domain/entities/MemberPayment';
import { MemberPaymentStatus } from '@core/payments/domain/entities/MemberPayment';
import { InMemoryMemberPaymentRepository, InMemoryMembershipFeeRepository } from './InMemoryMembershipFeeRepository';
describe('InMemoryMembershipFeeRepository', () => {
let feeRepo: InMemoryMembershipFeeRepository;
let paymentRepo: InMemoryMemberPaymentRepository;
let logger: Logger;
beforeEach(() => {
logger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
} as unknown as Logger;
feeRepo = new InMemoryMembershipFeeRepository(logger);
paymentRepo = new InMemoryMemberPaymentRepository(logger);
});
it('creates and finds membership fees', async () => {
const fee: MembershipFee = {
id: 'fee-1',
leagueId: 'league-1',
type: MembershipFeeType.SEASON,
amount: 100,
enabled: true,
createdAt: new Date('2025-01-01T00:00:00.000Z'),
updatedAt: new Date('2025-01-01T00:00:00.000Z'),
};
await feeRepo.create(fee);
expect((await feeRepo.findById('fee-1'))?.id).toBe('fee-1');
expect((await feeRepo.findByLeagueId('league-1'))?.id).toBe('fee-1');
const updated = await feeRepo.update({
...fee,
amount: 120,
updatedAt: new Date('2025-01-02T00:00:00.000Z'),
});
expect(updated.amount).toBe(120);
});
it('creates and queries member payments by league via fee lookup', async () => {
const fee1: MembershipFee = {
id: 'fee-a',
leagueId: 'league-a',
type: MembershipFeeType.SEASON,
amount: 100,
enabled: true,
createdAt: new Date('2025-01-01T00:00:00.000Z'),
updatedAt: new Date('2025-01-01T00:00:00.000Z'),
};
const fee2: MembershipFee = {
id: 'fee-b',
leagueId: 'league-b',
type: MembershipFeeType.SEASON,
amount: 50,
enabled: true,
createdAt: new Date('2025-01-01T00:00:00.000Z'),
updatedAt: new Date('2025-01-01T00:00:00.000Z'),
};
await feeRepo.create(fee1);
await feeRepo.create(fee2);
const p1: MemberPayment = {
id: 'mp-1',
feeId: 'fee-a',
driverId: 'driver-1',
amount: 100,
platformFee: 5,
netAmount: 95,
status: MemberPaymentStatus.PENDING,
dueDate: new Date('2025-02-01T00:00:00.000Z'),
};
const p2: MemberPayment = {
id: 'mp-2',
feeId: 'fee-b',
driverId: 'driver-1',
amount: 50,
platformFee: 2.5,
netAmount: 47.5,
status: MemberPaymentStatus.PENDING,
dueDate: new Date('2025-02-01T00:00:00.000Z'),
};
await paymentRepo.create(p1);
await paymentRepo.create(p2);
expect((await paymentRepo.findByFeeIdAndDriverId('fee-a', 'driver-1'))?.id).toBe('mp-1');
const leagueAPayments = await paymentRepo.findByLeagueIdAndDriverId('league-a', 'driver-1', feeRepo);
expect(leagueAPayments.map(p => p.id)).toEqual(['mp-1']);
});
});

View File

@@ -0,0 +1,64 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Logger } from '@core/shared/application/Logger';
import type { Payment } from '@core/payments/domain/entities/Payment';
import { PaymentType, PaymentStatus, PayerType } from '@core/payments/domain/entities/Payment';
import { InMemoryPaymentRepository } from './InMemoryPaymentRepository';
describe('InMemoryPaymentRepository', () => {
let repository: InMemoryPaymentRepository;
let logger: Logger;
beforeEach(() => {
logger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
} as unknown as Logger;
repository = new InMemoryPaymentRepository(logger);
});
it('creates and finds by filters', async () => {
const payment: Payment = {
id: 'pay-1',
type: PaymentType.SPONSORSHIP,
amount: 100,
platformFee: 5,
netAmount: 95,
payerId: 'sponsor-1',
payerType: PayerType.SPONSOR,
leagueId: 'league-1',
status: PaymentStatus.PENDING,
createdAt: new Date('2025-01-01T00:00:00.000Z'),
};
await repository.create(payment);
expect((await repository.findById('pay-1'))?.id).toBe('pay-1');
expect((await repository.findByLeagueId('league-1')).length).toBeGreaterThanOrEqual(1);
expect((await repository.findByPayerId('sponsor-1')).length).toBeGreaterThanOrEqual(1);
expect((await repository.findByType(PaymentType.SPONSORSHIP)).length).toBeGreaterThanOrEqual(1);
const filtered = await repository.findByFilters({ leagueId: 'league-1', payerId: 'sponsor-1', type: PaymentType.SPONSORSHIP });
expect(filtered.map(p => p.id)).toContain('pay-1');
});
it('updates', async () => {
const payment: Payment = {
id: 'pay-2',
type: PaymentType.MEMBERSHIP_FEE,
amount: 50,
platformFee: 2.5,
netAmount: 47.5,
payerId: 'driver-1',
payerType: PayerType.DRIVER,
leagueId: 'league-2',
status: PaymentStatus.PENDING,
createdAt: new Date('2025-01-02T00:00:00.000Z'),
};
await repository.create(payment);
const updated = await repository.update({ ...payment, status: PaymentStatus.COMPLETED, completedAt: new Date('2025-01-03T00:00:00.000Z') });
expect(updated.status).toBe(PaymentStatus.COMPLETED);
});
});

View File

@@ -0,0 +1,45 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Logger } from '@core/shared/application/Logger';
import type { Prize } from '@core/payments/domain/entities/Prize';
import { PrizeType } from '@core/payments/domain/entities/Prize';
import { InMemoryPrizeRepository } from './InMemoryPrizeRepository';
describe('InMemoryPrizeRepository', () => {
let repository: InMemoryPrizeRepository;
let logger: Logger;
beforeEach(() => {
logger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
} as unknown as Logger;
repository = new InMemoryPrizeRepository(logger);
});
it('creates and queries prizes', async () => {
const prize: Prize = {
id: 'prize-1',
leagueId: 'league-1',
seasonId: 'season-1',
position: 1,
name: 'First Place',
amount: 100,
type: PrizeType.CASH,
description: 'First place',
awarded: false,
createdAt: new Date('2025-01-01T00:00:00.000Z'),
};
await repository.create(prize);
expect((await repository.findById('prize-1'))?.id).toBe('prize-1');
expect((await repository.findByLeagueId('league-1')).map(p => p.id)).toContain('prize-1');
expect((await repository.findByLeagueIdAndSeasonId('league-1', 'season-1')).map(p => p.id)).toContain('prize-1');
expect((await repository.findByPosition('league-1', 'season-1', 1))?.id).toBe('prize-1');
await repository.delete('prize-1');
expect(await repository.findById('prize-1')).toBeNull();
});
});

View File

@@ -0,0 +1,60 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Logger } from '@core/shared/application/Logger';
import type { Wallet, Transaction } from '@core/payments/domain/entities/Wallet';
import { TransactionType } from '@core/payments/domain/entities/Wallet';
import { InMemoryTransactionRepository, InMemoryWalletRepository } from './InMemoryWalletRepository';
describe('InMemoryWalletRepository', () => {
let walletRepo: InMemoryWalletRepository;
let txRepo: InMemoryTransactionRepository;
let logger: Logger;
beforeEach(() => {
logger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
} as unknown as Logger;
walletRepo = new InMemoryWalletRepository(logger);
txRepo = new InMemoryTransactionRepository(logger);
});
it('creates and finds wallets', async () => {
const wallet: Wallet = {
id: 'wallet-1',
leagueId: 'league-1',
balance: 0,
totalRevenue: 0,
totalPlatformFees: 0,
totalWithdrawn: 0,
currency: 'USD',
createdAt: new Date('2025-01-01T00:00:00.000Z'),
};
await walletRepo.create(wallet);
expect((await walletRepo.findById('wallet-1'))?.id).toBe('wallet-1');
expect((await walletRepo.findByLeagueId('league-1'))?.id).toBe('wallet-1');
const updated = await walletRepo.update({ ...wallet, balance: 10 });
expect(updated.balance).toBe(10);
});
it('creates and queries transactions', async () => {
const tx: Transaction = {
id: 'tx-1',
walletId: 'wallet-2',
type: TransactionType.DEPOSIT,
amount: 25,
description: 'Test deposit',
createdAt: new Date('2025-01-02T00:00:00.000Z'),
};
await txRepo.create(tx);
expect((await txRepo.findById('tx-1'))?.id).toBe('tx-1');
expect((await txRepo.findByWalletId('wallet-2')).map(t => t.id)).toContain('tx-1');
});
});

View File

@@ -0,0 +1,18 @@
import { describe, expect, it } from 'vitest';
import { InMemoryAchievementRepository } from './InMemoryAchievementRepository';
describe('adapters/persistence/inmemory/achievement/InMemoryAchievementRepository', () => {
it('saves and queries achievements', async () => {
const repo = new InMemoryAchievementRepository();
const achievement: { id: string } = { id: 'a1' };
await repo.save(achievement as unknown as Parameters<InMemoryAchievementRepository['save']>[0]);
const found = await repo.findById('a1');
expect((found as { id: string } | null)?.id).toBe('a1');
const all = await repo.findAll();
expect(all.map(a => (a as { id: string }).id)).toContain('a1');
});
});

View File

@@ -0,0 +1,52 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Logger } from '@core/shared/application';
import { InMemoryLeagueMembershipRepository } from './InMemoryLeagueMembershipRepository';
describe('InMemoryLeagueMembershipRepository', () => {
let repository: InMemoryLeagueMembershipRepository;
let logger: Logger;
beforeEach(() => {
logger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
} as unknown as Logger;
repository = new InMemoryLeagueMembershipRepository(logger);
});
it('saves and queries memberships and join requests', async () => {
const membership: { id: string; leagueId: string; driverId: string; status: string } = {
id: 'm1',
leagueId: 'league-1',
driverId: 'driver-1',
status: 'active',
};
await repository.saveMembership(
membership as unknown as Parameters<InMemoryLeagueMembershipRepository['saveMembership']>[0],
);
expect((await repository.getMembership('league-1', 'driver-1'))?.id).toBe('m1');
expect((await repository.findActiveByLeagueIdAndDriverId('league-1', 'driver-1'))?.id).toBe('m1');
expect((await repository.findAllByLeagueId('league-1')).length).toBe(1);
expect((await repository.findAllByDriverId('driver-1')).length).toBe(1);
expect((await repository.getLeagueMembers('league-1')).length).toBe(1);
const joinRequest: { id: string; leagueId: string } = { id: 'jr1', leagueId: 'league-1' };
await repository.saveJoinRequest(
joinRequest as unknown as Parameters<InMemoryLeagueMembershipRepository['saveJoinRequest']>[0],
);
expect((await repository.getJoinRequests('league-1')).map(r => r.id)).toEqual(['jr1']);
await repository.removeJoinRequest('jr1');
expect((await repository.getJoinRequests('league-1')).length).toBe(0);
await repository.removeMembership('league-1', 'driver-1');
expect(await repository.getMembership('league-1', 'driver-1')).toBeNull();
});
});

View File

@@ -213,10 +213,19 @@ export class InMemoryStandingRepository implements IStandingRepository {
const standingsMap = new Map<string, Standing>();
results.forEach(result => {
const normalizePosition = (position: unknown): number => {
if (typeof position === 'number') return position;
if (typeof position === 'string') return Number(position);
if (position && typeof (position as { toNumber?: unknown }).toNumber === 'function') {
return (position as { toNumber: () => number }).toNumber();
}
return Number(position);
};
results.forEach((result) => {
const driverIdStr = result.driverId.toString();
let standing = standingsMap.get(driverIdStr);
if (!standing) {
standing = Standing.create({
leagueId,
@@ -225,7 +234,8 @@ export class InMemoryStandingRepository implements IStandingRepository {
this.logger.debug(`Created new standing for driver ${driverIdStr} in league ${leagueId}.`);
}
standing = standing.addRaceResult(result.position.toNumber(), resolvedPointsSystem);
const position = normalizePosition((result as { position: unknown }).position);
standing = standing.addRaceResult(position, resolvedPointsSystem);
standingsMap.set(driverIdStr, standing);
this.logger.debug(`Driver ${driverIdStr} in league ${leagueId} accumulated ${standing.points} points.`);
});

View File

@@ -0,0 +1,22 @@
import { describe, expect, it, vi } from 'vitest';
import type { Logger } from '@core/shared/application';
import { InMemoryDriverExtendedProfileProvider } from './InMemoryDriverExtendedProfileProvider';
describe('InMemoryDriverExtendedProfileProvider', () => {
it('returns an extended profile shape', () => {
const logger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
} as unknown as Logger;
const provider = new InMemoryDriverExtendedProfileProvider(logger);
const profile = provider.getExtendedProfile('driver-1');
expect(profile).not.toBeNull();
expect(profile?.socialHandles).toBeInstanceOf(Array);
expect(profile?.achievements).toBeInstanceOf(Array);
expect(typeof profile?.favoriteTrack).toBe('string');
});
});

View File

@@ -0,0 +1,25 @@
import { describe, expect, it, vi } from 'vitest';
import type { Logger } from '@core/shared/application';
import { InMemoryDriverRatingProvider } from './InMemoryDriverRatingProvider';
describe('InMemoryDriverRatingProvider', () => {
it('returns ratings for known drivers and null for unknown', () => {
const logger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
} as unknown as Logger;
const provider = new InMemoryDriverRatingProvider(logger);
expect(provider.getRating('driver-1')).toBe(2500);
expect(provider.getRating('driver-2')).toBe(2400);
expect(provider.getRating('driver-x')).toBeNull();
const map = provider.getRatings(['driver-1', 'driver-x', 'driver-2']);
expect(map.get('driver-1')).toBe(2500);
expect(map.get('driver-2')).toBe(2400);
expect(map.has('driver-x')).toBe(false);
});
});

View File

@@ -0,0 +1,21 @@
import { describe, expect, it, vi } from 'vitest';
import type { Logger } from '@core/shared/application';
import { InMemoryDriverStatsService } from './InMemoryDriverStatsService';
describe('InMemoryDriverStatsService', () => {
it('returns stats for known drivers', () => {
const logger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
} as unknown as Logger;
const service = new InMemoryDriverStatsService(logger);
const stats = service.getDriverStats('driver-1');
expect(stats?.rating).toBe(2500);
expect(service.getDriverStats('unknown')).toBeNull();
});
});

View File

@@ -0,0 +1,21 @@
import { describe, expect, it, vi } from 'vitest';
import type { Logger } from '@core/shared/application';
import { InMemoryRankingService } from './InMemoryRankingService';
describe('InMemoryRankingService', () => {
it('returns mock rankings', () => {
const logger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
} as unknown as Logger;
const service = new InMemoryRankingService(logger);
const rankings = service.getAllDriverRankings();
expect(rankings.length).toBeGreaterThanOrEqual(3);
expect(rankings[0]).toHaveProperty('driverId');
expect(rankings[0]).toHaveProperty('rating');
});
});

View File

@@ -0,0 +1,62 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Logger } from '@core/shared/application';
import type { RacingSeedData } from './InMemorySocialAndFeed';
import { InMemoryFeedRepository, InMemorySocialGraphRepository } from './InMemorySocialAndFeed';
describe('InMemorySocialAndFeed', () => {
let logger: Logger;
beforeEach(() => {
logger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
} as unknown as Logger;
});
it('builds feed for a driver based on friendships', async () => {
const seed = {
drivers: [{ id: 'd1' }, { id: 'd2' }, { id: 'd3' }],
friendships: [
{ driverId: 'd1', friendId: 'd2' },
{ driverId: 'd1', friendId: 'd3' },
],
feedEvents: [
{ id: 'f1', actorDriverId: 'd2', timestamp: new Date('2025-01-02T00:00:00.000Z') },
{ id: 'f2', actorDriverId: 'd3', timestamp: new Date('2025-01-03T00:00:00.000Z') },
{ id: 'f3', actorDriverId: 'd1', timestamp: new Date('2025-01-04T00:00:00.000Z') },
],
};
const feedRepo = new InMemoryFeedRepository(logger, seed as unknown as RacingSeedData);
const feed = await feedRepo.getFeedForDriver('d1');
expect(feed.map(f => (f as { id: string }).id)).toEqual(['f2', 'f1']);
const global = await feedRepo.getGlobalFeed(2);
expect(global.length).toBe(2);
});
it('returns friends and suggestions', async () => {
const seed = {
drivers: [{ id: 'a' }, { id: 'b' }, { id: 'c' }, { id: 'd' }],
friendships: [
{ driverId: 'a', friendId: 'b' },
{ driverId: 'b', friendId: 'c' },
{ driverId: 'a', friendId: 'd' },
{ driverId: 'd', friendId: 'c' },
],
feedEvents: [],
};
const repo = new InMemorySocialGraphRepository(logger, seed as unknown as RacingSeedData);
expect(await repo.getFriendIds('a')).toEqual(['b', 'd']);
const friends = await repo.getFriends('a');
expect(friends.map(d => (d as { id: string }).id).sort()).toEqual(['b', 'd']);
const suggested = await repo.getSuggestedFriends('a');
expect(suggested.map(d => (d as { id: string }).id)).toContain('c');
});
});