fix adapters
This commit is contained in:
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
21
adapters/media/ports/InMemoryFaceValidationAdapter.test.ts
Normal file
21
adapters/media/ports/InMemoryFaceValidationAdapter.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
21
adapters/media/ports/InMemoryImageServiceAdapter.test.ts
Normal file
21
adapters/media/ports/InMemoryImageServiceAdapter.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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.`);
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
25
adapters/racing/ports/InMemoryDriverRatingProvider.test.ts
Normal file
25
adapters/racing/ports/InMemoryDriverRatingProvider.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
21
adapters/racing/services/InMemoryDriverStatsService.test.ts
Normal file
21
adapters/racing/services/InMemoryDriverStatsService.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
21
adapters/racing/services/InMemoryRankingService.test.ts
Normal file
21
adapters/racing/services/InMemoryRankingService.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user