resolve todos in website
This commit is contained in:
@@ -210,6 +210,18 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["core/**/*.ts"],
|
||||||
|
"rules": {
|
||||||
|
"no-restricted-syntax": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"selector": "ExportDefaultDeclaration",
|
||||||
|
"message": "Default exports are forbidden. Use named exports instead."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||||
|
import { GetAnalyticsMetricsUseCase, type GetAnalyticsMetricsInput } from './GetAnalyticsMetricsUseCase';
|
||||||
|
import type { IPageViewRepository } from '../../domain/repositories/IPageViewRepository';
|
||||||
|
import type { Logger } from '@core/shared/application';
|
||||||
|
|
||||||
|
describe('GetAnalyticsMetricsUseCase', () => {
|
||||||
|
let pageViewRepository: {
|
||||||
|
save: Mock;
|
||||||
|
findById: Mock;
|
||||||
|
findByEntityId: Mock;
|
||||||
|
findBySessionId: Mock;
|
||||||
|
countByEntityId: Mock;
|
||||||
|
getUniqueVisitorsCount: Mock;
|
||||||
|
getAverageSessionDuration: Mock;
|
||||||
|
getBounceRate: Mock;
|
||||||
|
};
|
||||||
|
let logger: Logger;
|
||||||
|
let useCase: GetAnalyticsMetricsUseCase;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
pageViewRepository = {
|
||||||
|
save: vi.fn(),
|
||||||
|
findById: vi.fn(),
|
||||||
|
findByEntityId: vi.fn(),
|
||||||
|
findBySessionId: vi.fn(),
|
||||||
|
countByEntityId: vi.fn(),
|
||||||
|
getUniqueVisitorsCount: vi.fn(),
|
||||||
|
getAverageSessionDuration: vi.fn(),
|
||||||
|
getBounceRate: vi.fn(),
|
||||||
|
} as unknown as IPageViewRepository as any;
|
||||||
|
|
||||||
|
logger = {
|
||||||
|
debug: vi.fn(),
|
||||||
|
info: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
} as unknown as Logger;
|
||||||
|
|
||||||
|
useCase = new GetAnalyticsMetricsUseCase(
|
||||||
|
pageViewRepository as unknown as IPageViewRepository,
|
||||||
|
logger,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns default metrics and logs retrieval when no input is provided', async () => {
|
||||||
|
const result = await useCase.execute();
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
pageViews: 0,
|
||||||
|
uniqueVisitors: 0,
|
||||||
|
averageSessionDuration: 0,
|
||||||
|
bounceRate: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect((logger.info as unknown as Mock)).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses provided date range and logs error when execute throws', async () => {
|
||||||
|
const input: GetAnalyticsMetricsInput = {
|
||||||
|
startDate: new Date('2024-01-01'),
|
||||||
|
endDate: new Date('2024-01-31'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const erroringUseCase = new GetAnalyticsMetricsUseCase(
|
||||||
|
pageViewRepository as unknown as IPageViewRepository,
|
||||||
|
logger,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Simulate an error by temporarily spying on logger.info to throw
|
||||||
|
(logger.info as unknown as Mock).mockImplementation(() => {
|
||||||
|
throw new Error('Logging failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(erroringUseCase.execute(input)).rejects.toThrow('Logging failed');
|
||||||
|
expect((logger.error as unknown as Mock)).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { GetDashboardDataUseCase } from './GetDashboardDataUseCase';
|
||||||
|
import type { Logger } from '@core/shared/application';
|
||||||
|
|
||||||
|
describe('GetDashboardDataUseCase', () => {
|
||||||
|
let logger: Logger;
|
||||||
|
let useCase: GetDashboardDataUseCase;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
logger = {
|
||||||
|
debug: vi.fn(),
|
||||||
|
info: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
} as unknown as Logger;
|
||||||
|
|
||||||
|
useCase = new GetDashboardDataUseCase(logger);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns placeholder dashboard metrics and logs retrieval', async () => {
|
||||||
|
const result = await useCase.execute();
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
totalUsers: 0,
|
||||||
|
activeUsers: 0,
|
||||||
|
totalRaces: 0,
|
||||||
|
totalLeagues: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect((logger.info as unknown as ReturnType<typeof vi.fn>)).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -14,7 +14,7 @@ export class GetDashboardDataUseCase {
|
|||||||
private readonly logger: Logger,
|
private readonly logger: Logger,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async execute(_input: GetDashboardDataInput = {}): Promise<GetDashboardDataOutput> {
|
async execute(): Promise<GetDashboardDataOutput> {
|
||||||
try {
|
try {
|
||||||
// Placeholder implementation - would need repositories from identity and racing domains
|
// Placeholder implementation - would need repositories from identity and racing domains
|
||||||
const totalUsers = 0;
|
const totalUsers = 0;
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||||
|
import { GetEntityAnalyticsQuery, type GetEntityAnalyticsInput } from './GetEntityAnalyticsQuery';
|
||||||
|
import type { IPageViewRepository } from '../repositories/IPageViewRepository';
|
||||||
|
import type { IEngagementRepository } from '@core/analytics/domain/repositories/IEngagementRepository';
|
||||||
|
import type { IAnalyticsSnapshotRepository } from '@core/analytics/domain/repositories/IAnalyticsSnapshotRepository';
|
||||||
|
import type { Logger } from '@core/shared/application';
|
||||||
|
import type { EntityType } from '../../domain/types/PageView';
|
||||||
|
|
||||||
|
describe('GetEntityAnalyticsQuery', () => {
|
||||||
|
let pageViewRepository: {
|
||||||
|
countByEntityId: Mock;
|
||||||
|
countUniqueVisitors: Mock;
|
||||||
|
};
|
||||||
|
let engagementRepository: {
|
||||||
|
getSponsorClicksForEntity: Mock;
|
||||||
|
};
|
||||||
|
let snapshotRepository: IAnalyticsSnapshotRepository;
|
||||||
|
let logger: Logger;
|
||||||
|
let useCase: GetEntityAnalyticsQuery;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
pageViewRepository = {
|
||||||
|
countByEntityId: vi.fn(),
|
||||||
|
countUniqueVisitors: vi.fn(),
|
||||||
|
} as unknown as IPageViewRepository as any;
|
||||||
|
|
||||||
|
engagementRepository = {
|
||||||
|
getSponsorClicksForEntity: vi.fn(),
|
||||||
|
} as unknown as IEngagementRepository as any;
|
||||||
|
|
||||||
|
snapshotRepository = {} as IAnalyticsSnapshotRepository;
|
||||||
|
|
||||||
|
logger = {
|
||||||
|
debug: vi.fn(),
|
||||||
|
info: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
} as unknown as Logger;
|
||||||
|
|
||||||
|
useCase = new GetEntityAnalyticsQuery(
|
||||||
|
pageViewRepository as unknown as IPageViewRepository,
|
||||||
|
engagementRepository as unknown as IEngagementRepository,
|
||||||
|
snapshotRepository,
|
||||||
|
logger,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('aggregates entity analytics and returns summary and trends', async () => {
|
||||||
|
const input: GetEntityAnalyticsInput = {
|
||||||
|
entityType: 'league' as EntityType,
|
||||||
|
entityId: 'league-1',
|
||||||
|
period: 'weekly',
|
||||||
|
};
|
||||||
|
|
||||||
|
pageViewRepository.countByEntityId
|
||||||
|
.mockResolvedValueOnce(100) // current period total page views
|
||||||
|
.mockResolvedValueOnce(150); // previous period full page views
|
||||||
|
|
||||||
|
pageViewRepository.countUniqueVisitors
|
||||||
|
.mockResolvedValueOnce(40) // current period uniques
|
||||||
|
.mockResolvedValueOnce(60); // previous period full uniques
|
||||||
|
|
||||||
|
engagementRepository.getSponsorClicksForEntity
|
||||||
|
.mockResolvedValueOnce(10) // current clicks
|
||||||
|
.mockResolvedValueOnce(5); // for engagement score
|
||||||
|
|
||||||
|
const result = await useCase.execute(input);
|
||||||
|
|
||||||
|
expect(result.entityId).toBe(input.entityId);
|
||||||
|
expect(result.entityType).toBe(input.entityType);
|
||||||
|
|
||||||
|
expect(result.summary.totalPageViews).toBe(100);
|
||||||
|
expect(result.summary.uniqueVisitors).toBe(40);
|
||||||
|
expect(result.summary.sponsorClicks).toBe(10);
|
||||||
|
expect(typeof result.summary.engagementScore).toBe('number');
|
||||||
|
expect(result.summary.exposureValue).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
expect(result.trends.pageViewsChange).toBeDefined();
|
||||||
|
expect(result.trends.uniqueVisitorsChange).toBeDefined();
|
||||||
|
|
||||||
|
expect(result.period.start).toBeInstanceOf(Date);
|
||||||
|
expect(result.period.end).toBeInstanceOf(Date);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('propagates repository errors', async () => {
|
||||||
|
const input: GetEntityAnalyticsInput = {
|
||||||
|
entityType: 'league' as EntityType,
|
||||||
|
entityId: 'league-1',
|
||||||
|
};
|
||||||
|
|
||||||
|
pageViewRepository.countByEntityId.mockRejectedValue(new Error('DB error'));
|
||||||
|
|
||||||
|
await expect(useCase.execute(input)).rejects.toThrow('DB error');
|
||||||
|
expect((logger.error as unknown as Mock)).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,8 +5,7 @@
|
|||||||
* Returns metrics formatted for display to sponsors and admins.
|
* Returns metrics formatted for display to sponsors and admins.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { AsyncUseCase } from '@core/shared/application';
|
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||||
import type { Logger } from '@core/shared/application';
|
|
||||||
import type { IPageViewRepository } from '../repositories/IPageViewRepository';
|
import type { IPageViewRepository } from '../repositories/IPageViewRepository';
|
||||||
import type { IEngagementRepository } from '@core/analytics/domain/repositories/IEngagementRepository';
|
import type { IEngagementRepository } from '@core/analytics/domain/repositories/IEngagementRepository';
|
||||||
import type { IAnalyticsSnapshotRepository } from '@core/analytics/domain/repositories/IAnalyticsSnapshotRepository';
|
import type { IAnalyticsSnapshotRepository } from '@core/analytics/domain/repositories/IAnalyticsSnapshotRepository';
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||||
|
import { RecordEngagementUseCase, type RecordEngagementInput } from './RecordEngagementUseCase';
|
||||||
|
import type { IEngagementRepository } from '../../domain/repositories/IEngagementRepository';
|
||||||
|
import { EngagementEvent } from '../../domain/entities/EngagementEvent';
|
||||||
|
import type { Logger } from '@core/shared/application';
|
||||||
|
import type { EngagementAction, EngagementEntityType } from '../../domain/types/EngagementEvent';
|
||||||
|
|
||||||
|
describe('RecordEngagementUseCase', () => {
|
||||||
|
let engagementRepository: {
|
||||||
|
save: Mock;
|
||||||
|
};
|
||||||
|
let logger: Logger;
|
||||||
|
let useCase: RecordEngagementUseCase;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
engagementRepository = {
|
||||||
|
save: vi.fn(),
|
||||||
|
} as unknown as IEngagementRepository as any;
|
||||||
|
|
||||||
|
logger = {
|
||||||
|
debug: vi.fn(),
|
||||||
|
info: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
} as unknown as Logger;
|
||||||
|
|
||||||
|
useCase = new RecordEngagementUseCase(
|
||||||
|
engagementRepository as unknown as IEngagementRepository,
|
||||||
|
logger,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates and saves an EngagementEvent and returns its id and weight', async () => {
|
||||||
|
const input: RecordEngagementInput = {
|
||||||
|
action: 'view' as EngagementAction,
|
||||||
|
entityType: 'league' as EngagementEntityType,
|
||||||
|
entityId: 'league-1',
|
||||||
|
actorId: 'driver-1',
|
||||||
|
actorType: 'driver',
|
||||||
|
sessionId: 'session-1',
|
||||||
|
metadata: { foo: 'bar' },
|
||||||
|
};
|
||||||
|
|
||||||
|
engagementRepository.save.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
const result = await useCase.execute(input);
|
||||||
|
|
||||||
|
expect(engagementRepository.save).toHaveBeenCalledTimes(1);
|
||||||
|
const saved = (engagementRepository.save as unknown as Mock).mock.calls[0][0] as EngagementEvent;
|
||||||
|
|
||||||
|
expect(saved).toBeInstanceOf(EngagementEvent);
|
||||||
|
expect(saved.id).toBeDefined();
|
||||||
|
expect(saved.entityId).toBe(input.entityId);
|
||||||
|
expect(saved.entityType).toBe(input.entityType);
|
||||||
|
expect(result.eventId).toBe(saved.id);
|
||||||
|
expect(typeof result.engagementWeight).toBe('number');
|
||||||
|
expect((logger.info as unknown as Mock)).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('logs and rethrows when repository save fails', async () => {
|
||||||
|
const input: RecordEngagementInput = {
|
||||||
|
action: 'view' as EngagementAction,
|
||||||
|
entityType: 'league' as EngagementEntityType,
|
||||||
|
entityId: 'league-1',
|
||||||
|
actorType: 'anonymous',
|
||||||
|
sessionId: 'session-1',
|
||||||
|
};
|
||||||
|
|
||||||
|
const error = new Error('DB error');
|
||||||
|
engagementRepository.save.mockRejectedValue(error);
|
||||||
|
|
||||||
|
await expect(useCase.execute(input)).rejects.toThrow('DB error');
|
||||||
|
expect((logger.error as unknown as Mock)).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||||
|
import { RecordPageViewUseCase, type RecordPageViewInput } from './RecordPageViewUseCase';
|
||||||
|
import type { IPageViewRepository } from '../../domain/repositories/IPageViewRepository';
|
||||||
|
import { PageView } from '../../domain/entities/PageView';
|
||||||
|
import type { Logger } from '@core/shared/application';
|
||||||
|
import type { EntityType, VisitorType } from '../../domain/types/PageView';
|
||||||
|
|
||||||
|
describe('RecordPageViewUseCase', () => {
|
||||||
|
let pageViewRepository: {
|
||||||
|
save: Mock;
|
||||||
|
};
|
||||||
|
let logger: Logger;
|
||||||
|
let useCase: RecordPageViewUseCase;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
pageViewRepository = {
|
||||||
|
save: vi.fn(),
|
||||||
|
} as unknown as IPageViewRepository as any;
|
||||||
|
|
||||||
|
logger = {
|
||||||
|
debug: vi.fn(),
|
||||||
|
info: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
} as unknown as Logger;
|
||||||
|
|
||||||
|
useCase = new RecordPageViewUseCase(
|
||||||
|
pageViewRepository as unknown as IPageViewRepository,
|
||||||
|
logger,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates and saves a PageView and returns its id', async () => {
|
||||||
|
const input: RecordPageViewInput = {
|
||||||
|
entityType: 'league' as EntityType,
|
||||||
|
entityId: 'league-1',
|
||||||
|
visitorId: 'visitor-1',
|
||||||
|
visitorType: 'anonymous' as VisitorType,
|
||||||
|
sessionId: 'session-1',
|
||||||
|
referrer: 'https://example.com',
|
||||||
|
userAgent: 'jest',
|
||||||
|
country: 'US',
|
||||||
|
};
|
||||||
|
|
||||||
|
pageViewRepository.save.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
const result = await useCase.execute(input);
|
||||||
|
|
||||||
|
expect(pageViewRepository.save).toHaveBeenCalledTimes(1);
|
||||||
|
const saved = (pageViewRepository.save as unknown as Mock).mock.calls[0][0] as PageView;
|
||||||
|
|
||||||
|
expect(saved).toBeInstanceOf(PageView);
|
||||||
|
expect(saved.id).toBeDefined();
|
||||||
|
expect(saved.entityId).toBe(input.entityId);
|
||||||
|
expect(saved.entityType).toBe(input.entityType);
|
||||||
|
expect(result.pageViewId).toBe(saved.id);
|
||||||
|
expect((logger.info as unknown as Mock)).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('logs and rethrows when repository save fails', async () => {
|
||||||
|
const input: RecordPageViewInput = {
|
||||||
|
entityType: 'league' as EntityType,
|
||||||
|
entityId: 'league-1',
|
||||||
|
visitorType: 'anonymous' as VisitorType,
|
||||||
|
sessionId: 'session-1',
|
||||||
|
};
|
||||||
|
|
||||||
|
const error = new Error('DB error');
|
||||||
|
pageViewRepository.save.mockRejectedValue(error);
|
||||||
|
|
||||||
|
await expect(useCase.execute(input)).rejects.toThrow('DB error');
|
||||||
|
expect((logger.error as unknown as Mock)).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -20,7 +20,4 @@ export * from './application/use-cases/RecordPageViewUseCase';
|
|||||||
export * from './application/use-cases/RecordEngagementUseCase';
|
export * from './application/use-cases/RecordEngagementUseCase';
|
||||||
export * from './application/use-cases/GetEntityAnalyticsQuery';
|
export * from './application/use-cases/GetEntityAnalyticsQuery';
|
||||||
|
|
||||||
// Infrastructure (moved to adapters)
|
// Infrastructure (moved to adapters)
|
||||||
export type { IPageViewRepository } from './application/repositories/IPageViewRepository';
|
|
||||||
export type { IEngagementRepository } from './domain/repositories/IEngagementRepository';
|
|
||||||
export type { IAnalyticsSnapshotRepository } from './domain/repositories/IAnalyticsSnapshotRepository';
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||||
|
import { GetCurrentUserSessionUseCase } from './GetCurrentUserSessionUseCase';
|
||||||
|
import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
|
||||||
|
import type { AuthSessionDTO } from '../dto/AuthSessionDTO';
|
||||||
|
|
||||||
|
describe('GetCurrentUserSessionUseCase', () => {
|
||||||
|
let sessionPort: {
|
||||||
|
getCurrentSession: Mock;
|
||||||
|
createSession: Mock;
|
||||||
|
clearSession: Mock;
|
||||||
|
};
|
||||||
|
|
||||||
|
let useCase: GetCurrentUserSessionUseCase;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sessionPort = {
|
||||||
|
getCurrentSession: vi.fn(),
|
||||||
|
createSession: vi.fn(),
|
||||||
|
clearSession: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
useCase = new GetCurrentUserSessionUseCase(sessionPort as unknown as IdentitySessionPort);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the current auth session when one exists', async () => {
|
||||||
|
const session: AuthSessionDTO = {
|
||||||
|
user: {
|
||||||
|
id: 'user-1',
|
||||||
|
email: 'test@example.com',
|
||||||
|
displayName: 'Test User',
|
||||||
|
primaryDriverId: 'driver-1',
|
||||||
|
},
|
||||||
|
issuedAt: Date.now(),
|
||||||
|
expiresAt: Date.now() + 1000,
|
||||||
|
token: 'token-123',
|
||||||
|
};
|
||||||
|
|
||||||
|
sessionPort.getCurrentSession.mockResolvedValue(session);
|
||||||
|
|
||||||
|
const result = await useCase.execute();
|
||||||
|
|
||||||
|
expect(sessionPort.getCurrentSession).toHaveBeenCalledTimes(1);
|
||||||
|
expect(result).toEqual(session);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when there is no active session', async () => {
|
||||||
|
sessionPort.getCurrentSession.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result = await useCase.execute();
|
||||||
|
|
||||||
|
expect(sessionPort.getCurrentSession).toHaveBeenCalledTimes(1);
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
48
core/identity/application/use-cases/GetUserUseCase.test.ts
Normal file
48
core/identity/application/use-cases/GetUserUseCase.test.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||||
|
import { GetUserUseCase } from './GetUserUseCase';
|
||||||
|
import { User } from '../../domain/entities/User';
|
||||||
|
import type { IUserRepository, StoredUser } from '../../domain/repositories/IUserRepository';
|
||||||
|
|
||||||
|
describe('GetUserUseCase', () => {
|
||||||
|
let userRepository: {
|
||||||
|
findById: Mock;
|
||||||
|
};
|
||||||
|
|
||||||
|
let useCase: GetUserUseCase;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
userRepository = {
|
||||||
|
findById: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
useCase = new GetUserUseCase(userRepository as unknown as IUserRepository);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a User when the user exists', async () => {
|
||||||
|
const storedUser: StoredUser = {
|
||||||
|
id: 'user-1',
|
||||||
|
email: 'test@example.com',
|
||||||
|
displayName: 'Test User',
|
||||||
|
passwordHash: 'hash',
|
||||||
|
salt: 'salt',
|
||||||
|
primaryDriverId: 'driver-1',
|
||||||
|
createdAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
userRepository.findById.mockResolvedValue(storedUser);
|
||||||
|
|
||||||
|
const result = await useCase.execute('user-1');
|
||||||
|
|
||||||
|
expect(userRepository.findById).toHaveBeenCalledWith('user-1');
|
||||||
|
expect(result).toBeInstanceOf(User);
|
||||||
|
expect(result.getId().value).toBe('user-1');
|
||||||
|
expect(result.getDisplayName()).toBe('Test User');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when the user does not exist', async () => {
|
||||||
|
userRepository.findById.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(useCase.execute('missing-user')).rejects.toThrow('User not found');
|
||||||
|
expect(userRepository.findById).toHaveBeenCalledWith('missing-user');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||||
|
import { HandleAuthCallbackUseCase } from './HandleAuthCallbackUseCase';
|
||||||
|
import type { IdentityProviderPort } from '../ports/IdentityProviderPort';
|
||||||
|
import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
|
||||||
|
import type { AuthCallbackCommandDTO } from '../dto/AuthCallbackCommandDTO';
|
||||||
|
import type { AuthenticatedUserDTO } from '../dto/AuthenticatedUserDTO';
|
||||||
|
import type { AuthSessionDTO } from '../dto/AuthSessionDTO';
|
||||||
|
|
||||||
|
describe('HandleAuthCallbackUseCase', () => {
|
||||||
|
let provider: {
|
||||||
|
completeAuth: Mock;
|
||||||
|
};
|
||||||
|
let sessionPort: {
|
||||||
|
createSession: Mock;
|
||||||
|
getCurrentSession: Mock;
|
||||||
|
clearSession: Mock;
|
||||||
|
};
|
||||||
|
let useCase: HandleAuthCallbackUseCase;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
provider = {
|
||||||
|
completeAuth: vi.fn(),
|
||||||
|
};
|
||||||
|
sessionPort = {
|
||||||
|
createSession: vi.fn(),
|
||||||
|
getCurrentSession: vi.fn(),
|
||||||
|
clearSession: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
useCase = new HandleAuthCallbackUseCase(
|
||||||
|
provider as unknown as IdentityProviderPort,
|
||||||
|
sessionPort as unknown as IdentitySessionPort,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('completes auth and creates a session', async () => {
|
||||||
|
const command: AuthCallbackCommandDTO = {
|
||||||
|
code: 'auth-code',
|
||||||
|
state: 'state-123',
|
||||||
|
redirectUri: 'https://app/callback',
|
||||||
|
};
|
||||||
|
|
||||||
|
const user: AuthenticatedUserDTO = {
|
||||||
|
id: 'user-1',
|
||||||
|
email: 'test@example.com',
|
||||||
|
displayName: 'Test User',
|
||||||
|
};
|
||||||
|
|
||||||
|
const session: AuthSessionDTO = {
|
||||||
|
user,
|
||||||
|
issuedAt: Date.now(),
|
||||||
|
expiresAt: Date.now() + 1000,
|
||||||
|
token: 'session-token',
|
||||||
|
};
|
||||||
|
|
||||||
|
provider.completeAuth.mockResolvedValue(user);
|
||||||
|
sessionPort.createSession.mockResolvedValue(session);
|
||||||
|
|
||||||
|
const result = await useCase.execute(command);
|
||||||
|
|
||||||
|
expect(provider.completeAuth).toHaveBeenCalledWith(command);
|
||||||
|
expect(sessionPort.createSession).toHaveBeenCalledWith(user);
|
||||||
|
expect(result).toEqual(session);
|
||||||
|
});
|
||||||
|
});
|
||||||
81
core/identity/application/use-cases/LoginUseCase.test.ts
Normal file
81
core/identity/application/use-cases/LoginUseCase.test.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||||
|
import { LoginUseCase } from './LoginUseCase';
|
||||||
|
import { EmailAddress } from '../../domain/value-objects/EmailAddress';
|
||||||
|
import type { IAuthRepository } from '../../domain/repositories/IAuthRepository';
|
||||||
|
import type { IPasswordHashingService } from '../../domain/services/PasswordHashingService';
|
||||||
|
import { User } from '../../domain/entities/User';
|
||||||
|
|
||||||
|
describe('LoginUseCase', () => {
|
||||||
|
let authRepo: {
|
||||||
|
findByEmail: Mock;
|
||||||
|
};
|
||||||
|
let passwordService: {
|
||||||
|
verify: Mock;
|
||||||
|
};
|
||||||
|
let useCase: LoginUseCase;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
authRepo = {
|
||||||
|
findByEmail: vi.fn(),
|
||||||
|
};
|
||||||
|
passwordService = {
|
||||||
|
verify: vi.fn(),
|
||||||
|
};
|
||||||
|
useCase = new LoginUseCase(
|
||||||
|
authRepo as unknown as IAuthRepository,
|
||||||
|
passwordService as unknown as IPasswordHashingService,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the user when credentials are valid', async () => {
|
||||||
|
const email = 'test@example.com';
|
||||||
|
const password = 'password123';
|
||||||
|
const emailVO = EmailAddress.create(email);
|
||||||
|
|
||||||
|
const user = User.create({
|
||||||
|
id: { value: 'user-1' } as any,
|
||||||
|
displayName: 'Test User',
|
||||||
|
email: emailVO.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
(user as any).getPasswordHash = () => ({ value: 'stored-hash' });
|
||||||
|
|
||||||
|
authRepo.findByEmail.mockResolvedValue(user);
|
||||||
|
passwordService.verify.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await useCase.execute(email, password);
|
||||||
|
|
||||||
|
expect(authRepo.findByEmail).toHaveBeenCalledWith(emailVO);
|
||||||
|
expect(passwordService.verify).toHaveBeenCalledWith(password, 'stored-hash');
|
||||||
|
expect(result).toBe(user);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when user is not found', async () => {
|
||||||
|
const email = 'missing@example.com';
|
||||||
|
|
||||||
|
authRepo.findByEmail.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(useCase.execute(email, 'password')).rejects.toThrow('Invalid credentials');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when password is invalid', async () => {
|
||||||
|
const email = 'test@example.com';
|
||||||
|
const password = 'wrong-password';
|
||||||
|
const emailVO = EmailAddress.create(email);
|
||||||
|
|
||||||
|
const user = User.create({
|
||||||
|
id: { value: 'user-1' } as any,
|
||||||
|
displayName: 'Test User',
|
||||||
|
email: emailVO.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
(user as any).getPasswordHash = () => ({ value: 'stored-hash' });
|
||||||
|
|
||||||
|
authRepo.findByEmail.mockResolvedValue(user);
|
||||||
|
passwordService.verify.mockResolvedValue(false);
|
||||||
|
|
||||||
|
await expect(useCase.execute(email, password)).rejects.toThrow('Invalid credentials');
|
||||||
|
expect(authRepo.findByEmail).toHaveBeenCalled();
|
||||||
|
expect(passwordService.verify).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||||
|
import { LoginWithEmailUseCase, type LoginCommandDTO } from './LoginWithEmailUseCase';
|
||||||
|
import type { IUserRepository, StoredUser } from '../../domain/repositories/IUserRepository';
|
||||||
|
import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
|
||||||
|
import type { AuthSessionDTO } from '../dto/AuthSessionDTO';
|
||||||
|
|
||||||
|
describe('LoginWithEmailUseCase', () => {
|
||||||
|
let userRepository: {
|
||||||
|
findByEmail: Mock;
|
||||||
|
};
|
||||||
|
let sessionPort: {
|
||||||
|
createSession: Mock;
|
||||||
|
getCurrentSession: Mock;
|
||||||
|
clearSession: Mock;
|
||||||
|
};
|
||||||
|
let useCase: LoginWithEmailUseCase;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
userRepository = {
|
||||||
|
findByEmail: vi.fn(),
|
||||||
|
};
|
||||||
|
sessionPort = {
|
||||||
|
createSession: vi.fn(),
|
||||||
|
getCurrentSession: vi.fn(),
|
||||||
|
clearSession: vi.fn(),
|
||||||
|
};
|
||||||
|
useCase = new LoginWithEmailUseCase(
|
||||||
|
userRepository as unknown as IUserRepository,
|
||||||
|
sessionPort as unknown as IdentitySessionPort,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates a session for valid credentials', async () => {
|
||||||
|
const command: LoginCommandDTO = {
|
||||||
|
email: 'Test@Example.com',
|
||||||
|
password: 'password123',
|
||||||
|
};
|
||||||
|
|
||||||
|
const storedUser: StoredUser = {
|
||||||
|
id: 'user-1',
|
||||||
|
email: 'test@example.com',
|
||||||
|
displayName: 'Test User',
|
||||||
|
passwordHash: 'hashed-password',
|
||||||
|
salt: 'salt',
|
||||||
|
createdAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const session: AuthSessionDTO = {
|
||||||
|
user: {
|
||||||
|
id: storedUser.id,
|
||||||
|
email: storedUser.email,
|
||||||
|
displayName: storedUser.displayName,
|
||||||
|
},
|
||||||
|
issuedAt: Date.now(),
|
||||||
|
expiresAt: Date.now() + 1000,
|
||||||
|
token: 'token-123',
|
||||||
|
};
|
||||||
|
|
||||||
|
userRepository.findByEmail.mockResolvedValue(storedUser);
|
||||||
|
sessionPort.createSession.mockResolvedValue(session);
|
||||||
|
|
||||||
|
const result = await useCase.execute(command);
|
||||||
|
|
||||||
|
expect(userRepository.findByEmail).toHaveBeenCalledWith('test@example.com');
|
||||||
|
expect(sessionPort.createSession).toHaveBeenCalledWith({
|
||||||
|
id: storedUser.id,
|
||||||
|
email: storedUser.email,
|
||||||
|
displayName: storedUser.displayName,
|
||||||
|
});
|
||||||
|
expect(result).toEqual(session);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when email or password is missing', async () => {
|
||||||
|
await expect(useCase.execute({ email: '', password: 'x' })).rejects.toThrow('Email and password are required');
|
||||||
|
await expect(useCase.execute({ email: 'a@example.com', password: '' })).rejects.toThrow('Email and password are required');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when user does not exist', async () => {
|
||||||
|
const command: LoginCommandDTO = {
|
||||||
|
email: 'missing@example.com',
|
||||||
|
password: 'password',
|
||||||
|
};
|
||||||
|
|
||||||
|
userRepository.findByEmail.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(useCase.execute(command)).rejects.toThrow('Invalid email or password');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when password is invalid', async () => {
|
||||||
|
const command: LoginCommandDTO = {
|
||||||
|
email: 'test@example.com',
|
||||||
|
password: 'wrong',
|
||||||
|
};
|
||||||
|
|
||||||
|
const storedUser: StoredUser = {
|
||||||
|
id: 'user-1',
|
||||||
|
email: 'test@example.com',
|
||||||
|
displayName: 'Test User',
|
||||||
|
passwordHash: 'different-hash',
|
||||||
|
salt: 'salt',
|
||||||
|
createdAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
userRepository.findByEmail.mockResolvedValue(storedUser);
|
||||||
|
|
||||||
|
await expect(useCase.execute(command)).rejects.toThrow('Invalid email or password');
|
||||||
|
});
|
||||||
|
});
|
||||||
28
core/identity/application/use-cases/LogoutUseCase.test.ts
Normal file
28
core/identity/application/use-cases/LogoutUseCase.test.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||||
|
import { LogoutUseCase } from './LogoutUseCase';
|
||||||
|
import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
|
||||||
|
|
||||||
|
describe('LogoutUseCase', () => {
|
||||||
|
let sessionPort: {
|
||||||
|
clearSession: Mock;
|
||||||
|
getCurrentSession: Mock;
|
||||||
|
createSession: Mock;
|
||||||
|
};
|
||||||
|
let useCase: LogoutUseCase;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sessionPort = {
|
||||||
|
clearSession: vi.fn(),
|
||||||
|
getCurrentSession: vi.fn(),
|
||||||
|
createSession: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
useCase = new LogoutUseCase(sessionPort as unknown as IdentitySessionPort);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears the current session', async () => {
|
||||||
|
await useCase.execute();
|
||||||
|
|
||||||
|
expect(sessionPort.clearSession).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
71
core/identity/application/use-cases/SignupUseCase.test.ts
Normal file
71
core/identity/application/use-cases/SignupUseCase.test.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||||
|
import { SignupUseCase } from './SignupUseCase';
|
||||||
|
import { EmailAddress } from '../../domain/value-objects/EmailAddress';
|
||||||
|
import { UserId } from '../../domain/value-objects/UserId';
|
||||||
|
import { User } from '../../domain/entities/User';
|
||||||
|
import type { IAuthRepository } from '../../domain/repositories/IAuthRepository';
|
||||||
|
import type { IPasswordHashingService } from '../../domain/services/PasswordHashingService';
|
||||||
|
|
||||||
|
vi.mock('../../domain/value-objects/PasswordHash', () => ({
|
||||||
|
PasswordHash: {
|
||||||
|
fromHash: (hash: string) => ({ value: hash }),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('SignupUseCase', () => {
|
||||||
|
let authRepo: {
|
||||||
|
findByEmail: Mock;
|
||||||
|
save: Mock;
|
||||||
|
};
|
||||||
|
let passwordService: {
|
||||||
|
hash: Mock;
|
||||||
|
};
|
||||||
|
let useCase: SignupUseCase;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
authRepo = {
|
||||||
|
findByEmail: vi.fn(),
|
||||||
|
save: vi.fn(),
|
||||||
|
};
|
||||||
|
passwordService = {
|
||||||
|
hash: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
useCase = new SignupUseCase(
|
||||||
|
authRepo as unknown as IAuthRepository,
|
||||||
|
passwordService as unknown as IPasswordHashingService,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates and saves a new user when email is free', async () => {
|
||||||
|
const email = 'new@example.com';
|
||||||
|
const password = 'password123';
|
||||||
|
const displayName = 'New User';
|
||||||
|
|
||||||
|
authRepo.findByEmail.mockResolvedValue(null);
|
||||||
|
passwordService.hash.mockResolvedValue('hashed-password');
|
||||||
|
|
||||||
|
const result = await useCase.execute(email, password, displayName);
|
||||||
|
|
||||||
|
expect(authRepo.findByEmail).toHaveBeenCalledWith(EmailAddress.create(email));
|
||||||
|
expect(passwordService.hash).toHaveBeenCalledWith(password);
|
||||||
|
expect(authRepo.save).toHaveBeenCalled();
|
||||||
|
|
||||||
|
expect(result).toBeInstanceOf(User);
|
||||||
|
expect(result.getDisplayName()).toBe(displayName);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when user already exists', async () => {
|
||||||
|
const email = 'existing@example.com';
|
||||||
|
|
||||||
|
const existingUser = User.create({
|
||||||
|
id: UserId.create(),
|
||||||
|
displayName: 'Existing User',
|
||||||
|
email,
|
||||||
|
});
|
||||||
|
|
||||||
|
authRepo.findByEmail.mockResolvedValue(existingUser);
|
||||||
|
|
||||||
|
await expect(useCase.execute(email, 'password', 'Existing User')).rejects.toThrow('User already exists');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||||
|
import { SignupWithEmailUseCase, type SignupCommandDTO } from './SignupWithEmailUseCase';
|
||||||
|
import type { IUserRepository, StoredUser } from '../../domain/repositories/IUserRepository';
|
||||||
|
import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
|
||||||
|
import type { AuthSessionDTO } from '../dto/AuthSessionDTO';
|
||||||
|
|
||||||
|
describe('SignupWithEmailUseCase', () => {
|
||||||
|
let userRepository: {
|
||||||
|
findByEmail: Mock;
|
||||||
|
create: Mock;
|
||||||
|
};
|
||||||
|
let sessionPort: {
|
||||||
|
createSession: Mock;
|
||||||
|
getCurrentSession: Mock;
|
||||||
|
clearSession: Mock;
|
||||||
|
};
|
||||||
|
let useCase: SignupWithEmailUseCase;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
userRepository = {
|
||||||
|
findByEmail: vi.fn(),
|
||||||
|
create: vi.fn(),
|
||||||
|
};
|
||||||
|
sessionPort = {
|
||||||
|
createSession: vi.fn(),
|
||||||
|
getCurrentSession: vi.fn(),
|
||||||
|
clearSession: vi.fn(),
|
||||||
|
};
|
||||||
|
useCase = new SignupWithEmailUseCase(
|
||||||
|
userRepository as unknown as IUserRepository,
|
||||||
|
sessionPort as unknown as IdentitySessionPort,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates a new user and session for valid input', async () => {
|
||||||
|
const command: SignupCommandDTO = {
|
||||||
|
email: 'new@example.com',
|
||||||
|
password: 'password123',
|
||||||
|
displayName: 'New User',
|
||||||
|
};
|
||||||
|
|
||||||
|
userRepository.findByEmail.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const session: AuthSessionDTO = {
|
||||||
|
user: {
|
||||||
|
id: 'user-1',
|
||||||
|
email: command.email.toLowerCase(),
|
||||||
|
displayName: command.displayName,
|
||||||
|
},
|
||||||
|
issuedAt: Date.now(),
|
||||||
|
expiresAt: Date.now() + 1000,
|
||||||
|
token: 'session-token',
|
||||||
|
};
|
||||||
|
|
||||||
|
sessionPort.createSession.mockResolvedValue(session);
|
||||||
|
|
||||||
|
const result = await useCase.execute(command);
|
||||||
|
|
||||||
|
expect(userRepository.findByEmail).toHaveBeenCalledWith(command.email);
|
||||||
|
expect(userRepository.create).toHaveBeenCalled();
|
||||||
|
expect(sessionPort.createSession).toHaveBeenCalledWith({
|
||||||
|
id: expect.any(String),
|
||||||
|
email: command.email.toLowerCase(),
|
||||||
|
displayName: command.displayName,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.session).toEqual(session);
|
||||||
|
expect(result.isNewUser).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when email format is invalid', async () => {
|
||||||
|
const command: SignupCommandDTO = {
|
||||||
|
email: 'invalid-email',
|
||||||
|
password: 'password123',
|
||||||
|
displayName: 'User',
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(useCase.execute(command)).rejects.toThrow('Invalid email format');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when password is too short', async () => {
|
||||||
|
const command: SignupCommandDTO = {
|
||||||
|
email: 'valid@example.com',
|
||||||
|
password: 'short',
|
||||||
|
displayName: 'User',
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(useCase.execute(command)).rejects.toThrow('Password must be at least 8 characters');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when display name is too short', async () => {
|
||||||
|
const command: SignupCommandDTO = {
|
||||||
|
email: 'valid@example.com',
|
||||||
|
password: 'password123',
|
||||||
|
displayName: ' ',
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(useCase.execute(command)).rejects.toThrow('Display name must be at least 2 characters');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when email already exists', async () => {
|
||||||
|
const command: SignupCommandDTO = {
|
||||||
|
email: 'existing@example.com',
|
||||||
|
password: 'password123',
|
||||||
|
displayName: 'Existing User',
|
||||||
|
};
|
||||||
|
|
||||||
|
const existingUser: StoredUser = {
|
||||||
|
id: 'user-1',
|
||||||
|
email: command.email,
|
||||||
|
displayName: command.displayName,
|
||||||
|
passwordHash: 'hash',
|
||||||
|
salt: 'salt',
|
||||||
|
createdAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
userRepository.findByEmail.mockResolvedValue(existingUser);
|
||||||
|
|
||||||
|
await expect(useCase.execute(command)).rejects.toThrow('An account with this email already exists');
|
||||||
|
});
|
||||||
|
});
|
||||||
35
core/identity/application/use-cases/StartAuthUseCase.test.ts
Normal file
35
core/identity/application/use-cases/StartAuthUseCase.test.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||||
|
import { StartAuthUseCase } from './StartAuthUseCase';
|
||||||
|
import type { IdentityProviderPort } from '../ports/IdentityProviderPort';
|
||||||
|
import type { StartAuthCommandDTO } from '../dto/StartAuthCommandDTO';
|
||||||
|
|
||||||
|
describe('StartAuthUseCase', () => {
|
||||||
|
let provider: {
|
||||||
|
startAuth: Mock;
|
||||||
|
};
|
||||||
|
let useCase: StartAuthUseCase;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
provider = {
|
||||||
|
startAuth: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
useCase = new StartAuthUseCase(provider as unknown as IdentityProviderPort);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('delegates to the identity provider to start auth', async () => {
|
||||||
|
const command: StartAuthCommandDTO = {
|
||||||
|
redirectUri: 'https://app/callback',
|
||||||
|
provider: 'demo',
|
||||||
|
};
|
||||||
|
|
||||||
|
const expected = { redirectUrl: 'https://auth/redirect', state: 'state-123' };
|
||||||
|
|
||||||
|
provider.startAuth.mockResolvedValue(expected);
|
||||||
|
|
||||||
|
const result = await useCase.execute(command);
|
||||||
|
|
||||||
|
expect(provider.startAuth).toHaveBeenCalledWith(command);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||||
|
import { CreateAchievementUseCase, type IAchievementRepository } from './CreateAchievementUseCase';
|
||||||
|
import { Achievement } from '@core/identity/domain/entities/Achievement';
|
||||||
|
|
||||||
|
describe('CreateAchievementUseCase', () => {
|
||||||
|
let achievementRepository: {
|
||||||
|
save: Mock;
|
||||||
|
findById: Mock;
|
||||||
|
};
|
||||||
|
let useCase: CreateAchievementUseCase;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
achievementRepository = {
|
||||||
|
save: vi.fn(),
|
||||||
|
findById: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
useCase = new CreateAchievementUseCase(achievementRepository as unknown as IAchievementRepository);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates an achievement and persists it', async () => {
|
||||||
|
const props = {
|
||||||
|
id: 'achv-1',
|
||||||
|
name: 'First Win',
|
||||||
|
description: 'Awarded for winning your first race',
|
||||||
|
category: 'driver' as const,
|
||||||
|
rarity: 'common' as const,
|
||||||
|
iconUrl: 'https://example.com/icon.png',
|
||||||
|
points: 50,
|
||||||
|
requirements: [
|
||||||
|
{
|
||||||
|
type: 'wins',
|
||||||
|
value: 1,
|
||||||
|
operator: '>=',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isSecret: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
achievementRepository.save.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
const result = await useCase.execute(props);
|
||||||
|
|
||||||
|
expect(result).toBeInstanceOf(Achievement);
|
||||||
|
expect(result.id).toBe(props.id);
|
||||||
|
expect(result.name).toBe(props.name);
|
||||||
|
expect(result.description).toBe(props.description);
|
||||||
|
expect(result.category).toBe(props.category);
|
||||||
|
expect(result.points).toBe(props.points);
|
||||||
|
expect(result.requirements).toHaveLength(1);
|
||||||
|
expect(achievementRepository.save).toHaveBeenCalledWith(result);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,9 +11,6 @@ export * from './domain/repositories/ISponsorAccountRepository';
|
|||||||
export * from './domain/repositories/IUserRatingRepository';
|
export * from './domain/repositories/IUserRatingRepository';
|
||||||
export * from './domain/repositories/IAchievementRepository';
|
export * from './domain/repositories/IAchievementRepository';
|
||||||
|
|
||||||
export * from './infrastructure/repositories/InMemoryUserRatingRepository';
|
|
||||||
export * from './infrastructure/repositories/InMemoryAchievementRepository';
|
|
||||||
|
|
||||||
export * from './application/dto/AuthenticatedUserDTO';
|
export * from './application/dto/AuthenticatedUserDTO';
|
||||||
export * from './application/dto/AuthSessionDTO';
|
export * from './application/dto/AuthSessionDTO';
|
||||||
export * from './application/dto/AuthCallbackCommandDTO';
|
export * from './application/dto/AuthCallbackCommandDTO';
|
||||||
@@ -24,4 +21,4 @@ export * from './application/dto/IracingAuthStateDTO';
|
|||||||
export * from './application/use-cases/StartAuthUseCase';
|
export * from './application/use-cases/StartAuthUseCase';
|
||||||
export * from './application/use-cases/HandleAuthCallbackUseCase';
|
export * from './application/use-cases/HandleAuthCallbackUseCase';
|
||||||
export * from './application/use-cases/GetCurrentUserSessionUseCase';
|
export * from './application/use-cases/GetCurrentUserSessionUseCase';
|
||||||
export * from './application/use-cases/LogoutUseCase';
|
export * from './application/use-cases/LogoutUseCase';
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||||
|
import { GetLeagueStandingsUseCaseImpl } from './GetLeagueStandingsUseCaseImpl';
|
||||||
|
import type { ILeagueStandingsRepository, RawStanding } from '../ports/ILeagueStandingsRepository';
|
||||||
|
|
||||||
|
describe('GetLeagueStandingsUseCaseImpl', () => {
|
||||||
|
let repository: {
|
||||||
|
getLeagueStandings: Mock;
|
||||||
|
};
|
||||||
|
let useCase: GetLeagueStandingsUseCaseImpl;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
repository = {
|
||||||
|
getLeagueStandings: vi.fn(),
|
||||||
|
} as unknown as ILeagueStandingsRepository as any;
|
||||||
|
|
||||||
|
useCase = new GetLeagueStandingsUseCaseImpl(repository as unknown as ILeagueStandingsRepository);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps raw standings from repository to view model', async () => {
|
||||||
|
const leagueId = 'league-1';
|
||||||
|
const rawStandings: RawStanding[] = [
|
||||||
|
{
|
||||||
|
id: 's1',
|
||||||
|
leagueId,
|
||||||
|
seasonId: 'season-1',
|
||||||
|
driverId: 'driver-1',
|
||||||
|
position: 1,
|
||||||
|
points: 100,
|
||||||
|
wins: 3,
|
||||||
|
podiums: 5,
|
||||||
|
racesCompleted: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 's2',
|
||||||
|
leagueId,
|
||||||
|
seasonId: null,
|
||||||
|
driverId: 'driver-2',
|
||||||
|
position: 2,
|
||||||
|
points: 80,
|
||||||
|
wins: 1,
|
||||||
|
podiums: null,
|
||||||
|
racesCompleted: 10,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
repository.getLeagueStandings.mockResolvedValue(rawStandings);
|
||||||
|
|
||||||
|
const result = await useCase.execute(leagueId);
|
||||||
|
|
||||||
|
expect(repository.getLeagueStandings).toHaveBeenCalledWith(leagueId);
|
||||||
|
expect(result.leagueId).toBe(leagueId);
|
||||||
|
expect(result.standings).toEqual([
|
||||||
|
{
|
||||||
|
id: 's1',
|
||||||
|
leagueId,
|
||||||
|
seasonId: 'season-1',
|
||||||
|
driverId: 'driver-1',
|
||||||
|
position: 1,
|
||||||
|
points: 100,
|
||||||
|
wins: 3,
|
||||||
|
podiums: 5,
|
||||||
|
racesCompleted: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 's2',
|
||||||
|
leagueId,
|
||||||
|
seasonId: '',
|
||||||
|
driverId: 'driver-2',
|
||||||
|
position: 2,
|
||||||
|
points: 80,
|
||||||
|
wins: 1,
|
||||||
|
podiums: 0,
|
||||||
|
racesCompleted: 10,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
97
core/media/application/use-cases/DeleteMediaUseCase.test.ts
Normal file
97
core/media/application/use-cases/DeleteMediaUseCase.test.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||||
|
import { DeleteMediaUseCase } from './DeleteMediaUseCase';
|
||||||
|
import type { IMediaRepository } from '../../domain/repositories/IMediaRepository';
|
||||||
|
import type { MediaStoragePort } from '../ports/MediaStoragePort';
|
||||||
|
import type { IDeleteMediaPresenter } from '../presenters/IDeleteMediaPresenter';
|
||||||
|
import type { Logger } from '@core/shared/application';
|
||||||
|
import { Media } from '../../domain/entities/Media';
|
||||||
|
import { MediaUrl } from '../../domain/value-objects/MediaUrl';
|
||||||
|
|
||||||
|
describe('DeleteMediaUseCase', () => {
|
||||||
|
let mediaRepo: {
|
||||||
|
findById: Mock;
|
||||||
|
delete: Mock;
|
||||||
|
};
|
||||||
|
let mediaStorage: {
|
||||||
|
deleteMedia: Mock;
|
||||||
|
};
|
||||||
|
let logger: Logger;
|
||||||
|
let presenter: IDeleteMediaPresenter & { result?: any };
|
||||||
|
let useCase: DeleteMediaUseCase;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mediaRepo = {
|
||||||
|
findById: vi.fn(),
|
||||||
|
delete: vi.fn(),
|
||||||
|
} as unknown as IMediaRepository as any;
|
||||||
|
|
||||||
|
mediaStorage = {
|
||||||
|
deleteMedia: vi.fn(),
|
||||||
|
} as unknown as MediaStoragePort as any;
|
||||||
|
|
||||||
|
logger = {
|
||||||
|
debug: vi.fn(),
|
||||||
|
info: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
} as unknown as Logger;
|
||||||
|
|
||||||
|
presenter = {
|
||||||
|
present: vi.fn((result) => {
|
||||||
|
(presenter as any).result = result;
|
||||||
|
}),
|
||||||
|
} as unknown as IDeleteMediaPresenter & { result?: any };
|
||||||
|
|
||||||
|
useCase = new DeleteMediaUseCase(
|
||||||
|
mediaRepo as unknown as IMediaRepository,
|
||||||
|
mediaStorage as unknown as MediaStoragePort,
|
||||||
|
logger,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns error result when media is not found', async () => {
|
||||||
|
mediaRepo.findById.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await useCase.execute({ mediaId: 'missing' }, presenter);
|
||||||
|
|
||||||
|
expect(mediaRepo.findById).toHaveBeenCalledWith('missing');
|
||||||
|
expect((presenter.present as unknown as Mock)).toHaveBeenCalledWith({
|
||||||
|
success: false,
|
||||||
|
errorMessage: 'Media not found',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes media from storage and repository on success', async () => {
|
||||||
|
const media = Media.create({
|
||||||
|
id: 'media-1',
|
||||||
|
filename: 'file.png',
|
||||||
|
originalName: 'file.png',
|
||||||
|
mimeType: 'image/png',
|
||||||
|
size: 123,
|
||||||
|
url: MediaUrl.create('https://example.com/file.png'),
|
||||||
|
type: 'image',
|
||||||
|
uploadedBy: 'user-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
mediaRepo.findById.mockResolvedValue(media);
|
||||||
|
|
||||||
|
await useCase.execute({ mediaId: 'media-1' }, presenter);
|
||||||
|
|
||||||
|
expect(mediaRepo.findById).toHaveBeenCalledWith('media-1');
|
||||||
|
expect(mediaStorage.deleteMedia).toHaveBeenCalledWith(media.url.value);
|
||||||
|
expect(mediaRepo.delete).toHaveBeenCalledWith('media-1');
|
||||||
|
expect((presenter.present as unknown as Mock)).toHaveBeenCalledWith({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles errors and presents failure result', async () => {
|
||||||
|
mediaRepo.findById.mockRejectedValue(new Error('DB error'));
|
||||||
|
|
||||||
|
await useCase.execute({ mediaId: 'media-1' }, presenter);
|
||||||
|
|
||||||
|
expect((logger.error as unknown as Mock)).toHaveBeenCalled();
|
||||||
|
expect((presenter.present as unknown as Mock)).toHaveBeenCalledWith({
|
||||||
|
success: false,
|
||||||
|
errorMessage: 'Internal error occurred while deleting media',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
93
core/media/application/use-cases/GetAvatarUseCase.test.ts
Normal file
93
core/media/application/use-cases/GetAvatarUseCase.test.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||||
|
import { GetAvatarUseCase } from './GetAvatarUseCase';
|
||||||
|
import type { IAvatarRepository } from '../../domain/repositories/IAvatarRepository';
|
||||||
|
import type { IGetAvatarPresenter } from '../presenters/IGetAvatarPresenter';
|
||||||
|
import type { Logger } from '@core/shared/application';
|
||||||
|
import { Avatar } from '../../domain/entities/Avatar';
|
||||||
|
import { MediaUrl } from '../../domain/value-objects/MediaUrl';
|
||||||
|
|
||||||
|
interface TestPresenter extends IGetAvatarPresenter {
|
||||||
|
result?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('GetAvatarUseCase', () => {
|
||||||
|
let avatarRepo: {
|
||||||
|
findActiveByDriverId: Mock;
|
||||||
|
save: Mock;
|
||||||
|
};
|
||||||
|
let logger: Logger;
|
||||||
|
let presenter: TestPresenter;
|
||||||
|
let useCase: GetAvatarUseCase;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
avatarRepo = {
|
||||||
|
findActiveByDriverId: vi.fn(),
|
||||||
|
save: vi.fn(),
|
||||||
|
} as unknown as IAvatarRepository as any;
|
||||||
|
|
||||||
|
logger = {
|
||||||
|
debug: vi.fn(),
|
||||||
|
info: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
} as unknown as Logger;
|
||||||
|
|
||||||
|
presenter = {
|
||||||
|
present: vi.fn((result) => {
|
||||||
|
presenter.result = result;
|
||||||
|
}),
|
||||||
|
} as unknown as TestPresenter;
|
||||||
|
|
||||||
|
useCase = new GetAvatarUseCase(
|
||||||
|
avatarRepo as unknown as IAvatarRepository,
|
||||||
|
logger,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('presents error when no avatar exists for driver', async () => {
|
||||||
|
avatarRepo.findActiveByDriverId.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await useCase.execute({ driverId: 'driver-1' }, presenter);
|
||||||
|
|
||||||
|
expect(avatarRepo.findActiveByDriverId).toHaveBeenCalledWith('driver-1');
|
||||||
|
expect((presenter.present as unknown as Mock)).toHaveBeenCalledWith({
|
||||||
|
success: false,
|
||||||
|
errorMessage: 'Avatar not found',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('presents avatar details when avatar exists', async () => {
|
||||||
|
const avatar = Avatar.create({
|
||||||
|
id: 'avatar-1',
|
||||||
|
driverId: 'driver-1',
|
||||||
|
mediaUrl: MediaUrl.create('https://example.com/avatar.png'),
|
||||||
|
});
|
||||||
|
|
||||||
|
avatarRepo.findActiveByDriverId.mockResolvedValue(avatar);
|
||||||
|
|
||||||
|
await useCase.execute({ driverId: 'driver-1' }, presenter);
|
||||||
|
|
||||||
|
expect(avatarRepo.findActiveByDriverId).toHaveBeenCalledWith('driver-1');
|
||||||
|
expect((presenter.present as unknown as Mock)).toHaveBeenCalledWith({
|
||||||
|
success: true,
|
||||||
|
avatar: {
|
||||||
|
id: avatar.id,
|
||||||
|
driverId: avatar.driverId,
|
||||||
|
mediaUrl: avatar.mediaUrl.value,
|
||||||
|
selectedAt: avatar.selectedAt,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles errors by logging and presenting failure', async () => {
|
||||||
|
avatarRepo.findActiveByDriverId.mockRejectedValue(new Error('DB error'));
|
||||||
|
|
||||||
|
await useCase.execute({ driverId: 'driver-1' }, presenter);
|
||||||
|
|
||||||
|
expect((logger.error as unknown as Mock)).toHaveBeenCalled();
|
||||||
|
expect((presenter.present as unknown as Mock)).toHaveBeenCalledWith({
|
||||||
|
success: false,
|
||||||
|
errorMessage: 'Internal error occurred while retrieving avatar',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
102
core/media/application/use-cases/GetMediaUseCase.test.ts
Normal file
102
core/media/application/use-cases/GetMediaUseCase.test.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||||
|
import { GetMediaUseCase } from './GetMediaUseCase';
|
||||||
|
import type { IMediaRepository } from '../../domain/repositories/IMediaRepository';
|
||||||
|
import type { IGetMediaPresenter } from '../presenters/IGetMediaPresenter';
|
||||||
|
import type { Logger } from '@core/shared/application';
|
||||||
|
import { Media } from '../../domain/entities/Media';
|
||||||
|
import { MediaUrl } from '../../domain/value-objects/MediaUrl';
|
||||||
|
|
||||||
|
interface TestPresenter extends IGetMediaPresenter {
|
||||||
|
result?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('GetMediaUseCase', () => {
|
||||||
|
let mediaRepo: {
|
||||||
|
findById: Mock;
|
||||||
|
};
|
||||||
|
let logger: Logger;
|
||||||
|
let presenter: TestPresenter;
|
||||||
|
let useCase: GetMediaUseCase;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mediaRepo = {
|
||||||
|
findById: vi.fn(),
|
||||||
|
} as unknown as IMediaRepository as any;
|
||||||
|
|
||||||
|
logger = {
|
||||||
|
debug: vi.fn(),
|
||||||
|
info: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
} as unknown as Logger;
|
||||||
|
|
||||||
|
presenter = {
|
||||||
|
present: vi.fn((result) => {
|
||||||
|
presenter.result = result;
|
||||||
|
}),
|
||||||
|
} as unknown as TestPresenter;
|
||||||
|
|
||||||
|
useCase = new GetMediaUseCase(
|
||||||
|
mediaRepo as unknown as IMediaRepository,
|
||||||
|
logger,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('presents error when media is not found', async () => {
|
||||||
|
mediaRepo.findById.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await useCase.execute({ mediaId: 'missing' }, presenter);
|
||||||
|
|
||||||
|
expect(mediaRepo.findById).toHaveBeenCalledWith('missing');
|
||||||
|
expect((presenter.present as unknown as Mock)).toHaveBeenCalledWith({
|
||||||
|
success: false,
|
||||||
|
errorMessage: 'Media not found',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('presents media details when media exists', async () => {
|
||||||
|
const media = Media.create({
|
||||||
|
id: 'media-1',
|
||||||
|
filename: 'file.png',
|
||||||
|
originalName: 'file.png',
|
||||||
|
mimeType: 'image/png',
|
||||||
|
size: 123,
|
||||||
|
url: MediaUrl.create('https://example.com/file.png'),
|
||||||
|
type: 'image',
|
||||||
|
uploadedBy: 'user-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
mediaRepo.findById.mockResolvedValue(media);
|
||||||
|
|
||||||
|
await useCase.execute({ mediaId: 'media-1' }, presenter);
|
||||||
|
|
||||||
|
expect(mediaRepo.findById).toHaveBeenCalledWith('media-1');
|
||||||
|
expect((presenter.present as unknown as Mock)).toHaveBeenCalledWith({
|
||||||
|
success: true,
|
||||||
|
media: {
|
||||||
|
id: media.id,
|
||||||
|
filename: media.filename,
|
||||||
|
originalName: media.originalName,
|
||||||
|
mimeType: media.mimeType,
|
||||||
|
size: media.size,
|
||||||
|
url: media.url.value,
|
||||||
|
type: media.type,
|
||||||
|
uploadedBy: media.uploadedBy,
|
||||||
|
uploadedAt: media.uploadedAt,
|
||||||
|
metadata: media.metadata,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles errors by logging and presenting failure', async () => {
|
||||||
|
mediaRepo.findById.mockRejectedValue(new Error('DB error'));
|
||||||
|
|
||||||
|
await useCase.execute({ mediaId: 'media-1' }, presenter);
|
||||||
|
|
||||||
|
expect((logger.error as unknown as Mock)).toHaveBeenCalled();
|
||||||
|
expect((presenter.present as unknown as Mock)).toHaveBeenCalledWith({
|
||||||
|
success: false,
|
||||||
|
errorMessage: 'Internal error occurred while retrieving media',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||||
|
import { RequestAvatarGenerationUseCase } from './RequestAvatarGenerationUseCase';
|
||||||
|
import type { IAvatarGenerationRepository } from '../../domain/repositories/IAvatarGenerationRepository';
|
||||||
|
import type { FaceValidationPort } from '../ports/FaceValidationPort';
|
||||||
|
import type { AvatarGenerationPort } from '../ports/AvatarGenerationPort';
|
||||||
|
import type { Logger } from '@core/shared/application';
|
||||||
|
import { AvatarGenerationRequest } from '../../domain/entities/AvatarGenerationRequest';
|
||||||
|
import type { RequestAvatarGenerationInput } from './RequestAvatarGenerationUseCase';
|
||||||
|
|
||||||
|
describe('RequestAvatarGenerationUseCase', () => {
|
||||||
|
let avatarRepo: {
|
||||||
|
save: Mock;
|
||||||
|
findById: Mock;
|
||||||
|
};
|
||||||
|
let faceValidation: {
|
||||||
|
validateFacePhoto: Mock;
|
||||||
|
};
|
||||||
|
let avatarGeneration: {
|
||||||
|
generateAvatars: Mock;
|
||||||
|
};
|
||||||
|
let logger: Logger;
|
||||||
|
let useCase: RequestAvatarGenerationUseCase;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
avatarRepo = {
|
||||||
|
save: vi.fn(),
|
||||||
|
findById: vi.fn(),
|
||||||
|
} as unknown as IAvatarGenerationRepository as any;
|
||||||
|
|
||||||
|
faceValidation = {
|
||||||
|
validateFacePhoto: vi.fn(),
|
||||||
|
} as unknown as FaceValidationPort as any;
|
||||||
|
|
||||||
|
avatarGeneration = {
|
||||||
|
generateAvatars: vi.fn(),
|
||||||
|
} as unknown as AvatarGenerationPort as any;
|
||||||
|
|
||||||
|
logger = {
|
||||||
|
debug: vi.fn(),
|
||||||
|
info: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
} as unknown as Logger;
|
||||||
|
|
||||||
|
useCase = new RequestAvatarGenerationUseCase(
|
||||||
|
avatarRepo as unknown as IAvatarGenerationRepository,
|
||||||
|
faceValidation as unknown as FaceValidationPort,
|
||||||
|
avatarGeneration as unknown as AvatarGenerationPort,
|
||||||
|
logger,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const createPresenter = () => {
|
||||||
|
const presenter: { present: Mock; result?: any } = {
|
||||||
|
present: vi.fn((result) => {
|
||||||
|
presenter.result = result;
|
||||||
|
}),
|
||||||
|
result: undefined,
|
||||||
|
};
|
||||||
|
return presenter;
|
||||||
|
};
|
||||||
|
|
||||||
|
it('fails when face validation fails', async () => {
|
||||||
|
const input: RequestAvatarGenerationInput = {
|
||||||
|
userId: 'user-1',
|
||||||
|
facePhotoData: 'photo-data',
|
||||||
|
suitColor: 'red',
|
||||||
|
style: 'realistic',
|
||||||
|
};
|
||||||
|
|
||||||
|
const presenter = createPresenter();
|
||||||
|
|
||||||
|
faceValidation.validateFacePhoto.mockResolvedValue({
|
||||||
|
isValid: false,
|
||||||
|
hasFace: false,
|
||||||
|
faceCount: 0,
|
||||||
|
errorMessage: 'No face detected',
|
||||||
|
});
|
||||||
|
|
||||||
|
await useCase.execute(input, presenter as any);
|
||||||
|
|
||||||
|
expect((presenter.present as Mock)).toHaveBeenCalledWith({
|
||||||
|
requestId: expect.any(String),
|
||||||
|
status: 'failed',
|
||||||
|
errorMessage: 'No face detected',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('completes request and returns avatar URLs on success', async () => {
|
||||||
|
const input: RequestAvatarGenerationInput = {
|
||||||
|
userId: 'user-1',
|
||||||
|
facePhotoData: 'photo-data',
|
||||||
|
suitColor: 'red',
|
||||||
|
style: 'realistic',
|
||||||
|
};
|
||||||
|
|
||||||
|
const presenter = createPresenter();
|
||||||
|
|
||||||
|
faceValidation.validateFacePhoto.mockResolvedValue({
|
||||||
|
isValid: true,
|
||||||
|
hasFace: true,
|
||||||
|
faceCount: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
avatarGeneration.generateAvatars.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
avatars: [
|
||||||
|
{ url: 'https://example.com/avatar1.png' },
|
||||||
|
{ url: 'https://example.com/avatar2.png' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await useCase.execute(input, presenter as any);
|
||||||
|
|
||||||
|
expect(faceValidation.validateFacePhoto).toHaveBeenCalled();
|
||||||
|
expect(avatarGeneration.generateAvatars).toHaveBeenCalled();
|
||||||
|
expect((presenter.present as Mock)).toHaveBeenCalledWith({
|
||||||
|
requestId: expect.any(String),
|
||||||
|
status: 'completed',
|
||||||
|
avatarUrls: [
|
||||||
|
'https://example.com/avatar1.png',
|
||||||
|
'https://example.com/avatar2.png',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
76
core/media/application/use-cases/SelectAvatarUseCase.test.ts
Normal file
76
core/media/application/use-cases/SelectAvatarUseCase.test.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||||
|
import { SelectAvatarUseCase } from './SelectAvatarUseCase';
|
||||||
|
import type { IAvatarGenerationRepository } from '../../domain/repositories/IAvatarGenerationRepository';
|
||||||
|
import type { ISelectAvatarPresenter } from '../presenters/ISelectAvatarPresenter';
|
||||||
|
import type { Logger } from '@core/shared/application';
|
||||||
|
import { AvatarGenerationRequest } from '../../domain/entities/AvatarGenerationRequest';
|
||||||
|
|
||||||
|
interface TestPresenter extends ISelectAvatarPresenter {
|
||||||
|
result?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('SelectAvatarUseCase', () => {
|
||||||
|
let avatarRepo: {
|
||||||
|
findById: Mock;
|
||||||
|
save: Mock;
|
||||||
|
};
|
||||||
|
let logger: Logger;
|
||||||
|
let presenter: TestPresenter;
|
||||||
|
let useCase: SelectAvatarUseCase;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
avatarRepo = {
|
||||||
|
findById: vi.fn(),
|
||||||
|
save: vi.fn(),
|
||||||
|
} as unknown as IAvatarGenerationRepository as any;
|
||||||
|
|
||||||
|
logger = {
|
||||||
|
debug: vi.fn(),
|
||||||
|
info: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
} as unknown as Logger;
|
||||||
|
|
||||||
|
presenter = {
|
||||||
|
present: vi.fn((result) => {
|
||||||
|
presenter.result = result;
|
||||||
|
}),
|
||||||
|
} as unknown as TestPresenter;
|
||||||
|
|
||||||
|
useCase = new SelectAvatarUseCase(
|
||||||
|
avatarRepo as unknown as IAvatarGenerationRepository,
|
||||||
|
logger,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns error when request is not found', async () => {
|
||||||
|
avatarRepo.findById.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await useCase.execute({ requestId: 'req-1', selectedIndex: 0 }, presenter);
|
||||||
|
|
||||||
|
expect(avatarRepo.findById).toHaveBeenCalledWith('req-1');
|
||||||
|
expect((presenter.present as unknown as Mock)).toHaveBeenCalledWith({
|
||||||
|
success: false,
|
||||||
|
errorMessage: 'Avatar generation request not found',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns error when request is not completed', async () => {
|
||||||
|
const request = AvatarGenerationRequest.create({
|
||||||
|
id: 'req-1',
|
||||||
|
userId: 'user-1',
|
||||||
|
facePhotoUrl: 'photo',
|
||||||
|
suitColor: 'red',
|
||||||
|
style: 'realistic',
|
||||||
|
});
|
||||||
|
|
||||||
|
avatarRepo.findById.mockResolvedValue(request);
|
||||||
|
|
||||||
|
await useCase.execute({ requestId: 'req-1', selectedIndex: 0 }, presenter);
|
||||||
|
|
||||||
|
expect((presenter.present as unknown as Mock)).toHaveBeenCalledWith({
|
||||||
|
success: false,
|
||||||
|
errorMessage: 'Avatar generation is not completed yet',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||||
|
import { GetUnreadNotificationsUseCase } from './GetUnreadNotificationsUseCase';
|
||||||
|
import type { INotificationRepository } from '../../domain/repositories/INotificationRepository';
|
||||||
|
import type { Logger } from '@core/shared/application';
|
||||||
|
import { Notification } from '../../domain/entities/Notification';
|
||||||
|
|
||||||
|
interface NotificationRepositoryMock {
|
||||||
|
findUnreadByRecipientId: Mock;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('GetUnreadNotificationsUseCase', () => {
|
||||||
|
let notificationRepository: NotificationRepositoryMock;
|
||||||
|
let logger: Logger;
|
||||||
|
let useCase: GetUnreadNotificationsUseCase;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
notificationRepository = {
|
||||||
|
findUnreadByRecipientId: vi.fn(),
|
||||||
|
} as unknown as INotificationRepository as any;
|
||||||
|
|
||||||
|
logger = {
|
||||||
|
debug: vi.fn(),
|
||||||
|
info: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
} as unknown as Logger;
|
||||||
|
|
||||||
|
useCase = new GetUnreadNotificationsUseCase(
|
||||||
|
notificationRepository as unknown as INotificationRepository,
|
||||||
|
logger,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns unread notifications and total count', async () => {
|
||||||
|
const recipientId = 'driver-1';
|
||||||
|
const notifications: Notification[] = [
|
||||||
|
Notification.create({
|
||||||
|
id: 'n1',
|
||||||
|
recipientId,
|
||||||
|
type: 'info',
|
||||||
|
title: 'Test',
|
||||||
|
body: 'Body',
|
||||||
|
channel: 'in_app',
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
notificationRepository.findUnreadByRecipientId.mockResolvedValue(notifications);
|
||||||
|
|
||||||
|
const result = await useCase.execute(recipientId);
|
||||||
|
|
||||||
|
expect(notificationRepository.findUnreadByRecipientId).toHaveBeenCalledWith(recipientId);
|
||||||
|
expect(result.notifications).toEqual(notifications);
|
||||||
|
expect(result.totalCount).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles repository errors by logging and rethrowing', async () => {
|
||||||
|
const recipientId = 'driver-1';
|
||||||
|
const error = new Error('DB error');
|
||||||
|
notificationRepository.findUnreadByRecipientId.mockRejectedValue(error);
|
||||||
|
|
||||||
|
await expect(useCase.execute(recipientId)).rejects.toThrow('DB error');
|
||||||
|
expect((logger.error as unknown as Mock)).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,8 +4,7 @@
|
|||||||
* Retrieves unread notifications for a recipient.
|
* Retrieves unread notifications for a recipient.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { AsyncUseCase } from '@core/shared/application';
|
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||||
import type { Logger } from '@core/shared/application';
|
|
||||||
import type { Notification } from '../../domain/entities/Notification';
|
import type { Notification } from '../../domain/entities/Notification';
|
||||||
import type { INotificationRepository } from '../../domain/repositories/INotificationRepository';
|
import type { INotificationRepository } from '../../domain/repositories/INotificationRepository';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||||
|
import { MarkNotificationReadUseCase } from './MarkNotificationReadUseCase';
|
||||||
|
import type { INotificationRepository } from '../../domain/repositories/INotificationRepository';
|
||||||
|
import type { Logger } from '@core/shared/application';
|
||||||
|
import { Notification } from '../../domain/entities/Notification';
|
||||||
|
import { NotificationDomainError } from '../../domain/errors/NotificationDomainError';
|
||||||
|
|
||||||
|
interface NotificationRepositoryMock {
|
||||||
|
findById: Mock;
|
||||||
|
update: Mock;
|
||||||
|
markAllAsReadByRecipientId: Mock;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('MarkNotificationReadUseCase', () => {
|
||||||
|
let notificationRepository: NotificationRepositoryMock;
|
||||||
|
let logger: Logger;
|
||||||
|
let useCase: MarkNotificationReadUseCase;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
notificationRepository = {
|
||||||
|
findById: vi.fn(),
|
||||||
|
update: vi.fn(),
|
||||||
|
markAllAsReadByRecipientId: vi.fn(),
|
||||||
|
} as unknown as INotificationRepository as any;
|
||||||
|
|
||||||
|
logger = {
|
||||||
|
debug: vi.fn(),
|
||||||
|
info: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
} as unknown as Logger;
|
||||||
|
|
||||||
|
useCase = new MarkNotificationReadUseCase(
|
||||||
|
notificationRepository as unknown as INotificationRepository,
|
||||||
|
logger,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when notification is not found', async () => {
|
||||||
|
notificationRepository.findById.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
useCase.execute({ notificationId: 'n1', recipientId: 'driver-1' }),
|
||||||
|
).rejects.toThrow(NotificationDomainError);
|
||||||
|
|
||||||
|
expect((logger.warn as unknown as Mock)).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when recipientId does not match', async () => {
|
||||||
|
const notification = Notification.create({
|
||||||
|
id: 'n1',
|
||||||
|
recipientId: 'driver-2',
|
||||||
|
type: 'info',
|
||||||
|
title: 'Test',
|
||||||
|
body: 'Body',
|
||||||
|
channel: 'in_app',
|
||||||
|
});
|
||||||
|
|
||||||
|
notificationRepository.findById.mockResolvedValue(notification);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
useCase.execute({ notificationId: 'n1', recipientId: 'driver-1' }),
|
||||||
|
).rejects.toThrow(NotificationDomainError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('marks notification as read when unread', async () => {
|
||||||
|
const notification = Notification.create({
|
||||||
|
id: 'n1',
|
||||||
|
recipientId: 'driver-1',
|
||||||
|
type: 'info',
|
||||||
|
title: 'Test',
|
||||||
|
body: 'Body',
|
||||||
|
channel: 'in_app',
|
||||||
|
});
|
||||||
|
|
||||||
|
notificationRepository.findById.mockResolvedValue(notification);
|
||||||
|
|
||||||
|
await useCase.execute({ notificationId: 'n1', recipientId: 'driver-1' });
|
||||||
|
|
||||||
|
expect(notificationRepository.update).toHaveBeenCalled();
|
||||||
|
expect((logger.info as unknown as Mock)).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,10 +4,9 @@
|
|||||||
* Marks a notification as read.
|
* Marks a notification as read.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { AsyncUseCase } from '@core/shared/application';
|
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||||
import type { INotificationRepository } from '../../domain/repositories/INotificationRepository';
|
import type { INotificationRepository } from '../../domain/repositories/INotificationRepository';
|
||||||
import { NotificationDomainError } from '../../domain/errors/NotificationDomainError';
|
import { NotificationDomainError } from '../../domain/errors/NotificationDomainError';
|
||||||
import type { Logger } from '@core/shared/application';
|
|
||||||
|
|
||||||
export interface MarkNotificationReadCommand {
|
export interface MarkNotificationReadCommand {
|
||||||
notificationId: string;
|
notificationId: string;
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||||
|
import {
|
||||||
|
GetNotificationPreferencesQuery,
|
||||||
|
UpdateChannelPreferenceUseCase,
|
||||||
|
UpdateTypePreferenceUseCase,
|
||||||
|
UpdateQuietHoursUseCase,
|
||||||
|
SetDigestModeUseCase,
|
||||||
|
} from './NotificationPreferencesUseCases';
|
||||||
|
import type { INotificationPreferenceRepository } from '../../domain/repositories/INotificationPreferenceRepository';
|
||||||
|
import type { NotificationPreference , ChannelPreference, TypePreference } from '../../domain/entities/NotificationPreference';
|
||||||
|
import type { Logger } from '@core/shared/application';
|
||||||
|
import type { NotificationChannel, NotificationType } from '../../domain/types/NotificationTypes';
|
||||||
|
import { NotificationDomainError } from '../../domain/errors/NotificationDomainError';
|
||||||
|
|
||||||
|
describe('NotificationPreferencesUseCases', () => {
|
||||||
|
let preferenceRepository: {
|
||||||
|
getOrCreateDefault: Mock;
|
||||||
|
save: Mock;
|
||||||
|
};
|
||||||
|
let logger: Logger;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
preferenceRepository = {
|
||||||
|
getOrCreateDefault: vi.fn(),
|
||||||
|
save: vi.fn(),
|
||||||
|
} as unknown as INotificationPreferenceRepository as any;
|
||||||
|
|
||||||
|
logger = {
|
||||||
|
debug: vi.fn(),
|
||||||
|
info: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
} as unknown as Logger;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GetNotificationPreferencesQuery returns preferences from repository', async () => {
|
||||||
|
const preference = {
|
||||||
|
id: 'pref-1',
|
||||||
|
} as unknown as NotificationPreference;
|
||||||
|
|
||||||
|
preferenceRepository.getOrCreateDefault.mockResolvedValue(preference);
|
||||||
|
|
||||||
|
const useCase = new GetNotificationPreferencesQuery(
|
||||||
|
preferenceRepository as unknown as INotificationPreferenceRepository,
|
||||||
|
logger,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await useCase.execute('driver-1');
|
||||||
|
|
||||||
|
expect(preferenceRepository.getOrCreateDefault).toHaveBeenCalledWith('driver-1');
|
||||||
|
expect(result).toBe(preference);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('UpdateChannelPreferenceUseCase updates channel preference', async () => {
|
||||||
|
const preference = {
|
||||||
|
updateChannel: vi.fn().mockReturnThis(),
|
||||||
|
} as unknown as NotificationPreference;
|
||||||
|
|
||||||
|
preferenceRepository.getOrCreateDefault.mockResolvedValue(preference);
|
||||||
|
|
||||||
|
const useCase = new UpdateChannelPreferenceUseCase(
|
||||||
|
preferenceRepository as unknown as INotificationPreferenceRepository,
|
||||||
|
logger,
|
||||||
|
);
|
||||||
|
|
||||||
|
await useCase.execute({
|
||||||
|
driverId: 'driver-1',
|
||||||
|
channel: 'email' as NotificationChannel,
|
||||||
|
preference: 'enabled' as ChannelPreference,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(preference.updateChannel).toHaveBeenCalled();
|
||||||
|
expect(preferenceRepository.save).toHaveBeenCalledWith(preference);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('UpdateTypePreferenceUseCase updates type preference', async () => {
|
||||||
|
const preference = {
|
||||||
|
updateTypePreference: vi.fn().mockReturnThis(),
|
||||||
|
} as unknown as NotificationPreference;
|
||||||
|
|
||||||
|
preferenceRepository.getOrCreateDefault.mockResolvedValue(preference);
|
||||||
|
|
||||||
|
const useCase = new UpdateTypePreferenceUseCase(
|
||||||
|
preferenceRepository as unknown as INotificationPreferenceRepository,
|
||||||
|
logger,
|
||||||
|
);
|
||||||
|
|
||||||
|
await useCase.execute({
|
||||||
|
driverId: 'driver-1',
|
||||||
|
type: 'info' as NotificationType,
|
||||||
|
preference: 'enabled' as TypePreference,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(preference.updateTypePreference).toHaveBeenCalled();
|
||||||
|
expect(preferenceRepository.save).toHaveBeenCalledWith(preference);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('UpdateQuietHoursUseCase validates hours and updates preferences', async () => {
|
||||||
|
const preference = {
|
||||||
|
updateQuietHours: vi.fn().mockReturnThis(),
|
||||||
|
} as unknown as NotificationPreference;
|
||||||
|
|
||||||
|
preferenceRepository.getOrCreateDefault.mockResolvedValue(preference);
|
||||||
|
|
||||||
|
const useCase = new UpdateQuietHoursUseCase(
|
||||||
|
preferenceRepository as unknown as INotificationPreferenceRepository,
|
||||||
|
logger,
|
||||||
|
);
|
||||||
|
|
||||||
|
await useCase.execute({
|
||||||
|
driverId: 'driver-1',
|
||||||
|
startHour: 22,
|
||||||
|
endHour: 7,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(preference.updateQuietHours).toHaveBeenCalledWith(22, 7);
|
||||||
|
expect(preferenceRepository.save).toHaveBeenCalledWith(preference);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('UpdateQuietHoursUseCase throws on invalid hours', async () => {
|
||||||
|
const useCase = new UpdateQuietHoursUseCase(
|
||||||
|
preferenceRepository as unknown as INotificationPreferenceRepository,
|
||||||
|
logger,
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
useCase.execute({ driverId: 'd1', startHour: -1, endHour: 10 }),
|
||||||
|
).rejects.toThrow(NotificationDomainError);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
useCase.execute({ driverId: 'd1', startHour: 10, endHour: 24 }),
|
||||||
|
).rejects.toThrow(NotificationDomainError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SetDigestModeUseCase sets digest mode with valid frequency', async () => {
|
||||||
|
const preference = {
|
||||||
|
setDigestMode: vi.fn().mockReturnThis(),
|
||||||
|
} as unknown as NotificationPreference;
|
||||||
|
|
||||||
|
preferenceRepository.getOrCreateDefault.mockResolvedValue(preference);
|
||||||
|
|
||||||
|
const useCase = new SetDigestModeUseCase(
|
||||||
|
preferenceRepository as unknown as INotificationPreferenceRepository,
|
||||||
|
);
|
||||||
|
|
||||||
|
await useCase.execute({
|
||||||
|
driverId: 'driver-1',
|
||||||
|
enabled: true,
|
||||||
|
frequencyHours: 4,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(preference.setDigestMode).toHaveBeenCalledWith(true, 4);
|
||||||
|
expect(preferenceRepository.save).toHaveBeenCalledWith(preference);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SetDigestModeUseCase throws on invalid frequency', async () => {
|
||||||
|
const useCase = new SetDigestModeUseCase(
|
||||||
|
preferenceRepository as unknown as INotificationPreferenceRepository,
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
useCase.execute({ driverId: 'driver-1', enabled: true, frequencyHours: 0 }),
|
||||||
|
).rejects.toThrow(NotificationDomainError);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,8 +4,7 @@
|
|||||||
* Manages user notification preferences.
|
* Manages user notification preferences.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { AsyncUseCase } from '@core/shared/application';
|
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||||
import type { Logger } from '@core/shared/application';
|
|
||||||
import { NotificationPreference } from '../../domain/entities/NotificationPreference';
|
import { NotificationPreference } from '../../domain/entities/NotificationPreference';
|
||||||
import type { ChannelPreference, TypePreference } from '../../domain/entities/NotificationPreference';
|
import type { ChannelPreference, TypePreference } from '../../domain/entities/NotificationPreference';
|
||||||
import type { INotificationPreferenceRepository } from '../../domain/repositories/INotificationPreferenceRepository';
|
import type { INotificationPreferenceRepository } from '../../domain/repositories/INotificationPreferenceRepository';
|
||||||
|
|||||||
@@ -7,5 +7,5 @@ export { InMemoryNotificationRepository } from './repositories/InMemoryNotificat
|
|||||||
export { InMemoryNotificationPreferenceRepository } from './repositories/InMemoryNotificationPreferenceRepository';
|
export { InMemoryNotificationPreferenceRepository } from './repositories/InMemoryNotificationPreferenceRepository';
|
||||||
|
|
||||||
// Adapters
|
// Adapters
|
||||||
export { InAppNotificationAdapter } from './/InAppNotificationAdapter';
|
export { InAppNotificationAdapter } from "./InAppNotificationAdapter";
|
||||||
export { NotificationGatewayRegistry } from './/NotificationGatewayRegistry';
|
export { NotificationGatewayRegistry } from "./NotificationGatewayRegistry";
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||||
|
import { GetMembershipFeesUseCase, type GetMembershipFeesInput } from './GetMembershipFeesUseCase';
|
||||||
|
import type { IMembershipFeeRepository, IMemberPaymentRepository } from '../../domain/repositories/IMembershipFeeRepository';
|
||||||
|
import type { IGetMembershipFeesPresenter, GetMembershipFeesResultDTO, GetMembershipFeesViewModel } from '../presenters/IGetMembershipFeesPresenter';
|
||||||
|
|
||||||
|
interface TestPresenter extends IGetMembershipFeesPresenter {
|
||||||
|
reset: Mock;
|
||||||
|
present: Mock;
|
||||||
|
lastDto?: GetMembershipFeesResultDTO;
|
||||||
|
viewModel?: GetMembershipFeesViewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('GetMembershipFeesUseCase', () => {
|
||||||
|
let membershipFeeRepository: {
|
||||||
|
findByLeagueId: Mock;
|
||||||
|
};
|
||||||
|
let memberPaymentRepository: {
|
||||||
|
findByLeagueIdAndDriverId: Mock;
|
||||||
|
};
|
||||||
|
let presenter: TestPresenter;
|
||||||
|
let useCase: GetMembershipFeesUseCase;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
membershipFeeRepository = {
|
||||||
|
findByLeagueId: vi.fn(),
|
||||||
|
} as unknown as IMembershipFeeRepository as any;
|
||||||
|
|
||||||
|
memberPaymentRepository = {
|
||||||
|
findByLeagueIdAndDriverId: vi.fn(),
|
||||||
|
} as unknown as IMemberPaymentRepository as any;
|
||||||
|
|
||||||
|
presenter = {
|
||||||
|
reset: vi.fn(),
|
||||||
|
present: vi.fn((dto: GetMembershipFeesResultDTO) => {
|
||||||
|
presenter.lastDto = dto;
|
||||||
|
}),
|
||||||
|
toViewModel: vi.fn((dto: GetMembershipFeesResultDTO) => ({
|
||||||
|
fee: dto.fee,
|
||||||
|
payments: dto.payments,
|
||||||
|
})),
|
||||||
|
} as unknown as TestPresenter;
|
||||||
|
|
||||||
|
useCase = new GetMembershipFeesUseCase(
|
||||||
|
membershipFeeRepository as unknown as IMembershipFeeRepository,
|
||||||
|
memberPaymentRepository as unknown as IMemberPaymentRepository,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when leagueId is missing', async () => {
|
||||||
|
const input = { leagueId: '' } as GetMembershipFeesInput;
|
||||||
|
|
||||||
|
await expect(useCase.execute(input, presenter)).rejects.toThrow('leagueId is required');
|
||||||
|
expect(presenter.reset).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null fee and empty payments when no fee exists', async () => {
|
||||||
|
const input: GetMembershipFeesInput = { leagueId: 'league-1' };
|
||||||
|
|
||||||
|
membershipFeeRepository.findByLeagueId.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await useCase.execute(input, presenter);
|
||||||
|
|
||||||
|
expect(membershipFeeRepository.findByLeagueId).toHaveBeenCalledWith('league-1');
|
||||||
|
expect(memberPaymentRepository.findByLeagueIdAndDriverId).not.toHaveBeenCalled();
|
||||||
|
expect(presenter.present).toHaveBeenCalledWith({
|
||||||
|
fee: null,
|
||||||
|
payments: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps fee and payments when fee and driverId are provided', async () => {
|
||||||
|
const input: GetMembershipFeesInput = { leagueId: 'league-1', driverId: 'driver-1' };
|
||||||
|
|
||||||
|
const fee = {
|
||||||
|
id: 'fee-1',
|
||||||
|
leagueId: 'league-1',
|
||||||
|
seasonId: 'season-1',
|
||||||
|
type: 'season',
|
||||||
|
amount: 100,
|
||||||
|
enabled: true,
|
||||||
|
createdAt: new Date('2024-01-01'),
|
||||||
|
updatedAt: new Date('2024-01-02'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const payments = [
|
||||||
|
{
|
||||||
|
id: 'pay-1',
|
||||||
|
feeId: 'fee-1',
|
||||||
|
driverId: 'driver-1',
|
||||||
|
amount: 100,
|
||||||
|
platformFee: 5,
|
||||||
|
netAmount: 95,
|
||||||
|
status: 'paid',
|
||||||
|
dueDate: new Date('2024-02-01'),
|
||||||
|
paidAt: new Date('2024-01-15'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
membershipFeeRepository.findByLeagueId.mockResolvedValue(fee);
|
||||||
|
memberPaymentRepository.findByLeagueIdAndDriverId.mockResolvedValue(payments);
|
||||||
|
|
||||||
|
await useCase.execute(input, presenter);
|
||||||
|
|
||||||
|
expect(membershipFeeRepository.findByLeagueId).toHaveBeenCalledWith('league-1');
|
||||||
|
expect(memberPaymentRepository.findByLeagueIdAndDriverId).toHaveBeenCalledWith('league-1', 'driver-1', membershipFeeRepository as unknown as IMembershipFeeRepository);
|
||||||
|
|
||||||
|
expect(presenter.present).toHaveBeenCalledWith({
|
||||||
|
fee: {
|
||||||
|
id: fee.id,
|
||||||
|
leagueId: fee.leagueId,
|
||||||
|
seasonId: fee.seasonId,
|
||||||
|
type: fee.type,
|
||||||
|
amount: fee.amount,
|
||||||
|
enabled: fee.enabled,
|
||||||
|
createdAt: fee.createdAt,
|
||||||
|
updatedAt: fee.updatedAt,
|
||||||
|
},
|
||||||
|
payments: [
|
||||||
|
{
|
||||||
|
id: 'pay-1',
|
||||||
|
feeId: 'fee-1',
|
||||||
|
driverId: 'driver-1',
|
||||||
|
amount: 100,
|
||||||
|
platformFee: 5,
|
||||||
|
netAmount: 95,
|
||||||
|
status: 'paid',
|
||||||
|
dueDate: payments[0].dueDate,
|
||||||
|
paidAt: payments[0].paidAt,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
374
core/racing/application/services/SeasonApplicationService.ts
Normal file
374
core/racing/application/services/SeasonApplicationService.ts
Normal file
@@ -0,0 +1,374 @@
|
|||||||
|
import { Season } from '../../domain/entities/Season';
|
||||||
|
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||||
|
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||||
|
import type { LeagueConfigFormModel } from '../dto/LeagueConfigFormDTO';
|
||||||
|
import { SeasonSchedule } from '../../domain/value-objects/SeasonSchedule';
|
||||||
|
import { SeasonScoringConfig } from '../../domain/value-objects/SeasonScoringConfig';
|
||||||
|
import { SeasonDropPolicy } from '../../domain/value-objects/SeasonDropPolicy';
|
||||||
|
import { SeasonStewardingConfig } from '../../domain/value-objects/SeasonStewardingConfig';
|
||||||
|
import { RaceTimeOfDay } from '../../domain/value-objects/RaceTimeOfDay';
|
||||||
|
import { LeagueTimezone } from '../../domain/value-objects/LeagueTimezone';
|
||||||
|
import { RecurrenceStrategyFactory } from '../../domain/value-objects/RecurrenceStrategy';
|
||||||
|
import { WeekdaySet } from '../../domain/value-objects/WeekdaySet';
|
||||||
|
import { MonthlyRecurrencePattern } from '../../domain/value-objects/MonthlyRecurrencePattern';
|
||||||
|
import type { Weekday } from '../../domain/types/Weekday';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
export interface CreateSeasonForLeagueCommand {
|
||||||
|
leagueId: string;
|
||||||
|
name: string;
|
||||||
|
gameId: string;
|
||||||
|
sourceSeasonId?: string;
|
||||||
|
config?: LeagueConfigFormModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateSeasonForLeagueResultDTO {
|
||||||
|
seasonId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SeasonSummaryDTO {
|
||||||
|
seasonId: string;
|
||||||
|
leagueId: string;
|
||||||
|
name: string;
|
||||||
|
status: import('../../domain/entities/Season').SeasonStatus;
|
||||||
|
startDate?: Date;
|
||||||
|
endDate?: Date;
|
||||||
|
isPrimary: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListSeasonsForLeagueQuery {
|
||||||
|
leagueId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListSeasonsForLeagueResultDTO {
|
||||||
|
items: SeasonSummaryDTO[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetSeasonDetailsQuery {
|
||||||
|
leagueId: string;
|
||||||
|
seasonId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SeasonDetailsDTO {
|
||||||
|
seasonId: string;
|
||||||
|
leagueId: string;
|
||||||
|
gameId: string;
|
||||||
|
name: string;
|
||||||
|
status: import('../../domain/entities/Season').SeasonStatus;
|
||||||
|
startDate?: Date;
|
||||||
|
endDate?: Date;
|
||||||
|
maxDrivers?: number;
|
||||||
|
schedule?: {
|
||||||
|
startDate: Date;
|
||||||
|
plannedRounds: number;
|
||||||
|
};
|
||||||
|
scoring?: {
|
||||||
|
scoringPresetId: string;
|
||||||
|
customScoringEnabled: boolean;
|
||||||
|
};
|
||||||
|
dropPolicy?: {
|
||||||
|
strategy: import('../../domain/value-objects/SeasonDropPolicy').SeasonDropStrategy;
|
||||||
|
n?: number;
|
||||||
|
};
|
||||||
|
stewarding?: {
|
||||||
|
decisionMode: import('../../domain/entities/League').StewardingDecisionMode;
|
||||||
|
requiredVotes?: number;
|
||||||
|
requireDefense: boolean;
|
||||||
|
defenseTimeLimit: number;
|
||||||
|
voteTimeLimit: number;
|
||||||
|
protestDeadlineHours: number;
|
||||||
|
stewardingClosesHours: number;
|
||||||
|
notifyAccusedOnProtest: boolean;
|
||||||
|
notifyOnVoteRequired: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SeasonLifecycleTransition = 'activate' | 'complete' | 'archive' | 'cancel';
|
||||||
|
|
||||||
|
export interface ManageSeasonLifecycleCommand {
|
||||||
|
leagueId: string;
|
||||||
|
seasonId: string;
|
||||||
|
transition: SeasonLifecycleTransition;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ManageSeasonLifecycleResultDTO {
|
||||||
|
seasonId: string;
|
||||||
|
status: import('../../domain/entities/Season').SeasonStatus;
|
||||||
|
startDate?: Date;
|
||||||
|
endDate?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SeasonApplicationService {
|
||||||
|
constructor(
|
||||||
|
private readonly leagueRepository: ILeagueRepository,
|
||||||
|
private readonly seasonRepository: ISeasonRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async createSeasonForLeague(command: CreateSeasonForLeagueCommand): Promise<CreateSeasonForLeagueResultDTO> {
|
||||||
|
const league = await this.leagueRepository.findById(command.leagueId);
|
||||||
|
if (!league) {
|
||||||
|
throw new Error(`League not found: ${command.leagueId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let baseSeasonProps: {
|
||||||
|
schedule?: SeasonSchedule;
|
||||||
|
scoringConfig?: SeasonScoringConfig;
|
||||||
|
dropPolicy?: SeasonDropPolicy;
|
||||||
|
stewardingConfig?: SeasonStewardingConfig;
|
||||||
|
maxDrivers?: number;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
if (command.sourceSeasonId) {
|
||||||
|
const source = await this.seasonRepository.findById(command.sourceSeasonId);
|
||||||
|
if (!source) {
|
||||||
|
throw new Error(`Source Season not found: ${command.sourceSeasonId}`);
|
||||||
|
}
|
||||||
|
baseSeasonProps = {
|
||||||
|
...(source.schedule !== undefined ? { schedule: source.schedule } : {}),
|
||||||
|
...(source.scoringConfig !== undefined ? { scoringConfig: source.scoringConfig } : {}),
|
||||||
|
...(source.dropPolicy !== undefined ? { dropPolicy: source.dropPolicy } : {}),
|
||||||
|
...(source.stewardingConfig !== undefined ? { stewardingConfig: source.stewardingConfig } : {}),
|
||||||
|
...(source.maxDrivers !== undefined ? { maxDrivers: source.maxDrivers } : {}),
|
||||||
|
};
|
||||||
|
} else if (command.config) {
|
||||||
|
baseSeasonProps = this.deriveSeasonPropsFromConfig(command.config);
|
||||||
|
}
|
||||||
|
|
||||||
|
const seasonId = uuidv4();
|
||||||
|
|
||||||
|
const season = Season.create({
|
||||||
|
id: seasonId,
|
||||||
|
leagueId: league.id,
|
||||||
|
gameId: command.gameId,
|
||||||
|
name: command.name,
|
||||||
|
year: new Date().getFullYear(),
|
||||||
|
status: 'planned',
|
||||||
|
...(baseSeasonProps?.schedule ? { schedule: baseSeasonProps.schedule } : {}),
|
||||||
|
...(baseSeasonProps?.scoringConfig ? { scoringConfig: baseSeasonProps.scoringConfig } : {}),
|
||||||
|
...(baseSeasonProps?.dropPolicy ? { dropPolicy: baseSeasonProps.dropPolicy } : {}),
|
||||||
|
...(baseSeasonProps?.stewardingConfig ? { stewardingConfig: baseSeasonProps.stewardingConfig } : {}),
|
||||||
|
...(baseSeasonProps?.maxDrivers !== undefined ? { maxDrivers: baseSeasonProps.maxDrivers } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.seasonRepository.add(season);
|
||||||
|
|
||||||
|
return { seasonId };
|
||||||
|
}
|
||||||
|
|
||||||
|
async listSeasonsForLeague(query: ListSeasonsForLeagueQuery): Promise<ListSeasonsForLeagueResultDTO> {
|
||||||
|
const league = await this.leagueRepository.findById(query.leagueId);
|
||||||
|
if (!league) {
|
||||||
|
throw new Error(`League not found: ${query.leagueId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const seasons = await this.seasonRepository.listByLeague(league.id);
|
||||||
|
const items: SeasonSummaryDTO[] = seasons.map((s) => ({
|
||||||
|
seasonId: s.id,
|
||||||
|
leagueId: s.leagueId,
|
||||||
|
name: s.name,
|
||||||
|
status: s.status,
|
||||||
|
...(s.startDate !== undefined ? { startDate: s.startDate } : {}),
|
||||||
|
...(s.endDate !== undefined ? { endDate: s.endDate } : {}),
|
||||||
|
isPrimary: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { items };
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSeasonDetails(query: GetSeasonDetailsQuery): Promise<SeasonDetailsDTO> {
|
||||||
|
const league = await this.leagueRepository.findById(query.leagueId);
|
||||||
|
if (!league) {
|
||||||
|
throw new Error(`League not found: ${query.leagueId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const season = await this.seasonRepository.findById(query.seasonId);
|
||||||
|
if (!season || season.leagueId !== league.id) {
|
||||||
|
throw new Error(`Season ${query.seasonId} does not belong to league ${league.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
seasonId: season.id,
|
||||||
|
leagueId: season.leagueId,
|
||||||
|
gameId: season.gameId,
|
||||||
|
name: season.name,
|
||||||
|
status: season.status,
|
||||||
|
...(season.startDate !== undefined ? { startDate: season.startDate } : {}),
|
||||||
|
...(season.endDate !== undefined ? { endDate: season.endDate } : {}),
|
||||||
|
...(season.maxDrivers !== undefined ? { maxDrivers: season.maxDrivers } : {}),
|
||||||
|
...(season.schedule
|
||||||
|
? {
|
||||||
|
schedule: {
|
||||||
|
startDate: season.schedule.startDate,
|
||||||
|
plannedRounds: season.schedule.plannedRounds,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
...(season.scoringConfig
|
||||||
|
? {
|
||||||
|
scoring: {
|
||||||
|
scoringPresetId: season.scoringConfig.scoringPresetId,
|
||||||
|
customScoringEnabled: season.scoringConfig.customScoringEnabled ?? false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
...(season.dropPolicy
|
||||||
|
? {
|
||||||
|
dropPolicy: {
|
||||||
|
strategy: season.dropPolicy.strategy,
|
||||||
|
...(season.dropPolicy.n !== undefined ? { n: season.dropPolicy.n } : {}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
...(season.stewardingConfig
|
||||||
|
? {
|
||||||
|
stewarding: {
|
||||||
|
decisionMode: season.stewardingConfig.decisionMode,
|
||||||
|
...(season.stewardingConfig.requiredVotes !== undefined
|
||||||
|
? { requiredVotes: season.stewardingConfig.requiredVotes }
|
||||||
|
: {}),
|
||||||
|
requireDefense: season.stewardingConfig.requireDefense,
|
||||||
|
defenseTimeLimit: season.stewardingConfig.defenseTimeLimit,
|
||||||
|
voteTimeLimit: season.stewardingConfig.voteTimeLimit,
|
||||||
|
protestDeadlineHours: season.stewardingConfig.protestDeadlineHours,
|
||||||
|
stewardingClosesHours: season.stewardingConfig.stewardingClosesHours,
|
||||||
|
notifyAccusedOnProtest: season.stewardingConfig.notifyAccusedOnProtest,
|
||||||
|
notifyOnVoteRequired: season.stewardingConfig.notifyOnVoteRequired,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async manageSeasonLifecycle(command: ManageSeasonLifecycleCommand): Promise<ManageSeasonLifecycleResultDTO> {
|
||||||
|
const league = await this.leagueRepository.findById(command.leagueId);
|
||||||
|
if (!league) {
|
||||||
|
throw new Error(`League not found: ${command.leagueId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const season = await this.seasonRepository.findById(command.seasonId);
|
||||||
|
if (!season || season.leagueId !== league.id) {
|
||||||
|
throw new Error(`Season ${command.seasonId} does not belong to league ${league.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let updated: Season;
|
||||||
|
switch (command.transition) {
|
||||||
|
case 'activate':
|
||||||
|
updated = season.activate();
|
||||||
|
break;
|
||||||
|
case 'complete':
|
||||||
|
updated = season.complete();
|
||||||
|
break;
|
||||||
|
case 'archive':
|
||||||
|
updated = season.archive();
|
||||||
|
break;
|
||||||
|
case 'cancel':
|
||||||
|
updated = season.cancel();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('Unsupported Season lifecycle transition');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.seasonRepository.update(updated);
|
||||||
|
|
||||||
|
return {
|
||||||
|
seasonId: updated.id,
|
||||||
|
status: updated.status,
|
||||||
|
...(updated.startDate !== undefined ? { startDate: updated.startDate } : {}),
|
||||||
|
...(updated.endDate !== undefined ? { endDate: updated.endDate } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private deriveSeasonPropsFromConfig(config: LeagueConfigFormModel): {
|
||||||
|
schedule?: SeasonSchedule;
|
||||||
|
scoringConfig?: SeasonScoringConfig;
|
||||||
|
dropPolicy?: SeasonDropPolicy;
|
||||||
|
stewardingConfig?: SeasonStewardingConfig;
|
||||||
|
maxDrivers?: number;
|
||||||
|
} {
|
||||||
|
const schedule = this.buildScheduleFromTimings(config);
|
||||||
|
const scoringConfig = new SeasonScoringConfig({
|
||||||
|
scoringPresetId: config.scoring.patternId ?? 'custom',
|
||||||
|
customScoringEnabled: config.scoring.customScoringEnabled ?? false,
|
||||||
|
});
|
||||||
|
const dropPolicy = new SeasonDropPolicy({
|
||||||
|
strategy: config.dropPolicy.strategy,
|
||||||
|
...(config.dropPolicy.n !== undefined ? { n: config.dropPolicy.n } : {}),
|
||||||
|
});
|
||||||
|
const stewardingConfig = new SeasonStewardingConfig({
|
||||||
|
decisionMode: config.stewarding.decisionMode,
|
||||||
|
...(config.stewarding.requiredVotes !== undefined
|
||||||
|
? { requiredVotes: config.stewarding.requiredVotes }
|
||||||
|
: {}),
|
||||||
|
requireDefense: config.stewarding.requireDefense,
|
||||||
|
defenseTimeLimit: config.stewarding.defenseTimeLimit,
|
||||||
|
voteTimeLimit: config.stewarding.voteTimeLimit,
|
||||||
|
protestDeadlineHours: config.stewarding.protestDeadlineHours,
|
||||||
|
stewardingClosesHours: config.stewarding.stewardingClosesHours,
|
||||||
|
notifyAccusedOnProtest: config.stewarding.notifyAccusedOnProtest,
|
||||||
|
notifyOnVoteRequired: config.stewarding.notifyOnVoteRequired,
|
||||||
|
});
|
||||||
|
|
||||||
|
const structure = config.structure;
|
||||||
|
const maxDrivers =
|
||||||
|
typeof structure.maxDrivers === 'number' && structure.maxDrivers > 0
|
||||||
|
? structure.maxDrivers
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...(schedule !== undefined ? { schedule } : {}),
|
||||||
|
scoringConfig,
|
||||||
|
dropPolicy,
|
||||||
|
stewardingConfig,
|
||||||
|
...(maxDrivers !== undefined ? { maxDrivers } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildScheduleFromTimings(config: LeagueConfigFormModel): SeasonSchedule | undefined {
|
||||||
|
const { timings } = config;
|
||||||
|
if (!timings.seasonStartDate || !timings.raceStartTime) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startDate = new Date(timings.seasonStartDate);
|
||||||
|
const timeOfDay = RaceTimeOfDay.fromString(timings.raceStartTime);
|
||||||
|
const timezoneId = timings.timezoneId ?? 'UTC';
|
||||||
|
const timezone = new LeagueTimezone(timezoneId);
|
||||||
|
|
||||||
|
const plannedRounds =
|
||||||
|
typeof timings.roundsPlanned === 'number' && timings.roundsPlanned > 0
|
||||||
|
? timings.roundsPlanned
|
||||||
|
: timings.sessionCount;
|
||||||
|
|
||||||
|
const recurrence = (() => {
|
||||||
|
const weekdays: WeekdaySet =
|
||||||
|
timings.weekdays && timings.weekdays.length > 0
|
||||||
|
? WeekdaySet.fromArray(timings.weekdays as unknown as Weekday[])
|
||||||
|
: WeekdaySet.fromArray(['Mon']);
|
||||||
|
switch (timings.recurrenceStrategy) {
|
||||||
|
case 'everyNWeeks':
|
||||||
|
return RecurrenceStrategyFactory.everyNWeeks(
|
||||||
|
timings.intervalWeeks ?? 2,
|
||||||
|
weekdays,
|
||||||
|
);
|
||||||
|
case 'monthlyNthWeekday': {
|
||||||
|
const pattern = new MonthlyRecurrencePattern({
|
||||||
|
ordinal: (timings.monthlyOrdinal ?? 1) as 1 | 2 | 3 | 4,
|
||||||
|
weekday: (timings.monthlyWeekday ?? 'Mon') as Weekday,
|
||||||
|
});
|
||||||
|
return RecurrenceStrategyFactory.monthlyNthWeekday(pattern);
|
||||||
|
}
|
||||||
|
case 'weekly':
|
||||||
|
default:
|
||||||
|
return RecurrenceStrategyFactory.weekly(weekdays);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return new SeasonSchedule({
|
||||||
|
startDate,
|
||||||
|
timeOfDay,
|
||||||
|
timezone,
|
||||||
|
recurrence,
|
||||||
|
plannedRounds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,9 +10,8 @@ import type { ISponsorshipRequestRepository } from '../../domain/repositories/IS
|
|||||||
import type { ISponsorshipPricingRepository } from '../../domain/repositories/ISponsorshipPricingRepository';
|
import type { ISponsorshipPricingRepository } from '../../domain/repositories/ISponsorshipPricingRepository';
|
||||||
import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
|
import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
|
||||||
import { Money } from '../../domain/value-objects/Money';
|
import { Money } from '../../domain/value-objects/Money';
|
||||||
import type { AsyncUseCase } from '@core/shared/application';
|
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||||
import { Result } from '@core/shared/application/Result';
|
import { Result } from '@core/shared/application/Result';
|
||||||
import type { Logger } from '@core/shared/application';
|
|
||||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||||
import type { ApplyForSponsorshipPort } from '../ports/input/ApplyForSponsorshipPort';
|
import type { ApplyForSponsorshipPort } from '../ports/input/ApplyForSponsorshipPort';
|
||||||
import type { ApplyForSponsorshipResultPort } from '../ports/output/ApplyForSponsorshipResultPort';
|
import type { ApplyForSponsorshipResultPort } from '../ports/output/ApplyForSponsorshipResultPort';
|
||||||
|
|||||||
@@ -11,9 +11,8 @@ import type { IProtestRepository } from '../../domain/repositories/IProtestRepos
|
|||||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
import type { AsyncUseCase } from '@core/shared/application';
|
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||||
import { Result } from '@core/shared/application/Result';
|
import { Result } from '@core/shared/application/Result';
|
||||||
import type { Logger } from '@core/shared/application';
|
|
||||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||||
import type { ApplyPenaltyCommandPort } from '../ports/input/ApplyPenaltyCommandPort';
|
import type { ApplyPenaltyCommandPort } from '../ports/input/ApplyPenaltyCommandPort';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||||
import type { AsyncUseCase } from '@core/shared/application';
|
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||||
import type { Logger } from '@core/shared/application';
|
|
||||||
import { Result } from '@core/shared/application/Result';
|
import { Result } from '@core/shared/application/Result';
|
||||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||||
import type { CancelRaceCommandDTO } from '../dto/CancelRaceCommandDTO';
|
import type { CancelRaceCommandDTO } from '../dto/CancelRaceCommandDTO';
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { AsyncUseCase } from '@core/shared/application';
|
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||||
import type { Logger } from '@core/shared/application';
|
|
||||||
import type { IRaceEventRepository } from '../../domain/repositories/IRaceEventRepository';
|
import type { IRaceEventRepository } from '../../domain/repositories/IRaceEventRepository';
|
||||||
import type { IRaceRegistrationRepository } from '../../domain/repositories/IRaceRegistrationRepository';
|
import type { IRaceRegistrationRepository } from '../../domain/repositories/IRaceRegistrationRepository';
|
||||||
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
|
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import { Season } from '../../domain/entities/Season';
|
|||||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||||
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||||
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
|
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
|
||||||
import type { AsyncUseCase } from '@core/shared/application';
|
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||||
import type { Logger } from '@core/shared/application';
|
|
||||||
import type { GetLeagueScoringPresetByIdInputPort } from '../ports/input/GetLeagueScoringPresetByIdInputPort';
|
import type { GetLeagueScoringPresetByIdInputPort } from '../ports/input/GetLeagueScoringPresetByIdInputPort';
|
||||||
import type { LeagueScoringPresetOutputPort } from '../ports/output/LeagueScoringPresetOutputPort';
|
import type { LeagueScoringPresetOutputPort } from '../ports/output/LeagueScoringPresetOutputPort';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { Sponsor } from '../../domain/entities/Sponsor';
|
import { Sponsor } from '../../domain/entities/Sponsor';
|
||||||
import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
|
import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
|
||||||
import type { AsyncUseCase } from '@core/shared/application';
|
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||||
import type { Logger } from '@core/shared/application';
|
|
||||||
import { Result } from '@core/shared/application/Result';
|
import { Result } from '@core/shared/application/Result';
|
||||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||||
import type { CreateSponsorOutputPort } from '../ports/output/CreateSponsorOutputPort';
|
import type { CreateSponsorOutputPort } from '../ports/output/CreateSponsorOutputPort';
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ import type {
|
|||||||
TeamMembershipStatus,
|
TeamMembershipStatus,
|
||||||
TeamRole,
|
TeamRole,
|
||||||
} from '../../domain/types/TeamMembership';
|
} from '../../domain/types/TeamMembership';
|
||||||
import type { AsyncUseCase } from '@core/shared/application';
|
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||||
import type { Logger } from '@core/shared/application';
|
|
||||||
import { Result } from '@core/shared/application/Result';
|
import { Result } from '@core/shared/application/Result';
|
||||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||||
import type { CreateTeamOutputPort } from '../ports/output/CreateTeamOutputPort';
|
import type { CreateTeamOutputPort } from '../ports/output/CreateTeamOutputPort';
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import type { GetDriverAvatarOutputPort } from '../ports/output/GetDriverAvatarO
|
|||||||
import type { TeamJoinRequestsOutputPort } from '../ports/output/TeamJoinRequestsOutputPort';
|
import type { TeamJoinRequestsOutputPort } from '../ports/output/TeamJoinRequestsOutputPort';
|
||||||
import { Result } from '@core/shared/application/Result';
|
import { Result } from '@core/shared/application/Result';
|
||||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||||
import type { AsyncUseCase } from '@core/shared/application';
|
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||||
import type { Logger } from '@core/shared/application';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use Case for retrieving team join requests.
|
* Use Case for retrieving team join requests.
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import type { GetDriverAvatarOutputPort } from '../ports/output/GetDriverAvatarO
|
|||||||
import type { TeamMembersOutputPort } from '../ports/output/TeamMembersOutputPort';
|
import type { TeamMembersOutputPort } from '../ports/output/TeamMembersOutputPort';
|
||||||
import { Result } from '@core/shared/application/Result';
|
import { Result } from '@core/shared/application/Result';
|
||||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||||
import type { AsyncUseCase } from '@core/shared/application';
|
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||||
import type { Logger } from '@core/shared/application';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use Case for retrieving team members.
|
* Use Case for retrieving team members.
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import type { TeamsLeaderboardOutputPort, SkillLevel } from '../ports/output/Tea
|
|||||||
import { SkillLevelService } from '@core/racing/domain/services/SkillLevelService';
|
import { SkillLevelService } from '@core/racing/domain/services/SkillLevelService';
|
||||||
import { Result } from '@core/shared/application/Result';
|
import { Result } from '@core/shared/application/Result';
|
||||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||||
import type { AsyncUseCase } from '@core/shared/application';
|
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||||
import type { Logger } from '@core/shared/application';
|
|
||||||
|
|
||||||
interface DriverStatsAdapter {
|
interface DriverStatsAdapter {
|
||||||
rating: number | null;
|
rating: number | null;
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import type { IDriverRepository } from '../../domain/repositories/IDriverReposit
|
|||||||
import type { TotalDriversOutputPort } from '../ports/output/TotalDriversOutputPort';
|
import type { TotalDriversOutputPort } from '../ports/output/TotalDriversOutputPort';
|
||||||
import { Result } from '@core/shared/application/Result';
|
import { Result } from '@core/shared/application/Result';
|
||||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||||
import type { AsyncUseCase } from '@core/shared/application';
|
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||||
import type { Logger } from '@core/shared/application';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use Case for retrieving total number of drivers.
|
* Use Case for retrieving total number of drivers.
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
|
|||||||
import type { GetTotalLeaguesOutputPort } from '../ports/output/GetTotalLeaguesOutputPort';
|
import type { GetTotalLeaguesOutputPort } from '../ports/output/GetTotalLeaguesOutputPort';
|
||||||
import { Result } from '@core/shared/application/Result';
|
import { Result } from '@core/shared/application/Result';
|
||||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||||
import type { AsyncUseCase } from '@core/shared/application';
|
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||||
import type { Logger } from '@core/shared/application';
|
|
||||||
|
|
||||||
export class GetTotalLeaguesUseCase implements AsyncUseCase<void, GetTotalLeaguesOutputPort, 'REPOSITORY_ERROR'>
|
export class GetTotalLeaguesUseCase implements AsyncUseCase<void, GetTotalLeaguesOutputPort, 'REPOSITORY_ERROR'>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'
|
|||||||
import type { GetTotalRacesOutputPort } from '../ports/output/GetTotalRacesOutputPort';
|
import type { GetTotalRacesOutputPort } from '../ports/output/GetTotalRacesOutputPort';
|
||||||
import { Result } from '@core/shared/application/Result';
|
import { Result } from '@core/shared/application/Result';
|
||||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||||
import type { AsyncUseCase } from '@core/shared/application';
|
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||||
import type { Logger } from '@core/shared/application';
|
|
||||||
|
|
||||||
export class GetTotalRacesUseCase implements AsyncUseCase<void, GetTotalRacesOutputPort, 'REPOSITORY_ERROR'>
|
export class GetTotalRacesUseCase implements AsyncUseCase<void, GetTotalRacesOutputPort, 'REPOSITORY_ERROR'>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { Logger } from '@core/shared/application';
|
import type { Logger , AsyncUseCase } from '@core/shared/application';
|
||||||
import type { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
|
import type { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
|
||||||
import { LeagueMembership, type MembershipRole, type MembershipStatus } from '../../domain/entities/LeagueMembership';
|
import { LeagueMembership, type MembershipRole, type MembershipStatus } from '../../domain/entities/LeagueMembership';
|
||||||
import type { AsyncUseCase } from '@core/shared/application';
|
|
||||||
import { Result as SharedResult } from '@core/shared/application/Result';
|
import { Result as SharedResult } from '@core/shared/application/Result';
|
||||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||||
import type { JoinLeagueOutputPort } from '../ports/output/JoinLeagueOutputPort';
|
import type { JoinLeagueOutputPort } from '../ports/output/JoinLeagueOutputPort';
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepos
|
|||||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
import type { AsyncUseCase } from '@core/shared/application';
|
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||||
import type { Logger } from '@core/shared/application';
|
|
||||||
import { Result } from '@core/shared/application/Result';
|
import { Result } from '@core/shared/application/Result';
|
||||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,36 @@
|
|||||||
import { RejectLeagueJoinRequestUseCase } from '@core/racing/application/use-cases/RejectLeagueJoinRequestUseCase';
|
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||||
import { RejectLeagueJoinRequestPresenter } from '@apps/api/src/modules/league/presenters/RejectLeagueJoinRequestPresenter';
|
import { RejectLeagueJoinRequestUseCase, type RejectLeagueJoinRequestUseCaseParams } from './RejectLeagueJoinRequestUseCase';
|
||||||
|
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||||
|
|
||||||
|
interface LeagueMembershipRepositoryMock {
|
||||||
|
removeJoinRequest: Mock;
|
||||||
|
}
|
||||||
|
|
||||||
describe('RejectLeagueJoinRequestUseCase', () => {
|
describe('RejectLeagueJoinRequestUseCase', () => {
|
||||||
|
let leagueMembershipRepository: LeagueMembershipRepositoryMock;
|
||||||
let useCase: RejectLeagueJoinRequestUseCase;
|
let useCase: RejectLeagueJoinRequestUseCase;
|
||||||
let leagueMembershipRepository: jest.Mocked<ILeagueMembershipRepository>;
|
|
||||||
let presenter: RejectLeagueJoinRequestPresenter;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
leagueMembershipRepository = {
|
leagueMembershipRepository = {
|
||||||
removeJoinRequest: jest.fn(),
|
removeJoinRequest: vi.fn(),
|
||||||
} as unknown;
|
} as unknown as ILeagueMembershipRepository as any;
|
||||||
presenter = new RejectLeagueJoinRequestPresenter();
|
|
||||||
useCase = new RejectLeagueJoinRequestUseCase(leagueMembershipRepository);
|
useCase = new RejectLeagueJoinRequestUseCase(
|
||||||
|
leagueMembershipRepository as unknown as ILeagueMembershipRepository,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject join request', async () => {
|
it('removes the join request and returns success output', async () => {
|
||||||
const requestId = 'req-1';
|
const params: RejectLeagueJoinRequestUseCaseParams = {
|
||||||
|
requestId: 'req-1',
|
||||||
|
};
|
||||||
|
|
||||||
await useCase.execute({ requestId }, presenter);
|
(leagueMembershipRepository.removeJoinRequest as Mock).mockResolvedValue(undefined);
|
||||||
|
|
||||||
expect(leagueMembershipRepository.removeJoinRequest).toHaveBeenCalledWith(requestId);
|
const result = await useCase.execute(params);
|
||||||
expect(presenter.viewModel).toEqual({ success: true, message: 'Join request rejected.' });
|
|
||||||
|
expect(leagueMembershipRepository.removeJoinRequest).toHaveBeenCalledWith('req-1');
|
||||||
|
expect(result.isOk()).toBe(true);
|
||||||
|
expect(result.unwrap()).toEqual({ success: true, message: 'Join request rejected.' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||||
import type { AsyncUseCase } from '@core/shared/application';
|
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||||
import type { Logger } from '@core/shared/application';
|
|
||||||
import { Result } from '@core/shared/application/Result';
|
import { Result } from '@core/shared/application/Result';
|
||||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||||
import type { ReopenRaceCommandDTO } from '../dto/ReopenRaceCommandDTO';
|
import type { ReopenRaceCommandDTO } from '../dto/ReopenRaceCommandDTO';
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { ChampionshipType } from '../types/ChampionshipType';
|
import type { ChampionshipType } from "./ChampionshipType";
|
||||||
import type { SessionType } from '../types/SessionType';
|
import type { SessionType } from "./SessionType";
|
||||||
import type { PointsTable } from '../value-objects/PointsTable';
|
import type { PointsTable } from '../value-objects/PointsTable';
|
||||||
import type { BonusRule } from '../types/BonusRule';
|
import type { BonusRule } from "./BonusRule";
|
||||||
import type { DropScorePolicy } from '../types/DropScorePolicy';
|
import type { DropScorePolicy } from "./DropScorePolicy";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Domain Type: ChampionshipConfig
|
* Domain Type: ChampionshipConfig
|
||||||
|
|||||||
@@ -37,10 +37,6 @@ export * from './domain/repositories/ISponsorRepository';
|
|||||||
export * from './domain/repositories/ISeasonSponsorshipRepository';
|
export * from './domain/repositories/ISeasonSponsorshipRepository';
|
||||||
export * from './domain/repositories/ISponsorshipRequestRepository';
|
export * from './domain/repositories/ISponsorshipRequestRepository';
|
||||||
export * from './domain/repositories/ISponsorshipPricingRepository';
|
export * from './domain/repositories/ISponsorshipPricingRepository';
|
||||||
export * from './infrastructure/repositories/InMemorySponsorRepository';
|
|
||||||
export * from './infrastructure/repositories/InMemorySeasonSponsorshipRepository';
|
|
||||||
export * from './infrastructure/repositories/InMemorySponsorshipRequestRepository';
|
|
||||||
export * from './infrastructure/repositories/InMemorySponsorshipPricingRepository';
|
|
||||||
|
|
||||||
export * from './application/dtos/LeagueDriverSeasonStatsDTO';
|
export * from './application/dtos/LeagueDriverSeasonStatsDTO';
|
||||||
export * from './application/dtos/LeagueScoringConfigDTO';
|
export * from './application/dtos/LeagueScoringConfigDTO';
|
||||||
@@ -58,4 +54,4 @@ export * from './application/use-cases/RejectSponsorshipRequestUseCase';
|
|||||||
export * from './application/use-cases/GetPendingSponsorshipRequestsUseCase';
|
export * from './application/use-cases/GetPendingSponsorshipRequestsUseCase';
|
||||||
export * from './application/use-cases/GetEntitySponsorshipPricingUseCase';
|
export * from './application/use-cases/GetEntitySponsorshipPricingUseCase';
|
||||||
|
|
||||||
export * from './application/ports/output/CreateSponsorOutputPort';
|
export * from './application/ports/output/CreateSponsorOutputPort';
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { AsyncUseCase } from '@core/shared/application';
|
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||||
import type { Logger } from '@core/shared/application';
|
|
||||||
import type { ISocialGraphRepository } from '../../domain/repositories/ISocialGraphRepository';
|
import type { ISocialGraphRepository } from '../../domain/repositories/ISocialGraphRepository';
|
||||||
import type { CurrentUserSocialDTO } from '../dto/CurrentUserSocialDTO';
|
import type { CurrentUserSocialDTO } from '../dto/CurrentUserSocialDTO';
|
||||||
import type { FriendDTO } from '../dto/FriendDTO';
|
import type { FriendDTO } from '../dto/FriendDTO';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { AsyncUseCase } from '@core/shared/application';
|
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||||
import type { IFeedRepository } from '../../domain/repositories/IFeedRepository';
|
import type { IFeedRepository } from '../../domain/repositories/IFeedRepository';
|
||||||
import type { FeedItemDTO } from '../dto/FeedItemDTO';
|
import type { FeedItemDTO } from '../dto/FeedItemDTO';
|
||||||
import type { FeedItem } from '../../domain/types/FeedItem';
|
import type { FeedItem } from '../../domain/types/FeedItem';
|
||||||
@@ -6,7 +6,6 @@ import type {
|
|||||||
IUserFeedPresenter,
|
IUserFeedPresenter,
|
||||||
UserFeedViewModel,
|
UserFeedViewModel,
|
||||||
} from '../presenters/ISocialPresenters';
|
} from '../presenters/ISocialPresenters';
|
||||||
import type { Logger } from '@core/shared/application';
|
|
||||||
|
|
||||||
export interface GetUserFeedParams {
|
export interface GetUserFeedParams {
|
||||||
driverId: string;
|
driverId: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user