test apps api
This commit is contained in:
@@ -2,7 +2,7 @@ import type { GetAnalyticsMetricsOutput } from '@core/analytics/application/use-
|
||||
import type { RecordEngagementOutput } from '@core/analytics/application/use-cases/RecordEngagementUseCase';
|
||||
import type { RecordPageViewOutput } from '@core/analytics/application/use-cases/RecordPageViewUseCase';
|
||||
import type { IEngagementRepository } from '@core/analytics/domain/repositories/IEngagementRepository';
|
||||
import type { IPageViewRepository } from '@core/analytics/domain/repositories/IPageViewRepository';
|
||||
import type { IPageViewRepository } from '@core/analytics/application/repositories/IPageViewRepository';
|
||||
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
|
||||
import { Provider } from '@nestjs/common';
|
||||
|
||||
@@ -22,23 +22,31 @@ import { GetAnalyticsMetricsUseCase } from '@core/analytics/application/use-case
|
||||
import { GetDashboardDataOutput, GetDashboardDataUseCase } from '@core/analytics/application/use-cases/GetDashboardDataUseCase';
|
||||
import { RecordEngagementUseCase } from '@core/analytics/application/use-cases/RecordEngagementUseCase';
|
||||
import { RecordPageViewUseCase } from '@core/analytics/application/use-cases/RecordPageViewUseCase';
|
||||
import { AnalyticsService } from './AnalyticsService';
|
||||
import { GetAnalyticsMetricsPresenter } from './presenters/GetAnalyticsMetricsPresenter';
|
||||
import { GetDashboardDataPresenter } from './presenters/GetDashboardDataPresenter';
|
||||
import { RecordEngagementPresenter } from './presenters/RecordEngagementPresenter';
|
||||
import { RecordPageViewPresenter } from './presenters/RecordPageViewPresenter';
|
||||
|
||||
export const AnalyticsProviders: Provider[] = [
|
||||
AnalyticsService,
|
||||
RecordPageViewPresenter,
|
||||
RecordEngagementPresenter,
|
||||
GetDashboardDataPresenter,
|
||||
GetAnalyticsMetricsPresenter,
|
||||
{
|
||||
provide: Logger_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
{
|
||||
provide: IPAGE_VIEW_REPO_TOKEN,
|
||||
useClass: InMemoryPageViewRepository,
|
||||
useFactory: (logger: Logger) => new InMemoryPageViewRepository(logger),
|
||||
inject: [Logger_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: IENGAGEMENT_REPO_TOKEN,
|
||||
useClass: InMemoryEngagementRepository,
|
||||
useFactory: (logger: Logger) => new InMemoryEngagementRepository(logger),
|
||||
inject: [Logger_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: RECORD_PAGE_VIEW_OUTPUT_PORT_TOKEN,
|
||||
@@ -76,8 +84,8 @@ export const AnalyticsProviders: Provider[] = [
|
||||
},
|
||||
{
|
||||
provide: GetAnalyticsMetricsUseCase,
|
||||
useFactory: (repo: IPageViewRepository, logger: Logger, output: UseCaseOutputPort<GetAnalyticsMetricsOutput>) =>
|
||||
new GetAnalyticsMetricsUseCase(repo, logger, output),
|
||||
inject: [IPAGE_VIEW_REPO_TOKEN, Logger_TOKEN, GET_ANALYTICS_METRICS_OUTPUT_PORT_TOKEN],
|
||||
useFactory: (logger: Logger, output: UseCaseOutputPort<GetAnalyticsMetricsOutput>, repo: IPageViewRepository) =>
|
||||
new GetAnalyticsMetricsUseCase(logger, output, repo),
|
||||
inject: [Logger_TOKEN, GET_ANALYTICS_METRICS_OUTPUT_PORT_TOKEN, IPAGE_VIEW_REPO_TOKEN],
|
||||
},
|
||||
];
|
||||
278
apps/api/src/domain/analytics/AnalyticsService.test.ts
Normal file
278
apps/api/src/domain/analytics/AnalyticsService.test.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import { AnalyticsService } from './AnalyticsService';
|
||||
import { RecordPageViewPresenter } from './presenters/RecordPageViewPresenter';
|
||||
import { RecordEngagementPresenter } from './presenters/RecordEngagementPresenter';
|
||||
import { GetDashboardDataPresenter } from './presenters/GetDashboardDataPresenter';
|
||||
import { GetAnalyticsMetricsPresenter } from './presenters/GetAnalyticsMetricsPresenter';
|
||||
|
||||
describe('AnalyticsService', () => {
|
||||
it('recordPageView returns presenter response on success', async () => {
|
||||
const recordPageViewPresenter = new RecordPageViewPresenter();
|
||||
|
||||
const recordPageViewUseCase = {
|
||||
execute: vi.fn(async () => {
|
||||
recordPageViewPresenter.present({ pageViewId: 'pv-1' });
|
||||
return Result.ok(undefined);
|
||||
}),
|
||||
};
|
||||
|
||||
const service = new AnalyticsService(
|
||||
recordPageViewUseCase as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
recordPageViewPresenter,
|
||||
new RecordEngagementPresenter(),
|
||||
new GetDashboardDataPresenter(),
|
||||
new GetAnalyticsMetricsPresenter(),
|
||||
);
|
||||
|
||||
const dto = await service.recordPageView({
|
||||
entityType: 'league' as any,
|
||||
entityId: 'l1',
|
||||
visitorType: 'anonymous' as any,
|
||||
sessionId: 's1',
|
||||
});
|
||||
|
||||
expect(dto).toEqual({ pageViewId: 'pv-1' });
|
||||
expect(recordPageViewUseCase.execute).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('recordPageView throws on use case error', async () => {
|
||||
const service = new AnalyticsService(
|
||||
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'nope' } })) } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
new RecordPageViewPresenter(),
|
||||
new RecordEngagementPresenter(),
|
||||
new GetDashboardDataPresenter(),
|
||||
new GetAnalyticsMetricsPresenter(),
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.recordPageView({ entityType: 'league' as any, entityId: 'l1', visitorType: 'anonymous' as any, sessionId: 's1' }),
|
||||
).rejects.toThrow('nope');
|
||||
});
|
||||
|
||||
it('recordPageView throws with fallback message when no details.message', async () => {
|
||||
const service = new AnalyticsService(
|
||||
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR' } as any)) } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
new RecordPageViewPresenter(),
|
||||
new RecordEngagementPresenter(),
|
||||
new GetDashboardDataPresenter(),
|
||||
new GetAnalyticsMetricsPresenter(),
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.recordPageView({ entityType: 'league' as any, entityId: 'l1', visitorType: 'anonymous' as any, sessionId: 's1' }),
|
||||
).rejects.toThrow('Failed to record page view');
|
||||
});
|
||||
|
||||
it('recordEngagement returns response on success', async () => {
|
||||
const recordEngagementPresenter = new RecordEngagementPresenter();
|
||||
const recordEngagementUseCase = {
|
||||
execute: vi.fn(async () => {
|
||||
recordEngagementPresenter.present({ eventId: 'e1', engagementWeight: 7 });
|
||||
return Result.ok(undefined);
|
||||
}),
|
||||
};
|
||||
|
||||
const service = new AnalyticsService(
|
||||
{ execute: vi.fn() } as any,
|
||||
recordEngagementUseCase as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
new RecordPageViewPresenter(),
|
||||
recordEngagementPresenter,
|
||||
new GetDashboardDataPresenter(),
|
||||
new GetAnalyticsMetricsPresenter(),
|
||||
);
|
||||
|
||||
const dto = await service.recordEngagement({
|
||||
action: 'click' as any,
|
||||
entityType: 'league' as any,
|
||||
entityId: 'l1',
|
||||
actorType: 'anonymous',
|
||||
sessionId: 's1',
|
||||
});
|
||||
|
||||
expect(dto).toEqual({ eventId: 'e1', engagementWeight: 7 });
|
||||
});
|
||||
|
||||
it('recordEngagement throws with details message on error', async () => {
|
||||
const service = new AnalyticsService(
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'nope' } })) } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
new RecordPageViewPresenter(),
|
||||
new RecordEngagementPresenter(),
|
||||
new GetDashboardDataPresenter(),
|
||||
new GetAnalyticsMetricsPresenter(),
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.recordEngagement({
|
||||
action: 'click' as any,
|
||||
entityType: 'league' as any,
|
||||
entityId: 'l1',
|
||||
actorType: 'anonymous',
|
||||
sessionId: 's1',
|
||||
}),
|
||||
).rejects.toThrow('nope');
|
||||
});
|
||||
|
||||
it('recordEngagement throws with fallback message when no details.message', async () => {
|
||||
const service = new AnalyticsService(
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR' } as any)) } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
new RecordPageViewPresenter(),
|
||||
new RecordEngagementPresenter(),
|
||||
new GetDashboardDataPresenter(),
|
||||
new GetAnalyticsMetricsPresenter(),
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.recordEngagement({
|
||||
action: 'click' as any,
|
||||
entityType: 'league' as any,
|
||||
entityId: 'l1',
|
||||
actorType: 'anonymous',
|
||||
sessionId: 's1',
|
||||
}),
|
||||
).rejects.toThrow('Failed to record engagement');
|
||||
});
|
||||
|
||||
it('getDashboardData returns response on success', async () => {
|
||||
const getDashboardDataPresenter = new GetDashboardDataPresenter();
|
||||
const getDashboardDataUseCase = {
|
||||
execute: vi.fn(async () => {
|
||||
getDashboardDataPresenter.present({
|
||||
totalUsers: 1,
|
||||
activeUsers: 2,
|
||||
totalRaces: 3,
|
||||
totalLeagues: 4,
|
||||
});
|
||||
return Result.ok(undefined);
|
||||
}),
|
||||
};
|
||||
|
||||
const service = new AnalyticsService(
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
getDashboardDataUseCase as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
new RecordPageViewPresenter(),
|
||||
new RecordEngagementPresenter(),
|
||||
getDashboardDataPresenter,
|
||||
new GetAnalyticsMetricsPresenter(),
|
||||
);
|
||||
|
||||
await expect(service.getDashboardData()).resolves.toEqual({
|
||||
totalUsers: 1,
|
||||
activeUsers: 2,
|
||||
totalRaces: 3,
|
||||
totalLeagues: 4,
|
||||
});
|
||||
});
|
||||
|
||||
it('getDashboardData throws with details message on error', async () => {
|
||||
const service = new AnalyticsService(
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'boom' } })) } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
new RecordPageViewPresenter(),
|
||||
new RecordEngagementPresenter(),
|
||||
new GetDashboardDataPresenter(),
|
||||
new GetAnalyticsMetricsPresenter(),
|
||||
);
|
||||
|
||||
await expect(service.getDashboardData()).rejects.toThrow('boom');
|
||||
});
|
||||
|
||||
it('getDashboardData throws with fallback message when no details.message', async () => {
|
||||
const service = new AnalyticsService(
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR' } as any)) } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
new RecordPageViewPresenter(),
|
||||
new RecordEngagementPresenter(),
|
||||
new GetDashboardDataPresenter(),
|
||||
new GetAnalyticsMetricsPresenter(),
|
||||
);
|
||||
|
||||
await expect(service.getDashboardData()).rejects.toThrow('Failed to get dashboard data');
|
||||
});
|
||||
|
||||
it('getAnalyticsMetrics returns response on success', async () => {
|
||||
const getAnalyticsMetricsPresenter = new GetAnalyticsMetricsPresenter();
|
||||
const getAnalyticsMetricsUseCase = {
|
||||
execute: vi.fn(async () => {
|
||||
getAnalyticsMetricsPresenter.present({
|
||||
pageViews: 10,
|
||||
uniqueVisitors: 0,
|
||||
averageSessionDuration: 0,
|
||||
bounceRate: 0,
|
||||
});
|
||||
return Result.ok(undefined);
|
||||
}),
|
||||
};
|
||||
|
||||
const service = new AnalyticsService(
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
getAnalyticsMetricsUseCase as any,
|
||||
new RecordPageViewPresenter(),
|
||||
new RecordEngagementPresenter(),
|
||||
new GetDashboardDataPresenter(),
|
||||
getAnalyticsMetricsPresenter,
|
||||
);
|
||||
|
||||
await expect(service.getAnalyticsMetrics()).resolves.toEqual({
|
||||
pageViews: 10,
|
||||
uniqueVisitors: 0,
|
||||
averageSessionDuration: 0,
|
||||
bounceRate: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('getAnalyticsMetrics throws with details message on error', async () => {
|
||||
const service = new AnalyticsService(
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'boom' } })) } as any,
|
||||
new RecordPageViewPresenter(),
|
||||
new RecordEngagementPresenter(),
|
||||
new GetDashboardDataPresenter(),
|
||||
new GetAnalyticsMetricsPresenter(),
|
||||
);
|
||||
|
||||
await expect(service.getAnalyticsMetrics()).rejects.toThrow('boom');
|
||||
});
|
||||
|
||||
it('getAnalyticsMetrics throws with fallback message when no details.message', async () => {
|
||||
const service = new AnalyticsService(
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR' } as any)) } as any,
|
||||
new RecordPageViewPresenter(),
|
||||
new RecordEngagementPresenter(),
|
||||
new GetDashboardDataPresenter(),
|
||||
new GetAnalyticsMetricsPresenter(),
|
||||
);
|
||||
|
||||
await expect(service.getAnalyticsMetrics()).rejects.toThrow('Failed to get analytics metrics');
|
||||
});
|
||||
});
|
||||
@@ -24,10 +24,14 @@ export class AnalyticsService {
|
||||
@Inject(RecordEngagementUseCase) private readonly recordEngagementUseCase: RecordEngagementUseCase,
|
||||
@Inject(GetDashboardDataUseCase) private readonly getDashboardDataUseCase: GetDashboardDataUseCase,
|
||||
@Inject(GetAnalyticsMetricsUseCase) private readonly getAnalyticsMetricsUseCase: GetAnalyticsMetricsUseCase,
|
||||
@Inject(RecordPageViewPresenter) private readonly recordPageViewPresenter: RecordPageViewPresenter,
|
||||
@Inject(RecordEngagementPresenter) private readonly recordEngagementPresenter: RecordEngagementPresenter,
|
||||
@Inject(GetDashboardDataPresenter) private readonly getDashboardDataPresenter: GetDashboardDataPresenter,
|
||||
@Inject(GetAnalyticsMetricsPresenter) private readonly getAnalyticsMetricsPresenter: GetAnalyticsMetricsPresenter,
|
||||
) {}
|
||||
|
||||
async recordPageView(input: RecordPageViewInput): Promise<RecordPageViewOutputDTO> {
|
||||
const presenter = new RecordPageViewPresenter();
|
||||
this.recordPageViewPresenter.reset();
|
||||
|
||||
const result = await this.recordPageViewUseCase.execute(input);
|
||||
if (result.isErr()) {
|
||||
@@ -35,11 +39,11 @@ export class AnalyticsService {
|
||||
throw new Error(error.details?.message ?? 'Failed to record page view');
|
||||
}
|
||||
|
||||
return presenter.responseModel;
|
||||
return this.recordPageViewPresenter.responseModel;
|
||||
}
|
||||
|
||||
async recordEngagement(input: RecordEngagementInput): Promise<RecordEngagementOutputDTO> {
|
||||
const presenter = new RecordEngagementPresenter();
|
||||
this.recordEngagementPresenter.reset();
|
||||
|
||||
const result = await this.recordEngagementUseCase.execute(input);
|
||||
if (result.isErr()) {
|
||||
@@ -47,23 +51,23 @@ export class AnalyticsService {
|
||||
throw new Error(error.details?.message ?? 'Failed to record engagement');
|
||||
}
|
||||
|
||||
return presenter.responseModel;
|
||||
return this.recordEngagementPresenter.responseModel;
|
||||
}
|
||||
|
||||
async getDashboardData(): Promise<GetDashboardDataOutputDTO> {
|
||||
const presenter = new GetDashboardDataPresenter();
|
||||
this.getDashboardDataPresenter.reset();
|
||||
|
||||
const result = await this.getDashboardDataUseCase.execute({});
|
||||
const result = await this.getDashboardDataUseCase.execute();
|
||||
if (result.isErr()) {
|
||||
const error = result.unwrapErr();
|
||||
throw new Error(error.details?.message ?? 'Failed to get dashboard data');
|
||||
}
|
||||
|
||||
return presenter.responseModel;
|
||||
return this.getDashboardDataPresenter.responseModel;
|
||||
}
|
||||
|
||||
async getAnalyticsMetrics(): Promise<GetAnalyticsMetricsOutputDTO> {
|
||||
const presenter = new GetAnalyticsMetricsPresenter();
|
||||
this.getAnalyticsMetricsPresenter.reset();
|
||||
|
||||
const result = await this.getAnalyticsMetricsUseCase.execute({});
|
||||
if (result.isErr()) {
|
||||
@@ -71,6 +75,6 @@ export class AnalyticsService {
|
||||
throw new Error(error.details?.message ?? 'Failed to get analytics metrics');
|
||||
}
|
||||
|
||||
return presenter.responseModel;
|
||||
return this.getAnalyticsMetricsPresenter.responseModel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,10 @@ import type { GetAnalyticsMetricsOutputDTO } from '../dtos/GetAnalyticsMetricsOu
|
||||
export class GetAnalyticsMetricsPresenter implements UseCaseOutputPort<GetAnalyticsMetricsOutput> {
|
||||
private model: GetAnalyticsMetricsOutputDTO | null = null;
|
||||
|
||||
reset(): void {
|
||||
this.model = null;
|
||||
}
|
||||
|
||||
present(result: GetAnalyticsMetricsOutput): void {
|
||||
this.model = {
|
||||
pageViews: result.pageViews,
|
||||
|
||||
@@ -5,6 +5,10 @@ import type { GetDashboardDataOutputDTO } from '../dtos/GetDashboardDataOutputDT
|
||||
export class GetDashboardDataPresenter implements UseCaseOutputPort<GetDashboardDataOutput> {
|
||||
private model: GetDashboardDataOutputDTO | null = null;
|
||||
|
||||
reset(): void {
|
||||
this.model = null;
|
||||
}
|
||||
|
||||
present(result: GetDashboardDataOutput): void {
|
||||
this.model = {
|
||||
totalUsers: result.totalUsers,
|
||||
|
||||
@@ -5,6 +5,10 @@ import type { RecordEngagementOutputDTO } from '../dtos/RecordEngagementOutputDT
|
||||
export class RecordEngagementPresenter implements UseCaseOutputPort<RecordEngagementOutput> {
|
||||
private model: RecordEngagementOutputDTO | null = null;
|
||||
|
||||
reset(): void {
|
||||
this.model = null;
|
||||
}
|
||||
|
||||
present(result: RecordEngagementOutput): void {
|
||||
this.model = {
|
||||
eventId: result.eventId,
|
||||
|
||||
@@ -5,6 +5,10 @@ import type { RecordPageViewOutputDTO } from '../dtos/RecordPageViewOutputDTO';
|
||||
export class RecordPageViewPresenter implements UseCaseOutputPort<RecordPageViewOutput> {
|
||||
private model: RecordPageViewOutputDTO | null = null;
|
||||
|
||||
reset(): void {
|
||||
this.model = null;
|
||||
}
|
||||
|
||||
present(result: RecordPageViewOutput): void {
|
||||
this.model = {
|
||||
pageViewId: result.pageViewId,
|
||||
|
||||
Reference in New Issue
Block a user