add tests
Some checks failed
Contract Testing / contract-tests (push) Failing after 6m7s
Contract Testing / contract-snapshot (push) Failing after 4m46s

This commit is contained in:
2026-01-22 11:52:42 +01:00
parent 40bc15ff61
commit fb1221701d
112 changed files with 30625 additions and 1059 deletions

View File

@@ -0,0 +1,30 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AdminController } from './AdminController';
import { AdminModule } from './AdminModule';
import { AdminService } from './AdminService';
describe('AdminModule', () => {
let module: TestingModule;
beforeEach(async () => {
module = await Test.createTestingModule({
imports: [AdminModule],
}).compile();
});
it('should compile the module', () => {
expect(module).toBeDefined();
});
it('should provide AdminController', () => {
const controller = module.get<AdminController>(AdminController);
expect(controller).toBeDefined();
expect(controller).toBeInstanceOf(AdminController);
});
it('should provide AdminService', () => {
const service = module.get<AdminService>(AdminService);
expect(service).toBeDefined();
expect(service).toBeInstanceOf(AdminService);
});
});

View File

@@ -0,0 +1,40 @@
import { SetMetadata } from '@nestjs/common';
import { RequireSystemAdmin, REQUIRE_SYSTEM_ADMIN_METADATA_KEY } from './RequireSystemAdmin';
// Mock SetMetadata
vi.mock('@nestjs/common', () => ({
SetMetadata: vi.fn(() => () => {}),
}));
describe('RequireSystemAdmin', () => {
it('should return a method decorator', () => {
const decorator = RequireSystemAdmin();
expect(typeof decorator).toBe('function');
});
it('should call SetMetadata with correct key and value', () => {
RequireSystemAdmin();
expect(SetMetadata).toHaveBeenCalledWith(REQUIRE_SYSTEM_ADMIN_METADATA_KEY, {
required: true,
});
});
it('should return a decorator that can be used as both method and class decorator', () => {
const decorator = RequireSystemAdmin();
// Test as method decorator
const mockTarget = {};
const mockPropertyKey = 'testMethod';
const mockDescriptor = { value: () => {} };
const result = decorator(mockTarget, mockPropertyKey, mockDescriptor);
// The decorator should return the descriptor
expect(result).toBe(mockDescriptor);
});
it('should have correct metadata key', () => {
expect(REQUIRE_SYSTEM_ADMIN_METADATA_KEY).toBe('gridpilot:requireSystemAdmin');
});
});

View File

@@ -0,0 +1,680 @@
import { AdminUser } from '@core/admin/domain/entities/AdminUser';
import { AuthorizationService } from '@core/admin/domain/services/AuthorizationService';
import { Result } from '@core/shared/domain/Result';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { GetDashboardStatsUseCase } from './GetDashboardStatsUseCase';
// Mock dependencies
const mockAdminUserRepo = {
findById: vi.fn(),
list: vi.fn(),
};
describe('GetDashboardStatsUseCase', () => {
describe('TDD - Test First', () => {
let useCase: GetDashboardStatsUseCase;
beforeEach(() => {
vi.clearAllMocks();
useCase = new GetDashboardStatsUseCase(mockAdminUserRepo as never);
});
describe('execute', () => {
it('should return error when actor is not found', async () => {
// Arrange
mockAdminUserRepo.findById.mockResolvedValue(null);
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isErr()).toBe(true);
const error = result.unwrapErr();
expect(error.code).toBe('AUTHORIZATION_ERROR');
expect(error.details.message).toBe('Actor not found');
});
it('should return error when actor is not authorized to view dashboard', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['user'],
status: 'active',
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockResolvedValue({ users: [] });
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isErr()).toBe(true);
const error = result.unwrapErr();
expect(error.code).toBe('AUTHORIZATION_ERROR');
expect(error.details.message).toBe('User is not authorized to view dashboard');
});
it('should return empty stats when no users exist', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['owner'],
status: 'active',
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockResolvedValue({ users: [] });
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isOk()).toBe(true);
const stats = result.unwrap();
expect(stats.totalUsers).toBe(0);
expect(stats.activeUsers).toBe(0);
expect(stats.suspendedUsers).toBe(0);
expect(stats.deletedUsers).toBe(0);
expect(stats.systemAdmins).toBe(0);
expect(stats.recentLogins).toBe(0);
expect(stats.newUsersToday).toBe(0);
expect(stats.userGrowth).toEqual([]);
expect(stats.roleDistribution).toEqual([]);
expect(stats.statusDistribution).toEqual({
active: 0,
suspended: 0,
deleted: 0,
});
expect(stats.activityTimeline).toEqual([]);
});
it('should return correct stats when users exist', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['owner'],
status: 'active',
});
const user1 = AdminUser.create({
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['user'],
status: 'active',
createdAt: new Date(),
updatedAt: new Date(),
});
const user2 = AdminUser.create({
id: 'user-2',
email: 'user2@example.com',
displayName: 'User 2',
roles: ['admin'],
status: 'suspended',
createdAt: new Date(),
updatedAt: new Date(),
});
const user3 = AdminUser.create({
id: 'user-3',
email: 'user3@example.com',
displayName: 'User 3',
roles: ['owner'],
status: 'deleted',
createdAt: new Date(),
updatedAt: new Date(),
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockResolvedValue({ users: [user1, user2, user3] });
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isOk()).toBe(true);
const stats = result.unwrap();
expect(stats.totalUsers).toBe(3);
expect(stats.activeUsers).toBe(1);
expect(stats.suspendedUsers).toBe(1);
expect(stats.deletedUsers).toBe(1);
expect(stats.systemAdmins).toBe(2); // actor + user3
expect(stats.recentLogins).toBe(0); // no recent logins
expect(stats.newUsersToday).toBe(3); // all created today
expect(stats.userGrowth).toHaveLength(7);
expect(stats.roleDistribution).toHaveLength(3);
expect(stats.statusDistribution).toEqual({
active: 1,
suspended: 1,
deleted: 1,
});
expect(stats.activityTimeline).toHaveLength(7);
});
it('should count recent logins correctly', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['owner'],
status: 'active',
});
const recentLoginUser = AdminUser.create({
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['user'],
status: 'active',
createdAt: new Date(Date.now() - 86400000 * 2), // 2 days ago
updatedAt: new Date(Date.now() - 86400000 * 2),
lastLoginAt: new Date(Date.now() - 3600000), // 1 hour ago
});
const oldLoginUser = AdminUser.create({
id: 'user-2',
email: 'user2@example.com',
displayName: 'User 2',
roles: ['user'],
status: 'active',
createdAt: new Date(Date.now() - 86400000 * 2),
updatedAt: new Date(Date.now() - 86400000 * 2),
lastLoginAt: new Date(Date.now() - 86400000 * 2), // 2 days ago
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockResolvedValue({ users: [recentLoginUser, oldLoginUser] });
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isOk()).toBe(true);
const stats = result.unwrap();
expect(stats.recentLogins).toBe(1);
});
it('should count new users today correctly', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['owner'],
status: 'active',
});
const todayUser = AdminUser.create({
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['user'],
status: 'active',
createdAt: new Date(),
updatedAt: new Date(),
});
const yesterdayUser = AdminUser.create({
id: 'user-2',
email: 'user2@example.com',
displayName: 'User 2',
roles: ['user'],
status: 'active',
createdAt: new Date(Date.now() - 86400000),
updatedAt: new Date(Date.now() - 86400000),
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockResolvedValue({ users: [todayUser, yesterdayUser] });
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isOk()).toBe(true);
const stats = result.unwrap();
expect(stats.newUsersToday).toBe(1);
});
it('should calculate role distribution correctly', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['owner'],
status: 'active',
});
const user1 = AdminUser.create({
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['user'],
status: 'active',
createdAt: new Date(),
updatedAt: new Date(),
});
const user2 = AdminUser.create({
id: 'user-2',
email: 'user2@example.com',
displayName: 'User 2',
roles: ['admin'],
status: 'active',
createdAt: new Date(),
updatedAt: new Date(),
});
const user3 = AdminUser.create({
id: 'user-3',
email: 'user3@example.com',
displayName: 'User 3',
roles: ['owner'],
status: 'active',
createdAt: new Date(),
updatedAt: new Date(),
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockResolvedValue({ users: [user1, user2, user3] });
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isOk()).toBe(true);
const stats = result.unwrap();
expect(stats.roleDistribution).toHaveLength(3);
expect(stats.roleDistribution).toContainEqual({
label: 'Owner',
value: 2,
color: 'text-purple-500',
});
expect(stats.roleDistribution).toContainEqual({
label: 'Admin',
value: 1,
color: 'text-blue-500',
});
expect(stats.roleDistribution).toContainEqual({
label: 'User',
value: 1,
color: 'text-gray-500',
});
});
it('should handle users with multiple roles', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['owner'],
status: 'active',
});
const user1 = AdminUser.create({
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['user', 'admin'],
status: 'active',
createdAt: new Date(),
updatedAt: new Date(),
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockResolvedValue({ users: [user1] });
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isOk()).toBe(true);
const stats = result.unwrap();
expect(stats.roleDistribution).toHaveLength(2);
expect(stats.roleDistribution).toContainEqual({
label: 'User',
value: 1,
color: 'text-gray-500',
});
expect(stats.roleDistribution).toContainEqual({
label: 'Admin',
value: 1,
color: 'text-blue-500',
});
});
it('should calculate user growth for last 7 days', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['owner'],
status: 'active',
});
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const twoDaysAgo = new Date(today);
twoDaysAgo.setDate(twoDaysAgo.getDate() - 2);
const user1 = AdminUser.create({
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['user'],
status: 'active',
createdAt: today,
updatedAt: today,
});
const user2 = AdminUser.create({
id: 'user-2',
email: 'user2@example.com',
displayName: 'User 2',
roles: ['user'],
status: 'active',
createdAt: yesterday,
updatedAt: yesterday,
});
const user3 = AdminUser.create({
id: 'user-3',
email: 'user3@example.com',
displayName: 'User 3',
roles: ['user'],
status: 'active',
createdAt: twoDaysAgo,
updatedAt: twoDaysAgo,
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockResolvedValue({ users: [user1, user2, user3] });
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isOk()).toBe(true);
const stats = result.unwrap();
expect(stats.userGrowth).toHaveLength(7);
// Check that today has 1 user
const todayEntry = stats.userGrowth[6];
expect(todayEntry.value).toBe(1);
// Check that yesterday has 1 user
const yesterdayEntry = stats.userGrowth[5];
expect(yesterdayEntry.value).toBe(1);
// Check that two days ago has 1 user
const twoDaysAgoEntry = stats.userGrowth[4];
expect(twoDaysAgoEntry.value).toBe(1);
});
it('should calculate activity timeline for last 7 days', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['owner'],
status: 'active',
});
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const newUser = AdminUser.create({
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['user'],
status: 'active',
createdAt: today,
updatedAt: today,
});
const recentLoginUser = AdminUser.create({
id: 'user-2',
email: 'user2@example.com',
displayName: 'User 2',
roles: ['user'],
status: 'active',
createdAt: yesterday,
updatedAt: yesterday,
lastLoginAt: new Date(Date.now() - 3600000), // 1 hour ago
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockResolvedValue({ users: [newUser, recentLoginUser] });
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isOk()).toBe(true);
const stats = result.unwrap();
expect(stats.activityTimeline).toHaveLength(7);
// Check today's entry
const todayEntry = stats.activityTimeline[6];
expect(todayEntry.newUsers).toBe(1);
expect(todayEntry.logins).toBe(1);
// Check yesterday's entry
const yesterdayEntry = stats.activityTimeline[5];
expect(yesterdayEntry.newUsers).toBe(0);
expect(yesterdayEntry.logins).toBe(0);
});
it('should handle repository errors gracefully', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['owner'],
status: 'active',
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockRejectedValue(new Error('Database connection failed'));
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isErr()).toBe(true);
const error = result.unwrapErr();
expect(error.code).toBe('REPOSITORY_ERROR');
expect(error.details.message).toBe('Database connection failed');
});
it('should handle non-Error exceptions', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['owner'],
status: 'active',
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockRejectedValue('String error');
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isErr()).toBe(true);
const error = result.unwrapErr();
expect(error.code).toBe('REPOSITORY_ERROR');
expect(error.details.message).toBe('Failed to get dashboard stats');
});
it('should work with owner role', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['owner'],
status: 'active',
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockResolvedValue({ users: [] });
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isOk()).toBe(true);
});
it('should work with admin role', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['admin'],
status: 'active',
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockResolvedValue({ users: [] });
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isOk()).toBe(true);
});
it('should reject user role', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['user'],
status: 'active',
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockResolvedValue({ users: [] });
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isErr()).toBe(true);
const error = result.unwrapErr();
expect(error.code).toBe('AUTHORIZATION_ERROR');
});
it('should handle suspended actor', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['owner'],
status: 'suspended',
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockResolvedValue({ users: [] });
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isErr()).toBe(true);
const error = result.unwrapErr();
expect(error.code).toBe('AUTHORIZATION_ERROR');
});
it('should handle deleted actor', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['owner'],
status: 'deleted',
});
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockResolvedValue({ users: [] });
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isErr()).toBe(true);
const error = result.unwrapErr();
expect(error.code).toBe('AUTHORIZATION_ERROR');
});
it('should handle large number of users efficiently', async () => {
// Arrange
const actor = AdminUser.create({
id: 'actor-1',
email: 'actor@example.com',
displayName: 'Actor',
roles: ['owner'],
status: 'active',
});
const users = Array.from({ length: 1000 }, (_, i) =>
AdminUser.create({
id: `user-${i}`,
email: `user${i}@example.com`,
displayName: `User ${i}`,
roles: i % 3 === 0 ? ['owner'] : i % 3 === 1 ? ['admin'] : ['user'],
status: i % 4 === 0 ? 'suspended' : i % 4 === 1 ? 'deleted' : 'active',
createdAt: new Date(Date.now() - i * 3600000),
updatedAt: new Date(Date.now() - i * 3600000),
lastLoginAt: i % 10 === 0 ? new Date(Date.now() - i * 3600000) : undefined,
})
);
mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockResolvedValue({ users });
// Act
const result = await useCase.execute({ actorId: 'actor-1' });
// Assert
expect(result.isOk()).toBe(true);
const stats = result.unwrap();
expect(stats.totalUsers).toBe(1000);
expect(stats.activeUsers).toBe(500);
expect(stats.suspendedUsers).toBe(250);
expect(stats.deletedUsers).toBe(250);
expect(stats.systemAdmins).toBe(334); // owner + admin
expect(stats.recentLogins).toBe(100); // 10% of users
expect(stats.userGrowth).toHaveLength(7);
expect(stats.roleDistribution).toHaveLength(3);
expect(stats.activityTimeline).toHaveLength(7);
});
});
});
});

View File

@@ -0,0 +1,234 @@
import { Test, TestingModule } from '@nestjs/testing';
import { describe, expect, it, vi } from 'vitest';
import { AnalyticsProviders } from './AnalyticsProviders';
import { AnalyticsService } from './AnalyticsService';
import { RecordPageViewPresenter } from './presenters/RecordPageViewPresenter';
import { RecordEngagementPresenter } from './presenters/RecordEngagementPresenter';
import { GetDashboardDataPresenter } from './presenters/GetDashboardDataPresenter';
import { GetAnalyticsMetricsPresenter } from './presenters/GetAnalyticsMetricsPresenter';
import { RecordPageViewUseCase } from '@core/analytics/application/use-cases/RecordPageViewUseCase';
import { RecordEngagementUseCase } from '@core/analytics/application/use-cases/RecordEngagementUseCase';
import { GetDashboardDataUseCase } from '@core/analytics/application/use-cases/GetDashboardDataUseCase';
import { GetAnalyticsMetricsUseCase } from '@core/analytics/application/use-cases/GetAnalyticsMetricsUseCase';
import { ANALYTICS_ENGAGEMENT_REPOSITORY_TOKEN, ANALYTICS_PAGE_VIEW_REPOSITORY_TOKEN } from '../../persistence/analytics/AnalyticsPersistenceTokens';
describe('AnalyticsProviders', () => {
describe('AnalyticsService', () => {
it('should be defined as a provider', () => {
const provider = AnalyticsProviders.find(p => p === AnalyticsService);
expect(provider).toBeDefined();
});
});
describe('RecordPageViewPresenter', () => {
it('should be defined as a provider', () => {
const provider = AnalyticsProviders.find(p => p === RecordPageViewPresenter);
expect(provider).toBeDefined();
});
});
describe('RecordEngagementPresenter', () => {
it('should be defined as a provider', () => {
const provider = AnalyticsProviders.find(p => p === RecordEngagementPresenter);
expect(provider).toBeDefined();
});
});
describe('GetDashboardDataPresenter', () => {
it('should be defined as a provider', () => {
const provider = AnalyticsProviders.find(p => p === GetDashboardDataPresenter);
expect(provider).toBeDefined();
});
});
describe('GetAnalyticsMetricsPresenter', () => {
it('should be defined as a provider', () => {
const provider = AnalyticsProviders.find(p => p === GetAnalyticsMetricsPresenter);
expect(provider).toBeDefined();
});
});
describe('RecordPageViewUseCase', () => {
it('should be defined as a provider with useFactory', () => {
const provider = AnalyticsProviders.find(
p => typeof p === 'object' && 'provide' in p && p.provide === RecordPageViewUseCase,
);
expect(provider).toBeDefined();
expect(provider).toHaveProperty('useFactory');
expect(provider).toHaveProperty('inject');
});
it('should inject correct dependencies', () => {
const provider = AnalyticsProviders.find(
p => typeof p === 'object' && 'provide' in p && p.provide === RecordPageViewUseCase,
) as { inject: string[] };
expect(provider.inject).toEqual([ANALYTICS_PAGE_VIEW_REPOSITORY_TOKEN, 'Logger']);
});
});
describe('RecordEngagementUseCase', () => {
it('should be defined as a provider with useFactory', () => {
const provider = AnalyticsProviders.find(
p => typeof p === 'object' && 'provide' in p && p.provide === RecordEngagementUseCase,
);
expect(provider).toBeDefined();
expect(provider).toHaveProperty('useFactory');
expect(provider).toHaveProperty('inject');
});
it('should inject correct dependencies', () => {
const provider = AnalyticsProviders.find(
p => typeof p === 'object' && 'provide' in p && p.provide === RecordEngagementUseCase,
) as { inject: string[] };
expect(provider.inject).toEqual([ANALYTICS_ENGAGEMENT_REPOSITORY_TOKEN, 'Logger']);
});
});
describe('GetDashboardDataUseCase', () => {
it('should be defined as a provider with useFactory', () => {
const provider = AnalyticsProviders.find(
p => typeof p === 'object' && 'provide' in p && p.provide === GetDashboardDataUseCase,
);
expect(provider).toBeDefined();
expect(provider).toHaveProperty('useFactory');
expect(provider).toHaveProperty('inject');
});
it('should inject correct dependencies', () => {
const provider = AnalyticsProviders.find(
p => typeof p === 'object' && 'provide' in p && p.provide === GetDashboardDataUseCase,
) as { inject: string[] };
expect(provider.inject).toEqual(['Logger']);
});
});
describe('GetAnalyticsMetricsUseCase', () => {
it('should be defined as a provider with useFactory', () => {
const provider = AnalyticsProviders.find(
p => typeof p === 'object' && 'provide' in p && p.provide === GetAnalyticsMetricsUseCase,
);
expect(provider).toBeDefined();
expect(provider).toHaveProperty('useFactory');
expect(provider).toHaveProperty('inject');
});
it('should inject correct dependencies', () => {
const provider = AnalyticsProviders.find(
p => typeof p === 'object' && 'provide' in p && p.provide === GetAnalyticsMetricsUseCase,
) as { inject: string[] };
expect(provider.inject).toEqual(['Logger', ANALYTICS_PAGE_VIEW_REPOSITORY_TOKEN]);
});
});
describe('useFactory functions', () => {
it('should create RecordPageViewUseCase with correct dependencies', async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
...AnalyticsProviders,
{
provide: ANALYTICS_PAGE_VIEW_REPOSITORY_TOKEN,
useValue: {
save: vi.fn(),
findById: vi.fn(),
},
},
{
provide: 'Logger',
useValue: {
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
},
},
],
}).compile();
const useCase = module.get<RecordPageViewUseCase>(RecordPageViewUseCase);
expect(useCase).toBeDefined();
expect(useCase).toBeInstanceOf(RecordPageViewUseCase);
});
it('should create RecordEngagementUseCase with correct dependencies', async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
...AnalyticsProviders,
{
provide: ANALYTICS_ENGAGEMENT_REPOSITORY_TOKEN,
useValue: {
save: vi.fn(),
findById: vi.fn(),
},
},
{
provide: 'Logger',
useValue: {
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
},
},
],
}).compile();
const useCase = module.get<RecordEngagementUseCase>(RecordEngagementUseCase);
expect(useCase).toBeDefined();
expect(useCase).toBeInstanceOf(RecordEngagementUseCase);
});
it('should create GetDashboardDataUseCase with correct dependencies', async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
...AnalyticsProviders,
{
provide: 'Logger',
useValue: {
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
},
},
],
}).compile();
const useCase = module.get<GetDashboardDataUseCase>(GetDashboardDataUseCase);
expect(useCase).toBeDefined();
expect(useCase).toBeInstanceOf(GetDashboardDataUseCase);
});
it('should create GetAnalyticsMetricsUseCase with correct dependencies', async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
...AnalyticsProviders,
{
provide: ANALYTICS_PAGE_VIEW_REPOSITORY_TOKEN,
useValue: {
save: vi.fn(),
findById: vi.fn(),
findAll: vi.fn(),
},
},
{
provide: 'Logger',
useValue: {
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
},
},
],
}).compile();
const useCase = module.get<GetAnalyticsMetricsUseCase>(GetAnalyticsMetricsUseCase);
expect(useCase).toBeDefined();
expect(useCase).toBeInstanceOf(GetAnalyticsMetricsUseCase);
});
});
});

View File

@@ -6,7 +6,7 @@ import { Provider } from '@nestjs/common';
import {
ANALYTICS_ENGAGEMENT_REPOSITORY_TOKEN,
ANALYTICS_PAGE_VIEW_REPOSITORY_TOKEN,
} from '../../persistence/analytics/AnalyticsPersistenceTokens';
} from '../../../../persistence/analytics/AnalyticsPersistenceTokens';
const LOGGER_TOKEN = 'Logger';

View File

@@ -0,0 +1,312 @@
import { GetAnalyticsMetricsOutputDTO } from './GetAnalyticsMetricsOutputDTO';
describe('GetAnalyticsMetricsOutputDTO', () => {
describe('TDD - Test First', () => {
it('should create valid DTO with all fields', () => {
// Arrange & Act
const dto = new GetAnalyticsMetricsOutputDTO();
dto.pageViews = 1000;
dto.uniqueVisitors = 500;
dto.averageSessionDuration = 300;
dto.bounceRate = 0.4;
// Assert
expect(dto.pageViews).toBe(1000);
expect(dto.uniqueVisitors).toBe(500);
expect(dto.averageSessionDuration).toBe(300);
expect(dto.bounceRate).toBe(0.4);
});
it('should handle zero values', () => {
// Arrange & Act
const dto = new GetAnalyticsMetricsOutputDTO();
dto.pageViews = 0;
dto.uniqueVisitors = 0;
dto.averageSessionDuration = 0;
dto.bounceRate = 0;
// Assert
expect(dto.pageViews).toBe(0);
expect(dto.uniqueVisitors).toBe(0);
expect(dto.averageSessionDuration).toBe(0);
expect(dto.bounceRate).toBe(0);
});
it('should handle large numbers', () => {
// Arrange & Act
const dto = new GetAnalyticsMetricsOutputDTO();
dto.pageViews = 1000000;
dto.uniqueVisitors = 500000;
dto.averageSessionDuration = 3600;
dto.bounceRate = 0.95;
// Assert
expect(dto.pageViews).toBe(1000000);
expect(dto.uniqueVisitors).toBe(500000);
expect(dto.averageSessionDuration).toBe(3600);
expect(dto.bounceRate).toBe(0.95);
});
it('should handle single digit values', () => {
// Arrange & Act
const dto = new GetAnalyticsMetricsOutputDTO();
dto.pageViews = 1;
dto.uniqueVisitors = 1;
dto.averageSessionDuration = 1;
dto.bounceRate = 0.1;
// Assert
expect(dto.pageViews).toBe(1);
expect(dto.uniqueVisitors).toBe(1);
expect(dto.averageSessionDuration).toBe(1);
expect(dto.bounceRate).toBe(0.1);
});
it('should handle unique visitors greater than page views', () => {
// Arrange & Act
const dto = new GetAnalyticsMetricsOutputDTO();
dto.pageViews = 100;
dto.uniqueVisitors = 150;
dto.averageSessionDuration = 300;
dto.bounceRate = 0.4;
// Assert
expect(dto.pageViews).toBe(100);
expect(dto.uniqueVisitors).toBe(150);
});
it('should handle zero unique visitors', () => {
// Arrange & Act
const dto = new GetAnalyticsMetricsOutputDTO();
dto.pageViews = 100;
dto.uniqueVisitors = 0;
dto.averageSessionDuration = 300;
dto.bounceRate = 0.4;
// Assert
expect(dto.uniqueVisitors).toBe(0);
});
it('should handle zero page views', () => {
// Arrange & Act
const dto = new GetAnalyticsMetricsOutputDTO();
dto.pageViews = 0;
dto.uniqueVisitors = 0;
dto.averageSessionDuration = 300;
dto.bounceRate = 0.4;
// Assert
expect(dto.pageViews).toBe(0);
});
it('should handle zero average session duration', () => {
// Arrange & Act
const dto = new GetAnalyticsMetricsOutputDTO();
dto.pageViews = 100;
dto.uniqueVisitors = 50;
dto.averageSessionDuration = 0;
dto.bounceRate = 0.4;
// Assert
expect(dto.averageSessionDuration).toBe(0);
});
it('should handle zero bounce rate', () => {
// Arrange & Act
const dto = new GetAnalyticsMetricsOutputDTO();
dto.pageViews = 100;
dto.uniqueVisitors = 50;
dto.averageSessionDuration = 300;
dto.bounceRate = 0;
// Assert
expect(dto.bounceRate).toBe(0);
});
it('should handle bounce rate of 1.0', () => {
// Arrange & Act
const dto = new GetAnalyticsMetricsOutputDTO();
dto.pageViews = 100;
dto.uniqueVisitors = 50;
dto.averageSessionDuration = 300;
dto.bounceRate = 1.0;
// Assert
expect(dto.bounceRate).toBe(1.0);
});
it('should handle very large numbers', () => {
// Arrange & Act
const dto = new GetAnalyticsMetricsOutputDTO();
dto.pageViews = 999999999;
dto.uniqueVisitors = 888888888;
dto.averageSessionDuration = 777777777;
dto.bounceRate = 0.999999;
// Assert
expect(dto.pageViews).toBe(999999999);
expect(dto.uniqueVisitors).toBe(888888888);
expect(dto.averageSessionDuration).toBe(777777777);
expect(dto.bounceRate).toBe(0.999999);
});
it('should handle decimal numbers', () => {
// Arrange & Act
const dto = new GetAnalyticsMetricsOutputDTO();
dto.pageViews = 100.5;
dto.uniqueVisitors = 50.7;
dto.averageSessionDuration = 300.3;
dto.bounceRate = 0.45;
// Assert
expect(dto.pageViews).toBe(100.5);
expect(dto.uniqueVisitors).toBe(50.7);
expect(dto.averageSessionDuration).toBe(300.3);
expect(dto.bounceRate).toBe(0.45);
});
it('should handle negative numbers', () => {
// Arrange & Act
const dto = new GetAnalyticsMetricsOutputDTO();
dto.pageViews = -100;
dto.uniqueVisitors = -50;
dto.averageSessionDuration = -300;
dto.bounceRate = -0.4;
// Assert
expect(dto.pageViews).toBe(-100);
expect(dto.uniqueVisitors).toBe(-50);
expect(dto.averageSessionDuration).toBe(-300);
expect(dto.bounceRate).toBe(-0.4);
});
it('should handle scientific notation', () => {
// Arrange & Act
const dto = new GetAnalyticsMetricsOutputDTO();
dto.pageViews = 1e6;
dto.uniqueVisitors = 5e5;
dto.averageSessionDuration = 3e2;
dto.bounceRate = 4e-1;
// Assert
expect(dto.pageViews).toBe(1000000);
expect(dto.uniqueVisitors).toBe(500000);
expect(dto.averageSessionDuration).toBe(300);
expect(dto.bounceRate).toBe(0.4);
});
it('should handle maximum safe integer', () => {
// Arrange & Act
const dto = new GetAnalyticsMetricsOutputDTO();
dto.pageViews = Number.MAX_SAFE_INTEGER;
dto.uniqueVisitors = Number.MAX_SAFE_INTEGER;
dto.averageSessionDuration = Number.MAX_SAFE_INTEGER;
dto.bounceRate = 0.99;
// Assert
expect(dto.pageViews).toBe(Number.MAX_SAFE_INTEGER);
expect(dto.uniqueVisitors).toBe(Number.MAX_SAFE_INTEGER);
expect(dto.averageSessionDuration).toBe(Number.MAX_SAFE_INTEGER);
expect(dto.bounceRate).toBe(0.99);
});
it('should handle minimum safe integer', () => {
// Arrange & Act
const dto = new GetAnalyticsMetricsOutputDTO();
dto.pageViews = Number.MIN_SAFE_INTEGER;
dto.uniqueVisitors = Number.MIN_SAFE_INTEGER;
dto.averageSessionDuration = Number.MIN_SAFE_INTEGER;
dto.bounceRate = -0.99;
// Assert
expect(dto.pageViews).toBe(Number.MIN_SAFE_INTEGER);
expect(dto.uniqueVisitors).toBe(Number.MIN_SAFE_INTEGER);
expect(dto.averageSessionDuration).toBe(Number.MIN_SAFE_INTEGER);
expect(dto.bounceRate).toBe(-0.99);
});
it('should handle Infinity for page views', () => {
// Arrange & Act
const dto = new GetAnalyticsMetricsOutputDTO();
dto.pageViews = Infinity;
dto.uniqueVisitors = 500;
dto.averageSessionDuration = 300;
dto.bounceRate = 0.4;
// Assert
expect(dto.pageViews).toBe(Infinity);
});
it('should handle Infinity for unique visitors', () => {
// Arrange & Act
const dto = new GetAnalyticsMetricsOutputDTO();
dto.pageViews = 1000;
dto.uniqueVisitors = Infinity;
dto.averageSessionDuration = 300;
dto.bounceRate = 0.4;
// Assert
expect(dto.uniqueVisitors).toBe(Infinity);
});
it('should handle Infinity for average session duration', () => {
// Arrange & Act
const dto = new GetAnalyticsMetricsOutputDTO();
dto.pageViews = 1000;
dto.uniqueVisitors = 500;
dto.averageSessionDuration = Infinity;
dto.bounceRate = 0.4;
// Assert
expect(dto.averageSessionDuration).toBe(Infinity);
});
it('should handle NaN for page views', () => {
// Arrange & Act
const dto = new GetAnalyticsMetricsOutputDTO();
dto.pageViews = NaN;
dto.uniqueVisitors = 500;
dto.averageSessionDuration = 300;
dto.bounceRate = 0.4;
// Assert
expect(dto.pageViews).toBeNaN();
});
it('should handle NaN for unique visitors', () => {
// Arrange & Act
const dto = new GetAnalyticsMetricsOutputDTO();
dto.pageViews = 1000;
dto.uniqueVisitors = NaN;
dto.averageSessionDuration = 300;
dto.bounceRate = 0.4;
// Assert
expect(dto.uniqueVisitors).toBeNaN();
});
it('should handle NaN for average session duration', () => {
// Arrange & Act
const dto = new GetAnalyticsMetricsOutputDTO();
dto.pageViews = 1000;
dto.uniqueVisitors = 500;
dto.averageSessionDuration = NaN;
dto.bounceRate = 0.4;
// Assert
expect(dto.averageSessionDuration).toBeNaN();
});
it('should handle NaN for bounce rate', () => {
// Arrange & Act
const dto = new GetAnalyticsMetricsOutputDTO();
dto.pageViews = 1000;
dto.uniqueVisitors = 500;
dto.averageSessionDuration = 300;
dto.bounceRate = NaN;
// Assert
expect(dto.bounceRate).toBeNaN();
});
});
});

View File

@@ -0,0 +1,246 @@
import { GetDashboardDataOutputDTO } from './GetDashboardDataOutputDTO';
describe('GetDashboardDataOutputDTO', () => {
describe('TDD - Test First', () => {
it('should create valid DTO with all fields', () => {
// Arrange & Act
const dto = new GetDashboardDataOutputDTO();
dto.totalUsers = 100;
dto.activeUsers = 50;
dto.totalRaces = 20;
dto.totalLeagues = 5;
// Assert
expect(dto.totalUsers).toBe(100);
expect(dto.activeUsers).toBe(50);
expect(dto.totalRaces).toBe(20);
expect(dto.totalLeagues).toBe(5);
});
it('should handle zero values', () => {
// Arrange & Act
const dto = new GetDashboardDataOutputDTO();
dto.totalUsers = 0;
dto.activeUsers = 0;
dto.totalRaces = 0;
dto.totalLeagues = 0;
// Assert
expect(dto.totalUsers).toBe(0);
expect(dto.activeUsers).toBe(0);
expect(dto.totalRaces).toBe(0);
expect(dto.totalLeagues).toBe(0);
});
it('should handle large numbers', () => {
// Arrange & Act
const dto = new GetDashboardDataOutputDTO();
dto.totalUsers = 1000000;
dto.activeUsers = 500000;
dto.totalRaces = 100000;
dto.totalLeagues = 10000;
// Assert
expect(dto.totalUsers).toBe(1000000);
expect(dto.activeUsers).toBe(500000);
expect(dto.totalRaces).toBe(100000);
expect(dto.totalLeagues).toBe(10000);
});
it('should handle single digit values', () => {
// Arrange & Act
const dto = new GetDashboardDataOutputDTO();
dto.totalUsers = 1;
dto.activeUsers = 1;
dto.totalRaces = 1;
dto.totalLeagues = 1;
// Assert
expect(dto.totalUsers).toBe(1);
expect(dto.activeUsers).toBe(1);
expect(dto.totalRaces).toBe(1);
expect(dto.totalLeagues).toBe(1);
});
it('should handle active users greater than total users', () => {
// Arrange & Act
const dto = new GetDashboardDataOutputDTO();
dto.totalUsers = 100;
dto.activeUsers = 150;
dto.totalRaces = 20;
dto.totalLeagues = 5;
// Assert
expect(dto.totalUsers).toBe(100);
expect(dto.activeUsers).toBe(150);
});
it('should handle zero active users', () => {
// Arrange & Act
const dto = new GetDashboardDataOutputDTO();
dto.totalUsers = 100;
dto.activeUsers = 0;
dto.totalRaces = 20;
dto.totalLeagues = 5;
// Assert
expect(dto.activeUsers).toBe(0);
});
it('should handle zero total users', () => {
// Arrange & Act
const dto = new GetDashboardDataOutputDTO();
dto.totalUsers = 0;
dto.activeUsers = 0;
dto.totalRaces = 20;
dto.totalLeagues = 5;
// Assert
expect(dto.totalUsers).toBe(0);
});
it('should handle zero races', () => {
// Arrange & Act
const dto = new GetDashboardDataOutputDTO();
dto.totalUsers = 100;
dto.activeUsers = 50;
dto.totalRaces = 0;
dto.totalLeagues = 5;
// Assert
expect(dto.totalRaces).toBe(0);
});
it('should handle zero leagues', () => {
// Arrange & Act
const dto = new GetDashboardDataOutputDTO();
dto.totalUsers = 100;
dto.activeUsers = 50;
dto.totalRaces = 20;
dto.totalLeagues = 0;
// Assert
expect(dto.totalLeagues).toBe(0);
});
it('should handle very large numbers', () => {
// Arrange & Act
const dto = new GetDashboardDataOutputDTO();
dto.totalUsers = 999999999;
dto.activeUsers = 888888888;
dto.totalRaces = 777777777;
dto.totalLeagues = 666666666;
// Assert
expect(dto.totalUsers).toBe(999999999);
expect(dto.activeUsers).toBe(888888888);
expect(dto.totalRaces).toBe(777777777);
expect(dto.totalLeagues).toBe(666666666);
});
it('should handle decimal numbers', () => {
// Arrange & Act
const dto = new GetDashboardDataOutputDTO();
dto.totalUsers = 100.5;
dto.activeUsers = 50.7;
dto.totalRaces = 20.3;
dto.totalLeagues = 5.9;
// Assert
expect(dto.totalUsers).toBe(100.5);
expect(dto.activeUsers).toBe(50.7);
expect(dto.totalRaces).toBe(20.3);
expect(dto.totalLeagues).toBe(5.9);
});
it('should handle negative numbers', () => {
// Arrange & Act
const dto = new GetDashboardDataOutputDTO();
dto.totalUsers = -100;
dto.activeUsers = -50;
dto.totalRaces = -20;
dto.totalLeagues = -5;
// Assert
expect(dto.totalUsers).toBe(-100);
expect(dto.activeUsers).toBe(-50);
expect(dto.totalRaces).toBe(-20);
expect(dto.totalLeagues).toBe(-5);
});
it('should handle scientific notation', () => {
// Arrange & Act
const dto = new GetDashboardDataOutputDTO();
dto.totalUsers = 1e6;
dto.activeUsers = 5e5;
dto.totalRaces = 2e4;
dto.totalLeagues = 5e3;
// Assert
expect(dto.totalUsers).toBe(1000000);
expect(dto.activeUsers).toBe(500000);
expect(dto.totalRaces).toBe(20000);
expect(dto.totalLeagues).toBe(5000);
});
it('should handle maximum safe integer', () => {
// Arrange & Act
const dto = new GetDashboardDataOutputDTO();
dto.totalUsers = Number.MAX_SAFE_INTEGER;
dto.activeUsers = Number.MAX_SAFE_INTEGER;
dto.totalRaces = Number.MAX_SAFE_INTEGER;
dto.totalLeagues = Number.MAX_SAFE_INTEGER;
// Assert
expect(dto.totalUsers).toBe(Number.MAX_SAFE_INTEGER);
expect(dto.activeUsers).toBe(Number.MAX_SAFE_INTEGER);
expect(dto.totalRaces).toBe(Number.MAX_SAFE_INTEGER);
expect(dto.totalLeagues).toBe(Number.MAX_SAFE_INTEGER);
});
it('should handle minimum safe integer', () => {
// Arrange & Act
const dto = new GetDashboardDataOutputDTO();
dto.totalUsers = Number.MIN_SAFE_INTEGER;
dto.activeUsers = Number.MIN_SAFE_INTEGER;
dto.totalRaces = Number.MIN_SAFE_INTEGER;
dto.totalLeagues = Number.MIN_SAFE_INTEGER;
// Assert
expect(dto.totalUsers).toBe(Number.MIN_SAFE_INTEGER);
expect(dto.activeUsers).toBe(Number.MIN_SAFE_INTEGER);
expect(dto.totalRaces).toBe(Number.MIN_SAFE_INTEGER);
expect(dto.totalLeagues).toBe(Number.MIN_SAFE_INTEGER);
});
it('should handle Infinity', () => {
// Arrange & Act
const dto = new GetDashboardDataOutputDTO();
dto.totalUsers = Infinity;
dto.activeUsers = Infinity;
dto.totalRaces = Infinity;
dto.totalLeagues = Infinity;
// Assert
expect(dto.totalUsers).toBe(Infinity);
expect(dto.activeUsers).toBe(Infinity);
expect(dto.totalRaces).toBe(Infinity);
expect(dto.totalLeagues).toBe(Infinity);
});
it('should handle NaN', () => {
// Arrange & Act
const dto = new GetDashboardDataOutputDTO();
dto.totalUsers = NaN;
dto.activeUsers = NaN;
dto.totalRaces = NaN;
dto.totalLeagues = NaN;
// Assert
expect(dto.totalUsers).toBeNaN();
expect(dto.activeUsers).toBeNaN();
expect(dto.totalRaces).toBeNaN();
expect(dto.totalLeagues).toBeNaN();
});
});
});

View File

@@ -0,0 +1,340 @@
import { RecordEngagementInputDTO } from './RecordEngagementInputDTO';
import { EngagementAction, EngagementEntityType } from '@core/analytics/domain/types/EngagementEvent';
describe('RecordEngagementInputDTO', () => {
describe('TDD - Test First', () => {
it('should create valid DTO with all fields', () => {
// Arrange & Act
const dto = new RecordEngagementInputDTO();
dto.action = EngagementAction.CLICK_SPONSOR_LOGO;
dto.entityType = EngagementEntityType.RACE;
dto.entityId = 'race-123';
dto.actorId = 'actor-456';
dto.actorType = 'driver';
dto.sessionId = 'session-789';
dto.metadata = { key: 'value', count: 5 };
// Assert
expect(dto.action).toBe(EngagementAction.CLICK_SPONSOR_LOGO);
expect(dto.entityType).toBe(EngagementEntityType.RACE);
expect(dto.entityId).toBe('race-123');
expect(dto.actorId).toBe('actor-456');
expect(dto.actorType).toBe('driver');
expect(dto.sessionId).toBe('session-789');
expect(dto.metadata).toEqual({ key: 'value', count: 5 });
});
it('should create DTO with required fields only', () => {
// Arrange & Act
const dto = new RecordEngagementInputDTO();
dto.action = EngagementAction.CLICK_SPONSOR_LOGO;
dto.entityType = EngagementEntityType.RACE;
dto.entityId = 'race-123';
dto.actorType = 'anonymous';
dto.sessionId = 'session-456';
// Assert
expect(dto.action).toBe(EngagementAction.CLICK_SPONSOR_LOGO);
expect(dto.entityType).toBe(EngagementEntityType.RACE);
expect(dto.entityId).toBe('race-123');
expect(dto.actorType).toBe('anonymous');
expect(dto.sessionId).toBe('session-456');
expect(dto.actorId).toBeUndefined();
expect(dto.metadata).toBeUndefined();
});
it('should handle CLICK_SPONSOR_LOGO action', () => {
// Arrange & Act
const dto = new RecordEngagementInputDTO();
dto.action = EngagementAction.CLICK_SPONSOR_LOGO;
dto.entityType = EngagementEntityType.RACE;
dto.entityId = 'race-123';
dto.actorType = 'anonymous';
dto.sessionId = 'session-456';
// Assert
expect(dto.action).toBe(EngagementAction.CLICK_SPONSOR_LOGO);
});
it('should handle CLICK_SPONSOR_URL action', () => {
// Arrange & Act
const dto = new RecordEngagementInputDTO();
dto.action = EngagementAction.CLICK_SPONSOR_URL;
dto.entityType = EngagementEntityType.RACE;
dto.entityId = 'race-123';
dto.actorType = 'anonymous';
dto.sessionId = 'session-456';
// Assert
expect(dto.action).toBe(EngagementAction.CLICK_SPONSOR_URL);
});
it('should handle RACE entity type', () => {
// Arrange & Act
const dto = new RecordEngagementInputDTO();
dto.action = EngagementAction.CLICK_SPONSOR_LOGO;
dto.entityType = EngagementEntityType.RACE;
dto.entityId = 'race-123';
dto.actorType = 'anonymous';
dto.sessionId = 'session-456';
// Assert
expect(dto.entityType).toBe(EngagementEntityType.RACE);
});
it('should handle LEAGUE entity type', () => {
// Arrange & Act
const dto = new RecordEngagementInputDTO();
dto.action = EngagementAction.CLICK_SPONSOR_LOGO;
dto.entityType = EngagementEntityType.LEAGUE;
dto.entityId = 'league-123';
dto.actorType = 'anonymous';
dto.sessionId = 'session-456';
// Assert
expect(dto.entityType).toBe(EngagementEntityType.LEAGUE);
});
it('should handle DRIVER entity type', () => {
// Arrange & Act
const dto = new RecordEngagementInputDTO();
dto.action = EngagementAction.CLICK_SPONSOR_LOGO;
dto.entityType = EngagementEntityType.DRIVER;
dto.entityId = 'driver-123';
dto.actorType = 'anonymous';
dto.sessionId = 'session-456';
// Assert
expect(dto.entityType).toBe(EngagementEntityType.DRIVER);
});
it('should handle TEAM entity type', () => {
// Arrange & Act
const dto = new RecordEngagementInputDTO();
dto.action = EngagementAction.CLICK_SPONSOR_LOGO;
dto.entityType = EngagementEntityType.TEAM;
dto.entityId = 'team-123';
dto.actorType = 'anonymous';
dto.sessionId = 'session-456';
// Assert
expect(dto.entityType).toBe(EngagementEntityType.TEAM);
});
it('should handle anonymous actor type', () => {
// Arrange & Act
const dto = new RecordEngagementInputDTO();
dto.action = EngagementAction.CLICK_SPONSOR_LOGO;
dto.entityType = EngagementEntityType.RACE;
dto.entityId = 'race-123';
dto.actorType = 'anonymous';
dto.sessionId = 'session-456';
// Assert
expect(dto.actorType).toBe('anonymous');
});
it('should handle driver actor type', () => {
// Arrange & Act
const dto = new RecordEngagementInputDTO();
dto.action = EngagementAction.CLICK_SPONSOR_LOGO;
dto.entityType = EngagementEntityType.RACE;
dto.entityId = 'race-123';
dto.actorType = 'driver';
dto.sessionId = 'session-456';
// Assert
expect(dto.actorType).toBe('driver');
});
it('should handle sponsor actor type', () => {
// Arrange & Act
const dto = new RecordEngagementInputDTO();
dto.action = EngagementAction.CLICK_SPONSOR_LOGO;
dto.entityType = EngagementEntityType.RACE;
dto.entityId = 'race-123';
dto.actorType = 'sponsor';
dto.sessionId = 'session-456';
// Assert
expect(dto.actorType).toBe('sponsor');
});
it('should handle empty metadata', () => {
// Arrange & Act
const dto = new RecordEngagementInputDTO();
dto.action = EngagementAction.CLICK_SPONSOR_LOGO;
dto.entityType = EngagementEntityType.RACE;
dto.entityId = 'race-123';
dto.actorType = 'anonymous';
dto.sessionId = 'session-456';
dto.metadata = {};
// Assert
expect(dto.metadata).toEqual({});
});
it('should handle metadata with multiple keys', () => {
// Arrange & Act
const dto = new RecordEngagementInputDTO();
dto.action = EngagementAction.CLICK_SPONSOR_LOGO;
dto.entityType = EngagementEntityType.RACE;
dto.entityId = 'race-123';
dto.actorType = 'anonymous';
dto.sessionId = 'session-456';
dto.metadata = {
key1: 'value1',
key2: 'value2',
key3: 'value3',
};
// Assert
expect(dto.metadata).toEqual({
key1: 'value1',
key2: 'value2',
key3: 'value3',
});
});
it('should handle metadata with numeric values', () => {
// Arrange & Act
const dto = new RecordEngagementInputDTO();
dto.action = EngagementAction.CLICK_SPONSOR_LOGO;
dto.entityType = EngagementEntityType.RACE;
dto.entityId = 'race-123';
dto.actorType = 'anonymous';
dto.sessionId = 'session-456';
dto.metadata = { count: 10, score: 95.5 };
// Assert
expect(dto.metadata).toEqual({ count: 10, score: 95.5 });
});
it('should handle metadata with boolean values', () => {
// Arrange & Act
const dto = new RecordEngagementInputDTO();
dto.action = EngagementAction.CLICK_SPONSOR_LOGO;
dto.entityType = EngagementEntityType.RACE;
dto.entityId = 'race-123';
dto.actorType = 'anonymous';
dto.sessionId = 'session-456';
dto.metadata = { isFeatured: true, isPremium: false };
// Assert
expect(dto.metadata).toEqual({ isFeatured: true, isPremium: false });
});
it('should handle very long entity ID', () => {
// Arrange
const longId = 'a'.repeat(100);
// Act
const dto = new RecordEngagementInputDTO();
dto.action = EngagementAction.CLICK_SPONSOR_LOGO;
dto.entityType = EngagementEntityType.RACE;
dto.entityId = longId;
dto.actorType = 'anonymous';
dto.sessionId = 'session-456';
// Assert
expect(dto.entityId).toBe(longId);
});
it('should handle very long session ID', () => {
// Arrange
const longSessionId = 's'.repeat(100);
// Act
const dto = new RecordEngagementInputDTO();
dto.action = EngagementAction.CLICK_SPONSOR_LOGO;
dto.entityType = EngagementEntityType.RACE;
dto.entityId = 'race-123';
dto.actorType = 'anonymous';
dto.sessionId = longSessionId;
// Assert
expect(dto.sessionId).toBe(longSessionId);
});
it('should handle very long actor ID', () => {
// Arrange
const longActorId = 'a'.repeat(100);
// Act
const dto = new RecordEngagementInputDTO();
dto.action = EngagementAction.CLICK_SPONSOR_LOGO;
dto.entityType = EngagementEntityType.RACE;
dto.entityId = 'race-123';
dto.actorType = 'driver';
dto.sessionId = 'session-456';
dto.actorId = longActorId;
// Assert
expect(dto.actorId).toBe(longActorId);
});
it('should handle special characters in entity ID', () => {
// Arrange & Act
const dto = new RecordEngagementInputDTO();
dto.action = EngagementAction.CLICK_SPONSOR_LOGO;
dto.entityType = EngagementEntityType.RACE;
dto.entityId = 'race-123-test-456';
dto.actorType = 'anonymous';
dto.sessionId = 'session-789';
// Assert
expect(dto.entityId).toBe('race-123-test-456');
});
it('should handle UUID format for entity ID', () => {
// Arrange
const uuid = '550e8400-e29b-41d4-a716-446655440000';
// Act
const dto = new RecordEngagementInputDTO();
dto.action = EngagementAction.CLICK_SPONSOR_LOGO;
dto.entityType = EngagementEntityType.RACE;
dto.entityId = uuid;
dto.actorType = 'anonymous';
dto.sessionId = 'session-456';
// Assert
expect(dto.entityId).toBe(uuid);
});
it('should handle numeric entity ID', () => {
// Arrange & Act
const dto = new RecordEngagementInputDTO();
dto.action = EngagementAction.CLICK_SPONSOR_LOGO;
dto.entityType = EngagementEntityType.RACE;
dto.entityId = '123456';
dto.actorType = 'anonymous';
dto.sessionId = 'session-456';
// Assert
expect(dto.entityId).toBe('123456');
});
it('should handle complex metadata with string values', () => {
// Arrange & Act
const dto = new RecordEngagementInputDTO();
dto.action = EngagementAction.CLICK_SPONSOR_LOGO;
dto.entityType = EngagementEntityType.RACE;
dto.entityId = 'race-123';
dto.actorType = 'anonymous';
dto.sessionId = 'session-456';
dto.metadata = {
position: '100,200',
timestamp: '2024-01-01T00:00:00Z',
isValid: 'true',
};
// Assert
expect(dto.metadata).toEqual({
position: '100,200',
timestamp: '2024-01-01T00:00:00Z',
isValid: 'true',
});
});
});
});

View File

@@ -0,0 +1,222 @@
import { RecordEngagementOutputDTO } from './RecordEngagementOutputDTO';
describe('RecordEngagementOutputDTO', () => {
describe('TDD - Test First', () => {
it('should create valid DTO with all fields', () => {
// Arrange & Act
const dto = new RecordEngagementOutputDTO();
dto.eventId = 'event-123';
dto.engagementWeight = 10;
// Assert
expect(dto.eventId).toBe('event-123');
expect(dto.engagementWeight).toBe(10);
});
it('should handle zero engagement weight', () => {
// Arrange & Act
const dto = new RecordEngagementOutputDTO();
dto.eventId = 'event-123';
dto.engagementWeight = 0;
// Assert
expect(dto.engagementWeight).toBe(0);
});
it('should handle single digit engagement weight', () => {
// Arrange & Act
const dto = new RecordEngagementOutputDTO();
dto.eventId = 'event-123';
dto.engagementWeight = 1;
// Assert
expect(dto.engagementWeight).toBe(1);
});
it('should handle large engagement weight', () => {
// Arrange & Act
const dto = new RecordEngagementOutputDTO();
dto.eventId = 'event-123';
dto.engagementWeight = 1000;
// Assert
expect(dto.engagementWeight).toBe(1000);
});
it('should handle very large engagement weight', () => {
// Arrange & Act
const dto = new RecordEngagementOutputDTO();
dto.eventId = 'event-123';
dto.engagementWeight = 999999;
// Assert
expect(dto.engagementWeight).toBe(999999);
});
it('should handle negative engagement weight', () => {
// Arrange & Act
const dto = new RecordEngagementOutputDTO();
dto.eventId = 'event-123';
dto.engagementWeight = -10;
// Assert
expect(dto.engagementWeight).toBe(-10);
});
it('should handle decimal engagement weight', () => {
// Arrange & Act
const dto = new RecordEngagementOutputDTO();
dto.eventId = 'event-123';
dto.engagementWeight = 10.5;
// Assert
expect(dto.engagementWeight).toBe(10.5);
});
it('should handle very small decimal engagement weight', () => {
// Arrange & Act
const dto = new RecordEngagementOutputDTO();
dto.eventId = 'event-123';
dto.engagementWeight = 0.001;
// Assert
expect(dto.engagementWeight).toBe(0.001);
});
it('should handle scientific notation for engagement weight', () => {
// Arrange & Act
const dto = new RecordEngagementOutputDTO();
dto.eventId = 'event-123';
dto.engagementWeight = 1e3;
// Assert
expect(dto.engagementWeight).toBe(1000);
});
it('should handle UUID format for event ID', () => {
// Arrange
const uuid = '550e8400-e29b-41d4-a716-446655440000';
// Act
const dto = new RecordEngagementOutputDTO();
dto.eventId = uuid;
dto.engagementWeight = 10;
// Assert
expect(dto.eventId).toBe(uuid);
});
it('should handle numeric event ID', () => {
// Arrange & Act
const dto = new RecordEngagementOutputDTO();
dto.eventId = '123456';
dto.engagementWeight = 10;
// Assert
expect(dto.eventId).toBe('123456');
});
it('should handle special characters in event ID', () => {
// Arrange & Act
const dto = new RecordEngagementOutputDTO();
dto.eventId = 'event-123-test-456';
dto.engagementWeight = 10;
// Assert
expect(dto.eventId).toBe('event-123-test-456');
});
it('should handle very long event ID', () => {
// Arrange
const longId = 'e'.repeat(100);
// Act
const dto = new RecordEngagementOutputDTO();
dto.eventId = longId;
dto.engagementWeight = 10;
// Assert
expect(dto.eventId).toBe(longId);
});
it('should handle maximum safe integer for engagement weight', () => {
// Arrange & Act
const dto = new RecordEngagementOutputDTO();
dto.eventId = 'event-123';
dto.engagementWeight = Number.MAX_SAFE_INTEGER;
// Assert
expect(dto.engagementWeight).toBe(Number.MAX_SAFE_INTEGER);
});
it('should handle minimum safe integer for engagement weight', () => {
// Arrange & Act
const dto = new RecordEngagementOutputDTO();
dto.eventId = 'event-123';
dto.engagementWeight = Number.MIN_SAFE_INTEGER;
// Assert
expect(dto.engagementWeight).toBe(Number.MIN_SAFE_INTEGER);
});
it('should handle Infinity for engagement weight', () => {
// Arrange & Act
const dto = new RecordEngagementOutputDTO();
dto.eventId = 'event-123';
dto.engagementWeight = Infinity;
// Assert
expect(dto.engagementWeight).toBe(Infinity);
});
it('should handle NaN for engagement weight', () => {
// Arrange & Act
const dto = new RecordEngagementOutputDTO();
dto.eventId = 'event-123';
dto.engagementWeight = NaN;
// Assert
expect(dto.engagementWeight).toBeNaN();
});
it('should handle very small event ID', () => {
// Arrange & Act
const dto = new RecordEngagementOutputDTO();
dto.eventId = 'e';
dto.engagementWeight = 10;
// Assert
expect(dto.eventId).toBe('e');
});
it('should handle event ID with spaces', () => {
// Arrange & Act
const dto = new RecordEngagementOutputDTO();
dto.eventId = 'event 123 test';
dto.engagementWeight = 10;
// Assert
expect(dto.eventId).toBe('event 123 test');
});
it('should handle event ID with special characters', () => {
// Arrange & Act
const dto = new RecordEngagementOutputDTO();
dto.eventId = 'event@123#test$456';
dto.engagementWeight = 10;
// Assert
expect(dto.eventId).toBe('event@123#test$456');
});
it('should handle event ID with unicode characters', () => {
// Arrange & Act
const dto = new RecordEngagementOutputDTO();
dto.eventId = 'event-123-测试-456';
dto.engagementWeight = 10;
// Assert
expect(dto.eventId).toBe('event-123-测试-456');
});
});
});

View File

@@ -0,0 +1,299 @@
import { RecordPageViewInputDTO } from './RecordPageViewInputDTO';
import { EntityType, VisitorType } from '@core/analytics/domain/types/PageView';
describe('RecordPageViewInputDTO', () => {
describe('TDD - Test First', () => {
it('should create valid DTO with all fields', () => {
// Arrange & Act
const dto = new RecordPageViewInputDTO();
dto.entityType = EntityType.RACE;
dto.entityId = 'race-123';
dto.visitorId = 'visitor-456';
dto.visitorType = VisitorType.ANONYMOUS;
dto.sessionId = 'session-789';
dto.referrer = 'https://example.com';
dto.userAgent = 'Mozilla/5.0';
dto.country = 'US';
// Assert
expect(dto.entityType).toBe(EntityType.RACE);
expect(dto.entityId).toBe('race-123');
expect(dto.visitorId).toBe('visitor-456');
expect(dto.visitorType).toBe(VisitorType.ANONYMOUS);
expect(dto.sessionId).toBe('session-789');
expect(dto.referrer).toBe('https://example.com');
expect(dto.userAgent).toBe('Mozilla/5.0');
expect(dto.country).toBe('US');
});
it('should create DTO with required fields only', () => {
// Arrange & Act
const dto = new RecordPageViewInputDTO();
dto.entityType = EntityType.LEAGUE;
dto.entityId = 'league-123';
dto.visitorType = VisitorType.AUTHENTICATED;
dto.sessionId = 'session-456';
// Assert
expect(dto.entityType).toBe(EntityType.LEAGUE);
expect(dto.entityId).toBe('league-123');
expect(dto.visitorType).toBe(VisitorType.AUTHENTICATED);
expect(dto.sessionId).toBe('session-456');
expect(dto.visitorId).toBeUndefined();
expect(dto.referrer).toBeUndefined();
expect(dto.userAgent).toBeUndefined();
expect(dto.country).toBeUndefined();
});
it('should handle RACE entity type', () => {
// Arrange & Act
const dto = new RecordPageViewInputDTO();
dto.entityType = EntityType.RACE;
dto.entityId = 'race-123';
dto.visitorType = VisitorType.ANONYMOUS;
dto.sessionId = 'session-456';
// Assert
expect(dto.entityType).toBe(EntityType.RACE);
});
it('should handle LEAGUE entity type', () => {
// Arrange & Act
const dto = new RecordPageViewInputDTO();
dto.entityType = EntityType.LEAGUE;
dto.entityId = 'league-123';
dto.visitorType = VisitorType.ANONYMOUS;
dto.sessionId = 'session-456';
// Assert
expect(dto.entityType).toBe(EntityType.LEAGUE);
});
it('should handle DRIVER entity type', () => {
// Arrange & Act
const dto = new RecordPageViewInputDTO();
dto.entityType = EntityType.DRIVER;
dto.entityId = 'driver-123';
dto.visitorType = VisitorType.ANONYMOUS;
dto.sessionId = 'session-456';
// Assert
expect(dto.entityType).toBe(EntityType.DRIVER);
});
it('should handle TEAM entity type', () => {
// Arrange & Act
const dto = new RecordPageViewInputDTO();
dto.entityType = EntityType.TEAM;
dto.entityId = 'team-123';
dto.visitorType = VisitorType.ANONYMOUS;
dto.sessionId = 'session-456';
// Assert
expect(dto.entityType).toBe(EntityType.TEAM);
});
it('should handle ANONYMOUS visitor type', () => {
// Arrange & Act
const dto = new RecordPageViewInputDTO();
dto.entityType = EntityType.RACE;
dto.entityId = 'race-123';
dto.visitorType = VisitorType.ANONYMOUS;
dto.sessionId = 'session-456';
// Assert
expect(dto.visitorType).toBe(VisitorType.ANONYMOUS);
});
it('should handle AUTHENTICATED visitor type', () => {
// Arrange & Act
const dto = new RecordPageViewInputDTO();
dto.entityType = EntityType.RACE;
dto.entityId = 'race-123';
dto.visitorType = VisitorType.AUTHENTICATED;
dto.sessionId = 'session-456';
// Assert
expect(dto.visitorType).toBe(VisitorType.AUTHENTICATED);
});
it('should handle empty referrer', () => {
// Arrange & Act
const dto = new RecordPageViewInputDTO();
dto.entityType = EntityType.RACE;
dto.entityId = 'race-123';
dto.visitorType = VisitorType.ANONYMOUS;
dto.sessionId = 'session-456';
dto.referrer = '';
// Assert
expect(dto.referrer).toBe('');
});
it('should handle empty userAgent', () => {
// Arrange & Act
const dto = new RecordPageViewInputDTO();
dto.entityType = EntityType.RACE;
dto.entityId = 'race-123';
dto.visitorType = VisitorType.ANONYMOUS;
dto.sessionId = 'session-456';
dto.userAgent = '';
// Assert
expect(dto.userAgent).toBe('');
});
it('should handle empty country', () => {
// Arrange & Act
const dto = new RecordPageViewInputDTO();
dto.entityType = EntityType.RACE;
dto.entityId = 'race-123';
dto.visitorType = VisitorType.ANONYMOUS;
dto.sessionId = 'session-456';
dto.country = '';
// Assert
expect(dto.country).toBe('');
});
it('should handle very long entity ID', () => {
// Arrange
const longId = 'a'.repeat(100);
// Act
const dto = new RecordPageViewInputDTO();
dto.entityType = EntityType.RACE;
dto.entityId = longId;
dto.visitorType = VisitorType.ANONYMOUS;
dto.sessionId = 'session-456';
// Assert
expect(dto.entityId).toBe(longId);
});
it('should handle very long session ID', () => {
// Arrange
const longSessionId = 's'.repeat(100);
// Act
const dto = new RecordPageViewInputDTO();
dto.entityType = EntityType.RACE;
dto.entityId = 'race-123';
dto.visitorType = VisitorType.ANONYMOUS;
dto.sessionId = longSessionId;
// Assert
expect(dto.sessionId).toBe(longSessionId);
});
it('should handle very long visitor ID', () => {
// Arrange
const longVisitorId = 'v'.repeat(100);
// Act
const dto = new RecordPageViewInputDTO();
dto.entityType = EntityType.RACE;
dto.entityId = 'race-123';
dto.visitorType = VisitorType.AUTHENTICATED;
dto.sessionId = 'session-456';
dto.visitorId = longVisitorId;
// Assert
expect(dto.visitorId).toBe(longVisitorId);
});
it('should handle special characters in entity ID', () => {
// Arrange & Act
const dto = new RecordPageViewInputDTO();
dto.entityType = EntityType.RACE;
dto.entityId = 'race-123-test-456';
dto.visitorType = VisitorType.ANONYMOUS;
dto.sessionId = 'session-789';
// Assert
expect(dto.entityId).toBe('race-123-test-456');
});
it('should handle UUID format for entity ID', () => {
// Arrange
const uuid = '550e8400-e29b-41d4-a716-446655440000';
// Act
const dto = new RecordPageViewInputDTO();
dto.entityType = EntityType.RACE;
dto.entityId = uuid;
dto.visitorType = VisitorType.ANONYMOUS;
dto.sessionId = 'session-456';
// Assert
expect(dto.entityId).toBe(uuid);
});
it('should handle numeric entity ID', () => {
// Arrange & Act
const dto = new RecordPageViewInputDTO();
dto.entityType = EntityType.RACE;
dto.entityId = '123456';
dto.visitorType = VisitorType.ANONYMOUS;
dto.sessionId = 'session-456';
// Assert
expect(dto.entityId).toBe('123456');
});
it('should handle URL in referrer', () => {
// Arrange & Act
const dto = new RecordPageViewInputDTO();
dto.entityType = EntityType.RACE;
dto.entityId = 'race-123';
dto.visitorType = VisitorType.ANONYMOUS;
dto.sessionId = 'session-456';
dto.referrer = 'https://www.example.com/path/to/page?query=value';
// Assert
expect(dto.referrer).toBe('https://www.example.com/path/to/page?query=value');
});
it('should handle complex user agent string', () => {
// Arrange & Act
const dto = new RecordPageViewInputDTO();
dto.entityType = EntityType.RACE;
dto.entityId = 'race-123';
dto.visitorType = VisitorType.ANONYMOUS;
dto.sessionId = 'session-456';
dto.userAgent =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36';
// Assert
expect(dto.userAgent).toBe(
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
);
});
it('should handle country codes', () => {
// Arrange & Act
const dto = new RecordPageViewInputDTO();
dto.entityType = EntityType.RACE;
dto.entityId = 'race-123';
dto.visitorType = VisitorType.ANONYMOUS;
dto.sessionId = 'session-456';
dto.country = 'GB';
// Assert
expect(dto.country).toBe('GB');
});
it('should handle country with region', () => {
// Arrange & Act
const dto = new RecordPageViewInputDTO();
dto.entityType = EntityType.RACE;
dto.entityId = 'race-123';
dto.visitorType = VisitorType.ANONYMOUS;
dto.sessionId = 'session-456';
dto.country = 'US-CA';
// Assert
expect(dto.country).toBe('US-CA');
});
});
});

View File

@@ -0,0 +1,344 @@
import { RecordPageViewOutputDTO } from './RecordPageViewOutputDTO';
describe('RecordPageViewOutputDTO', () => {
describe('TDD - Test First', () => {
it('should create valid DTO with all fields', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = 'pv-123';
// Assert
expect(dto.pageViewId).toBe('pv-123');
});
it('should handle UUID format for page view ID', () => {
// Arrange
const uuid = '550e8400-e29b-41d4-a716-446655440000';
// Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = uuid;
// Assert
expect(dto.pageViewId).toBe(uuid);
});
it('should handle numeric page view ID', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = '123456';
// Assert
expect(dto.pageViewId).toBe('123456');
});
it('should handle special characters in page view ID', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = 'pv-123-test-456';
// Assert
expect(dto.pageViewId).toBe('pv-123-test-456');
});
it('should handle very long page view ID', () => {
// Arrange
const longId = 'p'.repeat(100);
// Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = longId;
// Assert
expect(dto.pageViewId).toBe(longId);
});
it('should handle very small page view ID', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = 'p';
// Assert
expect(dto.pageViewId).toBe('p');
});
it('should handle page view ID with spaces', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = 'pv 123 test';
// Assert
expect(dto.pageViewId).toBe('pv 123 test');
});
it('should handle page view ID with special characters', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = 'pv@123#test$456';
// Assert
expect(dto.pageViewId).toBe('pv@123#test$456');
});
it('should handle page view ID with unicode characters', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = 'pv-123-测试-456';
// Assert
expect(dto.pageViewId).toBe('pv-123-测试-456');
});
it('should handle page view ID with leading zeros', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = '000123';
// Assert
expect(dto.pageViewId).toBe('000123');
});
it('should handle page view ID with trailing zeros', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = '123000';
// Assert
expect(dto.pageViewId).toBe('123000');
});
it('should handle page view ID with mixed case', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = 'Pv-123-Test-456';
// Assert
expect(dto.pageViewId).toBe('Pv-123-Test-456');
});
it('should handle page view ID with underscores', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = 'pv_123_test_456';
// Assert
expect(dto.pageViewId).toBe('pv_123_test_456');
});
it('should handle page view ID with dots', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = 'pv.123.test.456';
// Assert
expect(dto.pageViewId).toBe('pv.123.test.456');
});
it('should handle page view ID with hyphens', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = 'pv-123-test-456';
// Assert
expect(dto.pageViewId).toBe('pv-123-test-456');
});
it('should handle page view ID with colons', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = 'pv:123:test:456';
// Assert
expect(dto.pageViewId).toBe('pv:123:test:456');
});
it('should handle page view ID with slashes', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = 'pv/123/test/456';
// Assert
expect(dto.pageViewId).toBe('pv/123/test/456');
});
it('should handle page view ID with backslashes', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = 'pv\\123\\test\\456';
// Assert
expect(dto.pageViewId).toBe('pv\\123\\test\\456');
});
it('should handle page view ID with pipes', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = 'pv|123|test|456';
// Assert
expect(dto.pageViewId).toBe('pv|123|test|456');
});
it('should handle page view ID with ampersands', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = 'pv&123&test&456';
// Assert
expect(dto.pageViewId).toBe('pv&123&test&456');
});
it('should handle page view ID with percent signs', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = 'pv%123%test%456';
// Assert
expect(dto.pageViewId).toBe('pv%123%test%456');
});
it('should handle page view ID with dollar signs', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = 'pv$123$test$456';
// Assert
expect(dto.pageViewId).toBe('pv$123$test$456');
});
it('should handle page view ID with exclamation marks', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = 'pv!123!test!456';
// Assert
expect(dto.pageViewId).toBe('pv!123!test!456');
});
it('should handle page view ID with question marks', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = 'pv?123?test?456';
// Assert
expect(dto.pageViewId).toBe('pv?123?test?456');
});
it('should handle page view ID with plus signs', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = 'pv+123+test+456';
// Assert
expect(dto.pageViewId).toBe('pv+123+test+456');
});
it('should handle page view ID with equals signs', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = 'pv=123=test=456';
// Assert
expect(dto.pageViewId).toBe('pv=123=test=456');
});
it('should handle page view ID with asterisks', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = 'pv*123*test*456';
// Assert
expect(dto.pageViewId).toBe('pv*123*test*456');
});
it('should handle page view ID with parentheses', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = 'pv(123)test(456)';
// Assert
expect(dto.pageViewId).toBe('pv(123)test(456)');
});
it('should handle page view ID with brackets', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = 'pv[123]test[456]';
// Assert
expect(dto.pageViewId).toBe('pv[123]test[456]');
});
it('should handle page view ID with curly braces', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = 'pv{123}test{456}';
// Assert
expect(dto.pageViewId).toBe('pv{123}test{456}');
});
it('should handle page view ID with angle brackets', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = 'pv<123>test<456>';
// Assert
expect(dto.pageViewId).toBe('pv<123>test<456>');
});
it('should handle page view ID with quotes', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = 'pv"123"test"456"';
// Assert
expect(dto.pageViewId).toBe('pv"123"test"456"');
});
it('should handle page view ID with single quotes', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = "pv'123'test'456'";
// Assert
expect(dto.pageViewId).toBe("pv'123'test'456'");
});
it('should handle page view ID with backticks', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = 'pv`123`test`456`';
// Assert
expect(dto.pageViewId).toBe('pv`123`test`456`');
});
it('should handle page view ID with tildes', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = 'pv~123~test~456';
// Assert
expect(dto.pageViewId).toBe('pv~123~test~456');
});
it('should handle page view ID with at signs', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = 'pv@123@test@456';
// Assert
expect(dto.pageViewId).toBe('pv@123@test@456');
});
it('should handle page view ID with hash signs', () => {
// Arrange & Act
const dto = new RecordPageViewOutputDTO();
dto.pageViewId = 'pv#123#test#456';
// Assert
expect(dto.pageViewId).toBe('pv#123#test#456');
});
});
});

View File

@@ -0,0 +1,153 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { AuthorizationService } from './AuthorizationService';
describe('AuthorizationService', () => {
let service: AuthorizationService;
beforeEach(() => {
// Clear environment variables
delete process.env.GRIDPILOT_AUTHZ_CACHE_MS;
delete process.env.GRIDPILOT_USER_ROLES_JSON;
service = new AuthorizationService();
});
describe('getRolesForUser', () => {
it('should return empty array when no roles are configured', () => {
const roles = service.getRolesForUser('user-123');
expect(roles).toEqual([]);
});
it('should return roles from environment variable', () => {
process.env.GRIDPILOT_USER_ROLES_JSON = JSON.stringify({
'user-123': ['admin', 'owner'],
'user-456': ['user'],
});
const roles = service.getRolesForUser('user-123');
expect(roles).toEqual(['admin', 'owner']);
});
it('should return empty array for user not in roles config', () => {
process.env.GRIDPILOT_USER_ROLES_JSON = JSON.stringify({
'user-123': ['admin'],
});
const roles = service.getRolesForUser('user-456');
expect(roles).toEqual([]);
});
it('should cache roles and return cached values', () => {
process.env.GRIDPILOT_USER_ROLES_JSON = JSON.stringify({
'user-123': ['admin'],
});
process.env.GRIDPILOT_AUTHZ_CACHE_MS = '10000';
// First call
const roles1 = service.getRolesForUser('user-123');
expect(roles1).toEqual(['admin']);
// Second call should return cached value
const roles2 = service.getRolesForUser('user-123');
expect(roles2).toEqual(['admin']);
});
it('should handle invalid JSON gracefully', () => {
process.env.GRIDPILOT_USER_ROLES_JSON = 'invalid json';
const roles = service.getRolesForUser('user-123');
expect(roles).toEqual([]);
});
it('should handle non-object JSON gracefully', () => {
process.env.GRIDPILOT_USER_ROLES_JSON = JSON.stringify('not an object');
const roles = service.getRolesForUser('user-123');
expect(roles).toEqual([]);
});
it('should filter out non-string roles', () => {
process.env.GRIDPILOT_USER_ROLES_JSON = JSON.stringify({
'user-123': ['admin', 123, null, 'owner', undefined, 'user'],
});
const roles = service.getRolesForUser('user-123');
expect(roles).toEqual(['admin', 'owner', 'user']);
});
it('should trim whitespace from roles', () => {
process.env.GRIDPILOT_USER_ROLES_JSON = JSON.stringify({
'user-123': [' admin ', ' owner '],
});
const roles = service.getRolesForUser('user-123');
expect(roles).toEqual(['admin', 'owner']);
});
it('should filter out empty strings after trimming', () => {
process.env.GRIDPILOT_USER_ROLES_JSON = JSON.stringify({
'user-123': ['admin', ' ', 'owner'],
});
const roles = service.getRolesForUser('user-123');
expect(roles).toEqual(['admin', 'owner']);
});
it('should use default cache time when not configured', () => {
process.env.GRIDPILOT_USER_ROLES_JSON = JSON.stringify({
'user-123': ['admin'],
});
const roles = service.getRolesForUser('user-123');
expect(roles).toEqual(['admin']);
});
it('should use configured cache time', () => {
process.env.GRIDPILOT_USER_ROLES_JSON = JSON.stringify({
'user-123': ['admin'],
});
process.env.GRIDPILOT_AUTHZ_CACHE_MS = '5000';
const roles = service.getRolesForUser('user-123');
expect(roles).toEqual(['admin']);
});
it('should handle invalid cache time gracefully', () => {
process.env.GRIDPILOT_USER_ROLES_JSON = JSON.stringify({
'user-123': ['admin'],
});
process.env.GRIDPILOT_AUTHZ_CACHE_MS = 'invalid';
const roles = service.getRolesForUser('user-123');
expect(roles).toEqual(['admin']);
});
it('should handle negative cache time gracefully', () => {
process.env.GRIDPILOT_USER_ROLES_JSON = JSON.stringify({
'user-123': ['admin'],
});
process.env.GRIDPILOT_AUTHZ_CACHE_MS = '-1000';
const roles = service.getRolesForUser('user-123');
expect(roles).toEqual(['admin']);
});
it('should handle empty roles array', () => {
process.env.GRIDPILOT_USER_ROLES_JSON = JSON.stringify({
'user-123': [],
});
const roles = service.getRolesForUser('user-123');
expect(roles).toEqual([]);
});
it('should handle user ID with special characters', () => {
process.env.GRIDPILOT_USER_ROLES_JSON = JSON.stringify({
'user-123@example.com': ['admin'],
});
const roles = service.getRolesForUser('user-123@example.com');
expect(roles).toEqual(['admin']);
});
});
});

View File

@@ -0,0 +1,40 @@
import { SetMetadata } from '@nestjs/common';
import { Public, PUBLIC_ROUTE_METADATA_KEY } from './Public';
// Mock SetMetadata
vi.mock('@nestjs/common', () => ({
SetMetadata: vi.fn(() => () => {}),
}));
describe('Public', () => {
it('should return a method decorator', () => {
const decorator = Public();
expect(typeof decorator).toBe('function');
});
it('should call SetMetadata with correct key and value', () => {
Public();
expect(SetMetadata).toHaveBeenCalledWith(PUBLIC_ROUTE_METADATA_KEY, {
public: true,
});
});
it('should return a decorator that can be used as both method and class decorator', () => {
const decorator = Public();
// Test as method decorator
const mockTarget = {};
const mockPropertyKey = 'testMethod';
const mockDescriptor = { value: () => {} };
const result = decorator(mockTarget, mockPropertyKey, mockDescriptor);
// The decorator should return the descriptor
expect(result).toBe(mockDescriptor);
});
it('should have correct metadata key', () => {
expect(PUBLIC_ROUTE_METADATA_KEY).toBe('gridpilot:publicRoute');
});
});

View File

@@ -0,0 +1,40 @@
import { SetMetadata } from '@nestjs/common';
import { RequireAuthenticatedUser, REQUIRE_AUTHENTICATED_USER_METADATA_KEY } from './RequireAuthenticatedUser';
// Mock SetMetadata
vi.mock('@nestjs/common', () => ({
SetMetadata: vi.fn(() => () => {}),
}));
describe('RequireAuthenticatedUser', () => {
it('should return a method decorator', () => {
const decorator = RequireAuthenticatedUser();
expect(typeof decorator).toBe('function');
});
it('should call SetMetadata with correct key and value', () => {
RequireAuthenticatedUser();
expect(SetMetadata).toHaveBeenCalledWith(REQUIRE_AUTHENTICATED_USER_METADATA_KEY, {
required: true,
});
});
it('should return a decorator that can be used as both method and class decorator', () => {
const decorator = RequireAuthenticatedUser();
// Test as method decorator
const mockTarget = {};
const mockPropertyKey = 'testMethod';
const mockDescriptor = { value: () => {} };
const result = decorator(mockTarget, mockPropertyKey, mockDescriptor);
// The decorator should return the descriptor
expect(result).toBe(mockDescriptor);
});
it('should have correct metadata key', () => {
expect(REQUIRE_AUTHENTICATED_USER_METADATA_KEY).toBe('gridpilot:requireAuthenticatedUser');
});
});

View File

@@ -0,0 +1,69 @@
import { SetMetadata } from '@nestjs/common';
import { RequireRoles, REQUIRE_ROLES_METADATA_KEY } from './RequireRoles';
// Mock SetMetadata
vi.mock('@nestjs/common', () => ({
SetMetadata: vi.fn(() => () => {}),
}));
describe('RequireRoles', () => {
it('should return a method decorator', () => {
const decorator = RequireRoles('admin');
expect(typeof decorator).toBe('function');
});
it('should call SetMetadata with correct key and value for single role', () => {
RequireRoles('admin');
expect(SetMetadata).toHaveBeenCalledWith(REQUIRE_ROLES_METADATA_KEY, {
anyOf: ['admin'],
});
});
it('should call SetMetadata with correct key and value for multiple roles', () => {
RequireRoles('admin', 'owner', 'moderator');
expect(SetMetadata).toHaveBeenCalledWith(REQUIRE_ROLES_METADATA_KEY, {
anyOf: ['admin', 'owner', 'moderator'],
});
});
it('should return a decorator that can be used as both method and class decorator', () => {
const decorator = RequireRoles('admin');
// Test as method decorator
const mockTarget = {};
const mockPropertyKey = 'testMethod';
const mockDescriptor = { value: () => {} };
const result = decorator(mockTarget, mockPropertyKey, mockDescriptor);
// The decorator should return the descriptor
expect(result).toBe(mockDescriptor);
});
it('should have correct metadata key', () => {
expect(REQUIRE_ROLES_METADATA_KEY).toBe('gridpilot:requireRoles');
});
it('should handle empty roles array', () => {
const decorator = RequireRoles();
// Test as method decorator
const mockTarget = {};
const mockPropertyKey = 'testMethod';
const mockDescriptor = { value: () => {} };
const result = decorator(mockTarget, mockPropertyKey, mockDescriptor);
expect(result).toBe(mockDescriptor);
});
it('should handle roles with special characters', () => {
RequireRoles('admin-user', 'owner@company');
expect(SetMetadata).toHaveBeenCalledWith(REQUIRE_ROLES_METADATA_KEY, {
anyOf: ['admin-user', 'owner@company'],
});
});
});

View File

@@ -0,0 +1,156 @@
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
import { getActorFromRequestContext } from './getActorFromRequestContext';
// Mock the http adapter
vi.mock('@adapters/http/RequestContext', () => ({
getHttpRequestContext: vi.fn(),
}));
import { getHttpRequestContext } from '@adapters/http/RequestContext';
describe('getActorFromRequestContext', () => {
const mockGetHttpRequestContext = vi.mocked(getHttpRequestContext);
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should return actor with userId and driverId from request', () => {
const mockContext = {
req: {
user: {
userId: 'user-123',
},
},
};
mockGetHttpRequestContext.mockReturnValue(mockContext as never);
const actor = getActorFromRequestContext();
expect(actor).toEqual({
userId: 'user-123',
driverId: 'user-123',
role: undefined,
});
});
it('should include role from request when available', () => {
const mockContext = {
req: {
user: {
userId: 'user-123',
role: 'admin',
},
},
};
mockGetHttpRequestContext.mockReturnValue(mockContext as never);
const actor = getActorFromRequestContext();
expect(actor).toEqual({
userId: 'user-123',
driverId: 'user-123',
role: 'admin',
});
});
it('should throw error when userId is missing', () => {
const mockContext = {
req: {
user: {},
},
};
mockGetHttpRequestContext.mockReturnValue(mockContext as never);
expect(() => getActorFromRequestContext()).toThrow('Unauthorized');
});
it('should throw error when user object is missing', () => {
const mockContext = {
req: {},
};
mockGetHttpRequestContext.mockReturnValue(mockContext as never);
expect(() => getActorFromRequestContext()).toThrow('Unauthorized');
});
it('should throw error when request is missing', () => {
const mockContext = {};
mockGetHttpRequestContext.mockReturnValue(mockContext as never);
expect(() => getActorFromRequestContext()).toThrow('Unauthorized');
});
it('should handle userId as empty string', () => {
const mockContext = {
req: {
user: {
userId: '',
},
},
};
mockGetHttpRequestContext.mockReturnValue(mockContext as never);
expect(() => getActorFromRequestContext()).toThrow('Unauthorized');
});
it('should map userId to driverId correctly', () => {
const mockContext = {
req: {
user: {
userId: 'driver-456',
},
},
};
mockGetHttpRequestContext.mockReturnValue(mockContext as never);
const actor = getActorFromRequestContext();
expect(actor.driverId).toBe('driver-456');
expect(actor.userId).toBe('driver-456');
});
it('should handle role as undefined when not provided', () => {
const mockContext = {
req: {
user: {
userId: 'user-123',
},
},
};
mockGetHttpRequestContext.mockReturnValue(mockContext as never);
const actor = getActorFromRequestContext();
expect(actor.role).toBeUndefined();
});
it('should handle role as null', () => {
const mockContext = {
req: {
user: {
userId: 'user-123',
role: null,
},
},
};
mockGetHttpRequestContext.mockReturnValue(mockContext as never);
const actor = getActorFromRequestContext();
expect(actor.role).toBeNull();
});
});

View File

@@ -0,0 +1,89 @@
import { Test, TestingModule } from '@nestjs/testing';
import { DatabaseModule } from './DatabaseModule';
describe('DatabaseModule', () => {
let module: TestingModule;
beforeEach(async () => {
// Clear environment variables to ensure consistent test behavior
delete process.env.DATABASE_URL;
delete process.env.DATABASE_HOST;
delete process.env.DATABASE_PORT;
delete process.env.DATABASE_USER;
delete process.env.DATABASE_PASSWORD;
delete process.env.DATABASE_NAME;
delete process.env.NODE_ENV;
module = await Test.createTestingModule({
imports: [DatabaseModule],
}).compile();
});
it('should compile the module', () => {
expect(module).toBeDefined();
});
it('should configure TypeORM with DATABASE_URL when provided', async () => {
process.env.DATABASE_URL = 'postgres://user:pass@localhost:5432/testdb';
process.env.NODE_ENV = 'production';
const testModule = await Test.createTestingModule({
imports: [DatabaseModule],
}).compile();
expect(testModule).toBeDefined();
});
it('should configure TypeORM with individual connection parameters when DATABASE_URL is not provided', async () => {
process.env.DATABASE_HOST = 'localhost';
process.env.DATABASE_PORT = '5432';
process.env.DATABASE_USER = 'testuser';
process.env.DATABASE_PASSWORD = 'testpass';
process.env.DATABASE_NAME = 'testdb';
process.env.NODE_ENV = 'development';
const testModule = await Test.createTestingModule({
imports: [DatabaseModule],
}).compile();
expect(testModule).toBeDefined();
});
it('should use default values when connection parameters are not provided', async () => {
process.env.NODE_ENV = 'test';
const testModule = await Test.createTestingModule({
imports: [DatabaseModule],
}).compile();
expect(testModule).toBeDefined();
});
it('should enable synchronization in non-production environments', async () => {
process.env.NODE_ENV = 'development';
const testModule = await Test.createTestingModule({
imports: [DatabaseModule],
}).compile();
expect(testModule).toBeDefined();
});
it('should disable synchronization in production environment', async () => {
process.env.NODE_ENV = 'production';
const testModule = await Test.createTestingModule({
imports: [DatabaseModule],
}).compile();
expect(testModule).toBeDefined();
});
it('should auto load entities', async () => {
const testModule = await Test.createTestingModule({
imports: [DatabaseModule],
}).compile();
expect(testModule).toBeDefined();
});
});

View File

@@ -0,0 +1,30 @@
import { Test, TestingModule } from '@nestjs/testing';
import { HelloController } from './HelloController';
import { HelloModule } from './HelloModule';
import { HelloService } from './HelloService';
describe('HelloModule', () => {
let module: TestingModule;
beforeEach(async () => {
module = await Test.createTestingModule({
imports: [HelloModule],
}).compile();
});
it('should compile the module', () => {
expect(module).toBeDefined();
});
it('should provide HelloController', () => {
const controller = module.get<HelloController>(HelloController);
expect(controller).toBeDefined();
expect(controller).toBeInstanceOf(HelloController);
});
it('should provide HelloService', () => {
const service = module.get<HelloService>(HelloService);
expect(service).toBeDefined();
expect(service).toBeInstanceOf(HelloService);
});
});

View File

@@ -0,0 +1,210 @@
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
import { ForbiddenException } from '@nestjs/common';
import { requireLeagueAdminOrOwner } from './LeagueAuthorization';
// Mock the auth module
vi.mock('../auth/getActorFromRequestContext', () => ({
getActorFromRequestContext: vi.fn(),
}));
import { getActorFromRequestContext } from '../auth/getActorFromRequestContext';
describe('requireLeagueAdminOrOwner', () => {
const mockGetActorFromRequestContext = vi.mocked(getActorFromRequestContext);
const mockGetLeagueAdminPermissionsUseCase = {
execute: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should allow access for demo session role "league-admin"', async () => {
mockGetActorFromRequestContext.mockReturnValue({
userId: 'user-123',
driverId: 'driver-123',
role: 'league-admin',
});
await expect(
requireLeagueAdminOrOwner('league-123', mockGetLeagueAdminPermissionsUseCase)
).resolves.not.toThrow();
expect(mockGetLeagueAdminPermissionsUseCase.execute).not.toHaveBeenCalled();
});
it('should allow access for demo session role "league-owner"', async () => {
mockGetActorFromRequestContext.mockReturnValue({
userId: 'user-123',
driverId: 'driver-123',
role: 'league-owner',
});
await expect(
requireLeagueAdminOrOwner('league-123', mockGetLeagueAdminPermissionsUseCase)
).resolves.not.toThrow();
expect(mockGetLeagueAdminPermissionsUseCase.execute).not.toHaveBeenCalled();
});
it('should allow access for demo session role "super-admin"', async () => {
mockGetActorFromRequestContext.mockReturnValue({
userId: 'user-123',
driverId: 'driver-123',
role: 'super-admin',
});
await expect(
requireLeagueAdminOrOwner('league-123', mockGetLeagueAdminPermissionsUseCase)
).resolves.not.toThrow();
expect(mockGetLeagueAdminPermissionsUseCase.execute).not.toHaveBeenCalled();
});
it('should allow access for demo session role "system-owner"', async () => {
mockGetActorFromRequestContext.mockReturnValue({
userId: 'user-123',
driverId: 'driver-123',
role: 'system-owner',
});
await expect(
requireLeagueAdminOrOwner('league-123', mockGetLeagueAdminPermissionsUseCase)
).resolves.not.toThrow();
expect(mockGetLeagueAdminPermissionsUseCase.execute).not.toHaveBeenCalled();
});
it('should check permissions for non-demo roles', async () => {
mockGetActorFromRequestContext.mockReturnValue({
userId: 'user-123',
driverId: 'driver-123',
role: 'user',
});
const mockResult = {
isErr: () => false,
};
mockGetLeagueAdminPermissionsUseCase.execute.mockResolvedValue(mockResult);
await expect(
requireLeagueAdminOrOwner('league-123', mockGetLeagueAdminPermissionsUseCase)
).resolves.not.toThrow();
expect(mockGetLeagueAdminPermissionsUseCase.execute).toHaveBeenCalledWith({
leagueId: 'league-123',
performerDriverId: 'driver-123',
});
});
it('should throw ForbiddenException when permission check fails', async () => {
mockGetActorFromRequestContext.mockReturnValue({
userId: 'user-123',
driverId: 'driver-123',
role: 'user',
});
const mockResult = {
isErr: () => true,
};
mockGetLeagueAdminPermissionsUseCase.execute.mockResolvedValue(mockResult);
await expect(
requireLeagueAdminOrOwner('league-123', mockGetLeagueAdminPermissionsUseCase)
).rejects.toThrow(ForbiddenException);
expect(mockGetLeagueAdminPermissionsUseCase.execute).toHaveBeenCalledWith({
leagueId: 'league-123',
performerDriverId: 'driver-123',
});
});
it('should throw ForbiddenException with correct message', async () => {
mockGetActorFromRequestContext.mockReturnValue({
userId: 'user-123',
driverId: 'driver-123',
role: 'user',
});
const mockResult = {
isErr: () => true,
};
mockGetLeagueAdminPermissionsUseCase.execute.mockResolvedValue(mockResult);
try {
await requireLeagueAdminOrOwner('league-123', mockGetLeagueAdminPermissionsUseCase);
expect(true).toBe(false); // Should not reach here
} catch (error) {
expect(error).toBeInstanceOf(ForbiddenException);
expect(error.message).toBe('Forbidden');
}
});
it('should handle different league IDs', async () => {
mockGetActorFromRequestContext.mockReturnValue({
userId: 'user-123',
driverId: 'driver-123',
role: 'user',
});
const mockResult = {
isErr: () => false,
};
mockGetLeagueAdminPermissionsUseCase.execute.mockResolvedValue(mockResult);
await requireLeagueAdminOrOwner('league-456', mockGetLeagueAdminPermissionsUseCase);
expect(mockGetLeagueAdminPermissionsUseCase.execute).toHaveBeenCalledWith({
leagueId: 'league-456',
performerDriverId: 'driver-123',
});
});
it('should handle actor without role', async () => {
mockGetActorFromRequestContext.mockReturnValue({
userId: 'user-123',
driverId: 'driver-123',
role: undefined,
});
const mockResult = {
isErr: () => false,
};
mockGetLeagueAdminPermissionsUseCase.execute.mockResolvedValue(mockResult);
await expect(
requireLeagueAdminOrOwner('league-123', mockGetLeagueAdminPermissionsUseCase)
).resolves.not.toThrow();
expect(mockGetLeagueAdminPermissionsUseCase.execute).toHaveBeenCalled();
});
it('should handle actor with null role', async () => {
mockGetActorFromRequestContext.mockReturnValue({
userId: 'user-123',
driverId: 'driver-123',
role: null,
});
const mockResult = {
isErr: () => false,
};
mockGetLeagueAdminPermissionsUseCase.execute.mockResolvedValue(mockResult);
await expect(
requireLeagueAdminOrOwner('league-123', mockGetLeagueAdminPermissionsUseCase)
).resolves.not.toThrow();
expect(mockGetLeagueAdminPermissionsUseCase.execute).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,30 @@
import { Test, TestingModule } from '@nestjs/testing';
import { LeagueController } from './LeagueController';
import { LeagueModule } from './LeagueModule';
import { LeagueService } from './LeagueService';
describe('LeagueModule', () => {
let module: TestingModule;
beforeEach(async () => {
module = await Test.createTestingModule({
imports: [LeagueModule],
}).compile();
});
it('should compile the module', () => {
expect(module).toBeDefined();
});
it('should provide LeagueController', () => {
const controller = module.get<LeagueController>(LeagueController);
expect(controller).toBeDefined();
expect(controller).toBeInstanceOf(LeagueController);
});
it('should provide LeagueService', () => {
const service = module.get<LeagueService>(LeagueService);
expect(service).toBeDefined();
expect(service).toBeInstanceOf(LeagueService);
});
});

View File

@@ -0,0 +1,32 @@
import { Test, TestingModule } from '@nestjs/testing';
import { LoggingModule } from './LoggingModule';
describe('LoggingModule', () => {
let module: TestingModule;
beforeEach(async () => {
module = await Test.createTestingModule({
imports: [LoggingModule],
}).compile();
});
it('should compile the module', () => {
expect(module).toBeDefined();
});
it('should provide Logger provider', () => {
const logger = module.get('Logger');
expect(logger).toBeDefined();
});
it('should export Logger provider', () => {
const logger = module.get('Logger');
expect(logger).toBeDefined();
});
it('should be a global module', () => {
// Check if the module has the @Global() decorator by verifying it's registered globally
// In NestJS, global modules are automatically available to all other modules
expect(module).toBeDefined();
});
});

View File

@@ -0,0 +1,215 @@
import { Test, TestingModule } from '@nestjs/testing';
import { NotificationsController } from './NotificationsController';
import { NotificationsService } from './NotificationsService';
import { vi } from 'vitest';
import type { Request, Response } from 'express';
describe('NotificationsController', () => {
let controller: NotificationsController;
let service: ReturnType<typeof vi.mocked<NotificationsService>>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [NotificationsController],
providers: [
{
provide: NotificationsService,
useValue: {
getUnreadNotifications: vi.fn(),
getAllNotifications: vi.fn(),
markAsRead: vi.fn(),
},
},
],
}).compile();
controller = module.get<NotificationsController>(NotificationsController);
service = vi.mocked(module.get(NotificationsService));
});
describe('getUnreadNotifications', () => {
it('should return unread notifications for authenticated user', async () => {
const mockNotifications = [
{ id: '1', message: 'Test notification 1' },
{ id: '2', message: 'Test notification 2' },
];
service.getUnreadNotifications.mockResolvedValue(mockNotifications);
const mockReq = {
user: { userId: 'user-123' },
} as unknown as Request;
const mockRes = {
status: vi.fn().mockReturnThis(),
json: vi.fn(),
} as unknown as Response;
await controller.getUnreadNotifications(mockReq, mockRes);
expect(service.getUnreadNotifications).toHaveBeenCalledWith('user-123');
expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.json).toHaveBeenCalledWith({ notifications: mockNotifications });
});
it('should return 401 when user is not authenticated', async () => {
const mockReq = {} as unknown as Request;
const mockRes = {
status: vi.fn().mockReturnThis(),
json: vi.fn(),
} as unknown as Response;
await controller.getUnreadNotifications(mockReq, mockRes);
expect(service.getUnreadNotifications).not.toHaveBeenCalled();
expect(mockRes.status).toHaveBeenCalledWith(401);
expect(mockRes.json).toHaveBeenCalledWith({ error: 'Unauthorized' });
});
it('should return 401 when userId is missing', async () => {
const mockReq = {
user: {},
} as unknown as Request;
const mockRes = {
status: vi.fn().mockReturnThis(),
json: vi.fn(),
} as unknown as Response;
await controller.getUnreadNotifications(mockReq, mockRes);
expect(service.getUnreadNotifications).not.toHaveBeenCalled();
expect(mockRes.status).toHaveBeenCalledWith(401);
expect(mockRes.json).toHaveBeenCalledWith({ error: 'Unauthorized' });
});
});
describe('markAsRead', () => {
it('should mark notification as read for authenticated user', async () => {
service.markAsRead.mockResolvedValue(undefined);
const mockReq = {
user: { userId: 'user-123' },
} as unknown as Request;
const mockRes = {
status: vi.fn().mockReturnThis(),
json: vi.fn(),
} as unknown as Response;
await controller.markAsRead('notification-123', mockReq, mockRes);
expect(service.markAsRead).toHaveBeenCalledWith('notification-123', 'user-123');
expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.json).toHaveBeenCalledWith({ success: true });
});
it('should return 401 when user is not authenticated', async () => {
const mockReq = {} as unknown as Request;
const mockRes = {
status: vi.fn().mockReturnThis(),
json: vi.fn(),
} as unknown as Response;
await controller.markAsRead('notification-123', mockReq, mockRes);
expect(service.markAsRead).not.toHaveBeenCalled();
expect(mockRes.status).toHaveBeenCalledWith(401);
expect(mockRes.json).toHaveBeenCalledWith({ error: 'Unauthorized' });
});
it('should return 401 when userId is missing', async () => {
const mockReq = {
user: {},
} as unknown as Request;
const mockRes = {
status: vi.fn().mockReturnThis(),
json: vi.fn(),
} as unknown as Response;
await controller.markAsRead('notification-123', mockReq, mockRes);
expect(service.markAsRead).not.toHaveBeenCalled();
expect(mockRes.status).toHaveBeenCalledWith(401);
expect(mockRes.json).toHaveBeenCalledWith({ error: 'Unauthorized' });
});
});
describe('getAllNotifications', () => {
it('should return all notifications for authenticated user', async () => {
const mockNotifications = [
{ id: '1', message: 'Test notification 1' },
{ id: '2', message: 'Test notification 2' },
{ id: '3', message: 'Test notification 3' },
];
service.getAllNotifications.mockResolvedValue(mockNotifications);
const mockReq = {
user: { userId: 'user-123' },
} as unknown as Request;
const mockRes = {
status: vi.fn().mockReturnThis(),
json: vi.fn(),
} as unknown as Response;
await controller.getAllNotifications(mockReq, mockRes);
expect(service.getAllNotifications).toHaveBeenCalledWith('user-123');
expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.json).toHaveBeenCalledWith({ notifications: mockNotifications });
});
it('should return 401 when user is not authenticated', async () => {
const mockReq = {} as unknown as Request;
const mockRes = {
status: vi.fn().mockReturnThis(),
json: vi.fn(),
} as unknown as Response;
await controller.getAllNotifications(mockReq, mockRes);
expect(service.getAllNotifications).not.toHaveBeenCalled();
expect(mockRes.status).toHaveBeenCalledWith(401);
expect(mockRes.json).toHaveBeenCalledWith({ error: 'Unauthorized' });
});
it('should return 401 when userId is missing', async () => {
const mockReq = {
user: {},
} as unknown as Request;
const mockRes = {
status: vi.fn().mockReturnThis(),
json: vi.fn(),
} as unknown as Response;
await controller.getAllNotifications(mockReq, mockRes);
expect(service.getAllNotifications).not.toHaveBeenCalled();
expect(mockRes.status).toHaveBeenCalledWith(401);
expect(mockRes.json).toHaveBeenCalledWith({ error: 'Unauthorized' });
});
it('should handle empty notifications list', async () => {
service.getAllNotifications.mockResolvedValue([]);
const mockReq = {
user: { userId: 'user-123' },
} as unknown as Request;
const mockRes = {
status: vi.fn().mockReturnThis(),
json: vi.fn(),
} as unknown as Response;
await controller.getAllNotifications(mockReq, mockRes);
expect(service.getAllNotifications).toHaveBeenCalledWith('user-123');
expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.json).toHaveBeenCalledWith({ notifications: [] });
});
});
});

View File

@@ -0,0 +1,30 @@
import { Test, TestingModule } from '@nestjs/testing';
import { NotificationsController } from './NotificationsController';
import { NotificationsModule } from './NotificationsModule';
import { NotificationsService } from './NotificationsService';
describe('NotificationsModule', () => {
let module: TestingModule;
beforeEach(async () => {
module = await Test.createTestingModule({
imports: [NotificationsModule],
}).compile();
});
it('should compile the module', () => {
expect(module).toBeDefined();
});
it('should provide NotificationsController', () => {
const controller = module.get<NotificationsController>(NotificationsController);
expect(controller).toBeDefined();
expect(controller).toBeInstanceOf(NotificationsController);
});
it('should provide NotificationsService', () => {
const service = module.get<NotificationsService>(NotificationsService);
expect(service).toBeDefined();
expect(service).toBeInstanceOf(NotificationsService);
});
});

View File

@@ -0,0 +1,211 @@
import { Result } from '@core/shared/domain/Result';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { NotificationsService } from './NotificationsService';
describe('NotificationsService', () => {
const mockGetUnreadNotificationsUseCase = { execute: vi.fn() };
const mockGetAllNotificationsUseCase = { execute: vi.fn() };
const mockMarkNotificationReadUseCase = { execute: vi.fn() };
let service: NotificationsService;
beforeEach(() => {
vi.clearAllMocks();
service = new NotificationsService(
mockGetUnreadNotificationsUseCase as never,
mockGetAllNotificationsUseCase as never,
mockMarkNotificationReadUseCase as never,
);
});
describe('getUnreadNotifications', () => {
it('should return unread notifications on success', async () => {
const mockNotification = {
toJSON: () => ({ id: '1', message: 'Test notification' }),
};
mockGetUnreadNotificationsUseCase.execute.mockResolvedValue(
Result.ok({ notifications: [mockNotification] })
);
const result = await service.getUnreadNotifications('user-123');
expect(mockGetUnreadNotificationsUseCase.execute).toHaveBeenCalledWith({
recipientId: 'user-123',
});
expect(result).toEqual([{ id: '1', message: 'Test notification' }]);
});
it('should throw error when use case fails', async () => {
mockGetUnreadNotificationsUseCase.execute.mockResolvedValue(
Result.err({ code: 'ERROR', details: { message: 'Failed to get notifications' } })
);
await expect(service.getUnreadNotifications('user-123')).rejects.toThrow(
'Failed to get notifications'
);
});
it('should throw generic error when no message provided', async () => {
mockGetUnreadNotificationsUseCase.execute.mockResolvedValue(
Result.err({ code: 'ERROR', details: {} })
);
await expect(service.getUnreadNotifications('user-123')).rejects.toThrow(
'Failed to get unread notifications'
);
});
it('should handle empty notifications list', async () => {
mockGetUnreadNotificationsUseCase.execute.mockResolvedValue(
Result.ok({ notifications: [] })
);
const result = await service.getUnreadNotifications('user-123');
expect(result).toEqual([]);
});
it('should handle multiple notifications', async () => {
const mockNotifications = [
{ toJSON: () => ({ id: '1', message: 'Notification 1' }) },
{ toJSON: () => ({ id: '2', message: 'Notification 2' }) },
{ toJSON: () => ({ id: '3', message: 'Notification 3' }) },
];
mockGetUnreadNotificationsUseCase.execute.mockResolvedValue(
Result.ok({ notifications: mockNotifications })
);
const result = await service.getUnreadNotifications('user-123');
expect(result).toHaveLength(3);
expect(result[0]).toEqual({ id: '1', message: 'Notification 1' });
expect(result[1]).toEqual({ id: '2', message: 'Notification 2' });
expect(result[2]).toEqual({ id: '3', message: 'Notification 3' });
});
});
describe('getAllNotifications', () => {
it('should return all notifications on success', async () => {
const mockNotification = {
toJSON: () => ({ id: '1', message: 'Test notification' }),
};
mockGetAllNotificationsUseCase.execute.mockResolvedValue(
Result.ok({ notifications: [mockNotification] })
);
const result = await service.getAllNotifications('user-123');
expect(mockGetAllNotificationsUseCase.execute).toHaveBeenCalledWith({
recipientId: 'user-123',
});
expect(result).toEqual([{ id: '1', message: 'Test notification' }]);
});
it('should throw error when use case fails', async () => {
mockGetAllNotificationsUseCase.execute.mockResolvedValue(
Result.err({ code: 'ERROR', details: { message: 'Failed to get notifications' } })
);
await expect(service.getAllNotifications('user-123')).rejects.toThrow(
'Failed to get notifications'
);
});
it('should throw generic error when no message provided', async () => {
mockGetAllNotificationsUseCase.execute.mockResolvedValue(
Result.err({ code: 'ERROR', details: {} })
);
await expect(service.getAllNotifications('user-123')).rejects.toThrow(
'Failed to get all notifications'
);
});
it('should handle empty notifications list', async () => {
mockGetAllNotificationsUseCase.execute.mockResolvedValue(
Result.ok({ notifications: [] })
);
const result = await service.getAllNotifications('user-123');
expect(result).toEqual([]);
});
it('should handle multiple notifications', async () => {
const mockNotifications = [
{ toJSON: () => ({ id: '1', message: 'Notification 1' }) },
{ toJSON: () => ({ id: '2', message: 'Notification 2' }) },
{ toJSON: () => ({ id: '3', message: 'Notification 3' }) },
];
mockGetAllNotificationsUseCase.execute.mockResolvedValue(
Result.ok({ notifications: mockNotifications })
);
const result = await service.getAllNotifications('user-123');
expect(result).toHaveLength(3);
expect(result[0]).toEqual({ id: '1', message: 'Notification 1' });
expect(result[1]).toEqual({ id: '2', message: 'Notification 2' });
expect(result[2]).toEqual({ id: '3', message: 'Notification 3' });
});
});
describe('markAsRead', () => {
it('should mark notification as read on success', async () => {
mockMarkNotificationReadUseCase.execute.mockResolvedValue(Result.ok(undefined));
await service.markAsRead('notification-123', 'user-123');
expect(mockMarkNotificationReadUseCase.execute).toHaveBeenCalledWith({
notificationId: 'notification-123',
recipientId: 'user-123',
});
});
it('should throw error when use case fails', async () => {
mockMarkNotificationReadUseCase.execute.mockResolvedValue(
Result.err({ code: 'ERROR', details: { message: 'Failed to mark as read' } })
);
await expect(service.markAsRead('notification-123', 'user-123')).rejects.toThrow(
'Failed to mark as read'
);
});
it('should throw generic error when no message provided', async () => {
mockMarkNotificationReadUseCase.execute.mockResolvedValue(
Result.err({ code: 'ERROR', details: {} })
);
await expect(service.markAsRead('notification-123', 'user-123')).rejects.toThrow(
'Failed to mark notification as read'
);
});
it('should handle different notification IDs', async () => {
mockMarkNotificationReadUseCase.execute.mockResolvedValue(Result.ok(undefined));
await service.markAsRead('notification-456', 'user-123');
expect(mockMarkNotificationReadUseCase.execute).toHaveBeenCalledWith({
notificationId: 'notification-456',
recipientId: 'user-123',
});
});
it('should handle different user IDs', async () => {
mockMarkNotificationReadUseCase.execute.mockResolvedValue(Result.ok(undefined));
await service.markAsRead('notification-123', 'user-456');
expect(mockMarkNotificationReadUseCase.execute).toHaveBeenCalledWith({
notificationId: 'notification-123',
recipientId: 'user-456',
});
});
});
});

View File

@@ -0,0 +1,209 @@
import { AwardPrizePresenter } from './AwardPrizePresenter';
import { AwardPrizeResultDTO } from '../dtos/AwardPrizeDTO';
import { PrizeType } from '../dtos/PaymentsDto';
describe('AwardPrizePresenter', () => {
let presenter: AwardPrizePresenter;
beforeEach(() => {
presenter = new AwardPrizePresenter();
});
describe('present', () => {
it('should store the result', () => {
const result: AwardPrizeResultDTO = {
prize: {
id: 'prize-123',
leagueId: 'league-123',
seasonId: 'season-123',
position: 1,
name: 'Test Prize',
amount: 100,
type: PrizeType.CASH,
description: 'Test Description',
awarded: false,
createdAt: new Date('2024-01-01'),
},
};
presenter.present(result);
expect(presenter.getResponseModel()).toEqual(result);
});
it('should overwrite previous result', () => {
const firstResult: AwardPrizeResultDTO = {
prize: {
id: 'prize-123',
leagueId: 'league-123',
seasonId: 'season-123',
position: 1,
name: 'Test Prize',
amount: 100,
type: PrizeType.CASH,
description: 'Test Description',
awarded: false,
createdAt: new Date('2024-01-01'),
},
};
const secondResult: AwardPrizeResultDTO = {
prize: {
id: 'prize-456',
leagueId: 'league-456',
seasonId: 'season-456',
position: 2,
name: 'Another Prize',
amount: 200,
type: PrizeType.MERCHANDISE,
description: 'Another Description',
awarded: false,
createdAt: new Date('2024-01-02'),
},
};
presenter.present(firstResult);
presenter.present(secondResult);
expect(presenter.getResponseModel()).toEqual(secondResult);
});
});
describe('getResponseModel', () => {
it('should return null when not presented', () => {
expect(presenter.getResponseModel()).toBeNull();
});
it('should return the result after present()', () => {
const result: AwardPrizeResultDTO = {
prize: {
id: 'prize-123',
leagueId: 'league-123',
seasonId: 'season-123',
position: 1,
name: 'Test Prize',
amount: 100,
type: PrizeType.CASH,
description: 'Test Description',
awarded: false,
createdAt: new Date('2024-01-01'),
},
};
presenter.present(result);
expect(presenter.getResponseModel()).toEqual(result);
});
});
describe('reset', () => {
it('should clear the result', () => {
const result: AwardPrizeResultDTO = {
prize: {
id: 'prize-123',
leagueId: 'league-123',
seasonId: 'season-123',
position: 1,
name: 'Test Prize',
amount: 100,
type: PrizeType.CASH,
description: 'Test Description',
awarded: false,
createdAt: new Date('2024-01-01'),
},
};
presenter.present(result);
presenter.reset();
expect(presenter.getResponseModel()).toBeNull();
});
it('should allow presenting again after reset', () => {
const firstResult: AwardPrizeResultDTO = {
prize: {
id: 'prize-123',
leagueId: 'league-123',
seasonId: 'season-123',
position: 1,
name: 'Test Prize',
amount: 100,
type: PrizeType.CASH,
description: 'Test Description',
awarded: false,
createdAt: new Date('2024-01-01'),
},
};
const secondResult: AwardPrizeResultDTO = {
prize: {
id: 'prize-456',
leagueId: 'league-456',
seasonId: 'season-456',
position: 2,
name: 'Another Prize',
amount: 200,
type: PrizeType.MERCHANDISE,
description: 'Another Description',
awarded: false,
createdAt: new Date('2024-01-02'),
},
};
presenter.present(firstResult);
presenter.reset();
presenter.present(secondResult);
expect(presenter.getResponseModel()).toEqual(secondResult);
});
});
describe('viewModel', () => {
it('should return the result', () => {
const result: AwardPrizeResultDTO = {
prize: {
id: 'prize-123',
leagueId: 'league-123',
seasonId: 'season-123',
position: 1,
name: 'Test Prize',
amount: 100,
type: PrizeType.CASH,
description: 'Test Description',
awarded: false,
createdAt: new Date('2024-01-01'),
},
};
presenter.present(result);
expect(presenter.viewModel).toEqual(result);
});
it('should throw error when accessed before present()', () => {
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
it('should throw error after reset', () => {
const result: AwardPrizeResultDTO = {
prize: {
id: 'prize-123',
leagueId: 'league-123',
seasonId: 'season-123',
position: 1,
name: 'Test Prize',
amount: 100,
type: PrizeType.CASH,
description: 'Test Description',
awarded: false,
createdAt: new Date('2024-01-01'),
},
};
presenter.present(result);
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
});
});

View File

@@ -0,0 +1,229 @@
import { CreatePaymentPresenter } from './CreatePaymentPresenter';
import { CreatePaymentOutput } from '../dtos/PaymentsDto';
describe('CreatePaymentPresenter', () => {
let presenter: CreatePaymentPresenter;
beforeEach(() => {
presenter = new CreatePaymentPresenter();
});
describe('present', () => {
it('should map result to response model', () => {
const result = {
payment: {
id: 'payment-123',
type: 'membership',
amount: 100,
platformFee: 5,
netAmount: 95,
payerId: 'user-123',
payerType: 'driver',
leagueId: 'league-123',
status: 'pending',
createdAt: new Date('2024-01-01'),
},
};
presenter.present(result);
const responseModel = presenter.getResponseModel();
expect(responseModel).toEqual({
payment: {
id: 'payment-123',
type: 'membership',
amount: 100,
platformFee: 5,
netAmount: 95,
payerId: 'user-123',
payerType: 'driver',
leagueId: 'league-123',
status: 'pending',
createdAt: new Date('2024-01-01'),
},
});
});
it('should include seasonId when provided', () => {
const result = {
payment: {
id: 'payment-123',
type: 'membership',
amount: 100,
platformFee: 5,
netAmount: 95,
payerId: 'user-123',
payerType: 'driver',
leagueId: 'league-123',
seasonId: 'season-123',
status: 'pending',
createdAt: new Date('2024-01-01'),
},
};
presenter.present(result);
const responseModel = presenter.getResponseModel();
expect(responseModel.payment.seasonId).toBe('season-123');
});
it('should include completedAt when provided', () => {
const result = {
payment: {
id: 'payment-123',
type: 'membership',
amount: 100,
platformFee: 5,
netAmount: 95,
payerId: 'user-123',
payerType: 'driver',
leagueId: 'league-123',
status: 'completed',
createdAt: new Date('2024-01-01'),
completedAt: new Date('2024-01-02'),
},
};
presenter.present(result);
const responseModel = presenter.getResponseModel();
expect(responseModel.payment.completedAt).toEqual(new Date('2024-01-02'));
});
it('should not include seasonId when not provided', () => {
const result = {
payment: {
id: 'payment-123',
type: 'membership',
amount: 100,
platformFee: 5,
netAmount: 95,
payerId: 'user-123',
payerType: 'driver',
leagueId: 'league-123',
status: 'pending',
createdAt: new Date('2024-01-01'),
},
};
presenter.present(result);
const responseModel = presenter.getResponseModel();
expect(responseModel.payment.seasonId).toBeUndefined();
});
it('should not include completedAt when not provided', () => {
const result = {
payment: {
id: 'payment-123',
type: 'membership',
amount: 100,
platformFee: 5,
netAmount: 95,
payerId: 'user-123',
payerType: 'driver',
leagueId: 'league-123',
status: 'pending',
createdAt: new Date('2024-01-01'),
},
};
presenter.present(result);
const responseModel = presenter.getResponseModel();
expect(responseModel.payment.completedAt).toBeUndefined();
});
});
describe('getResponseModel', () => {
it('should throw error when accessed before present()', () => {
expect(() => presenter.getResponseModel()).toThrow('Presenter not presented');
});
it('should return model after present()', () => {
const result = {
payment: {
id: 'payment-123',
type: 'membership',
amount: 100,
platformFee: 5,
netAmount: 95,
payerId: 'user-123',
payerType: 'driver',
leagueId: 'league-123',
status: 'pending',
createdAt: new Date('2024-01-01'),
},
};
presenter.present(result);
const responseModel = presenter.getResponseModel();
expect(responseModel).toBeDefined();
expect(responseModel.payment.id).toBe('payment-123');
});
});
describe('reset', () => {
it('should clear the response model', () => {
const result = {
payment: {
id: 'payment-123',
type: 'membership',
amount: 100,
platformFee: 5,
netAmount: 95,
payerId: 'user-123',
payerType: 'driver',
leagueId: 'league-123',
status: 'pending',
createdAt: new Date('2024-01-01'),
},
};
presenter.present(result);
presenter.reset();
expect(() => presenter.getResponseModel()).toThrow('Presenter not presented');
});
it('should allow presenting again after reset', () => {
const firstResult = {
payment: {
id: 'payment-123',
type: 'membership',
amount: 100,
platformFee: 5,
netAmount: 95,
payerId: 'user-123',
payerType: 'driver',
leagueId: 'league-123',
status: 'pending',
createdAt: new Date('2024-01-01'),
},
};
const secondResult = {
payment: {
id: 'payment-456',
type: 'membership',
amount: 200,
platformFee: 10,
netAmount: 190,
payerId: 'user-456',
payerType: 'driver',
leagueId: 'league-456',
status: 'pending',
createdAt: new Date('2024-01-02'),
},
};
presenter.present(firstResult);
presenter.reset();
presenter.present(secondResult);
const responseModel = presenter.getResponseModel();
expect(responseModel.payment.id).toBe('payment-456');
});
});
});

View File

@@ -0,0 +1,209 @@
import { CreatePrizePresenter } from './CreatePrizePresenter';
import { CreatePrizeResultDTO } from '../dtos/CreatePrizeDTO';
import { PrizeType } from '../dtos/PaymentsDto';
describe('CreatePrizePresenter', () => {
let presenter: CreatePrizePresenter;
beforeEach(() => {
presenter = new CreatePrizePresenter();
});
describe('present', () => {
it('should store the result', () => {
const result: CreatePrizeResultDTO = {
prize: {
id: 'prize-123',
leagueId: 'league-123',
seasonId: 'season-123',
position: 1,
name: 'Test Prize',
amount: 100,
type: PrizeType.CASH,
description: 'Test Description',
awarded: false,
createdAt: new Date('2024-01-01'),
},
};
presenter.present(result);
expect(presenter.getResponseModel()).toEqual(result);
});
it('should overwrite previous result', () => {
const firstResult: CreatePrizeResultDTO = {
prize: {
id: 'prize-123',
leagueId: 'league-123',
seasonId: 'season-123',
position: 1,
name: 'Test Prize',
amount: 100,
type: PrizeType.CASH,
description: 'Test Description',
awarded: false,
createdAt: new Date('2024-01-01'),
},
};
const secondResult: CreatePrizeResultDTO = {
prize: {
id: 'prize-456',
leagueId: 'league-456',
seasonId: 'season-456',
position: 2,
name: 'Another Prize',
amount: 200,
type: PrizeType.MERCHANDISE,
description: 'Another Description',
awarded: false,
createdAt: new Date('2024-01-02'),
},
};
presenter.present(firstResult);
presenter.present(secondResult);
expect(presenter.getResponseModel()).toEqual(secondResult);
});
});
describe('getResponseModel', () => {
it('should return null when not presented', () => {
expect(presenter.getResponseModel()).toBeNull();
});
it('should return the result after present()', () => {
const result: CreatePrizeResultDTO = {
prize: {
id: 'prize-123',
leagueId: 'league-123',
seasonId: 'season-123',
position: 1,
name: 'Test Prize',
amount: 100,
type: PrizeType.CASH,
description: 'Test Description',
awarded: false,
createdAt: new Date('2024-01-01'),
},
};
presenter.present(result);
expect(presenter.getResponseModel()).toEqual(result);
});
});
describe('reset', () => {
it('should clear the result', () => {
const result: CreatePrizeResultDTO = {
prize: {
id: 'prize-123',
leagueId: 'league-123',
seasonId: 'season-123',
position: 1,
name: 'Test Prize',
amount: 100,
type: PrizeType.CASH,
description: 'Test Description',
awarded: false,
createdAt: new Date('2024-01-01'),
},
};
presenter.present(result);
presenter.reset();
expect(presenter.getResponseModel()).toBeNull();
});
it('should allow presenting again after reset', () => {
const firstResult: CreatePrizeResultDTO = {
prize: {
id: 'prize-123',
leagueId: 'league-123',
seasonId: 'season-123',
position: 1,
name: 'Test Prize',
amount: 100,
type: PrizeType.CASH,
description: 'Test Description',
awarded: false,
createdAt: new Date('2024-01-01'),
},
};
const secondResult: CreatePrizeResultDTO = {
prize: {
id: 'prize-456',
leagueId: 'league-456',
seasonId: 'season-456',
position: 2,
name: 'Another Prize',
amount: 200,
type: PrizeType.MERCHANDISE,
description: 'Another Description',
awarded: false,
createdAt: new Date('2024-01-02'),
},
};
presenter.present(firstResult);
presenter.reset();
presenter.present(secondResult);
expect(presenter.getResponseModel()).toEqual(secondResult);
});
});
describe('viewModel', () => {
it('should return the result', () => {
const result: CreatePrizeResultDTO = {
prize: {
id: 'prize-123',
leagueId: 'league-123',
seasonId: 'season-123',
position: 1,
name: 'Test Prize',
amount: 100,
type: PrizeType.CASH,
description: 'Test Description',
awarded: false,
createdAt: new Date('2024-01-01'),
},
};
presenter.present(result);
expect(presenter.viewModel).toEqual(result);
});
it('should throw error when accessed before present()', () => {
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
it('should throw error after reset', () => {
const result: CreatePrizeResultDTO = {
prize: {
id: 'prize-123',
leagueId: 'league-123',
seasonId: 'season-123',
position: 1,
name: 'Test Prize',
amount: 100,
type: PrizeType.CASH,
description: 'Test Description',
awarded: false,
createdAt: new Date('2024-01-01'),
},
};
presenter.present(result);
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
});
});

View File

@@ -0,0 +1,109 @@
import { DeletePrizePresenter } from './DeletePrizePresenter';
import { DeletePrizeResultDTO } from '../dtos/DeletePrizeDTO';
describe('DeletePrizePresenter', () => {
let presenter: DeletePrizePresenter;
beforeEach(() => {
presenter = new DeletePrizePresenter();
});
describe('present', () => {
it('should store the result', () => {
const result: DeletePrizeResultDTO = {
success: true,
};
presenter.present(result);
expect(presenter.getResponseModel()).toEqual(result);
});
it('should overwrite previous result', () => {
const firstResult: DeletePrizeResultDTO = {
success: true,
};
const secondResult: DeletePrizeResultDTO = {
success: false,
};
presenter.present(firstResult);
presenter.present(secondResult);
expect(presenter.getResponseModel()).toEqual(secondResult);
});
});
describe('getResponseModel', () => {
it('should return null when not presented', () => {
expect(presenter.getResponseModel()).toBeNull();
});
it('should return the result after present()', () => {
const result: DeletePrizeResultDTO = {
success: true,
};
presenter.present(result);
expect(presenter.getResponseModel()).toEqual(result);
});
});
describe('reset', () => {
it('should clear the result', () => {
const result: DeletePrizeResultDTO = {
success: true,
};
presenter.present(result);
presenter.reset();
expect(presenter.getResponseModel()).toBeNull();
});
it('should allow presenting again after reset', () => {
const firstResult: DeletePrizeResultDTO = {
success: true,
};
const secondResult: DeletePrizeResultDTO = {
success: false,
};
presenter.present(firstResult);
presenter.reset();
presenter.present(secondResult);
expect(presenter.getResponseModel()).toEqual(secondResult);
});
});
describe('viewModel', () => {
it('should return the result', () => {
const result: DeletePrizeResultDTO = {
success: true,
};
presenter.present(result);
expect(presenter.viewModel).toEqual(result);
});
it('should throw error when accessed before present()', () => {
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
it('should throw error after reset', () => {
const result: DeletePrizeResultDTO = {
success: true,
};
presenter.present(result);
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
});
});

View File

@@ -0,0 +1,191 @@
import { GetMembershipFeesPresenter } from './GetMembershipFeesPresenter';
import { GetMembershipFeesResultDTO } from '../dtos/GetMembershipFeesDTO';
import { MembershipFeeType, MemberPaymentStatus } from '../dtos/PaymentsDto';
describe('GetMembershipFeesPresenter', () => {
let presenter: GetMembershipFeesPresenter;
beforeEach(() => {
presenter = new GetMembershipFeesPresenter();
});
describe('present', () => {
it('should store the result', () => {
const result: GetMembershipFeesResultDTO = {
fee: {
id: 'fee-123',
leagueId: 'league-123',
type: MembershipFeeType.MONTHLY,
amount: 100,
enabled: true,
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-01'),
},
payments: [],
};
presenter.present(result);
expect(presenter.getResponseModel()).toEqual(result);
});
it('should overwrite previous result', () => {
const firstResult: GetMembershipFeesResultDTO = {
fee: {
id: 'fee-123',
leagueId: 'league-123',
type: MembershipFeeType.MONTHLY,
amount: 100,
enabled: true,
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-01'),
},
payments: [],
};
const secondResult: GetMembershipFeesResultDTO = {
fee: {
id: 'fee-456',
leagueId: 'league-456',
type: MembershipFeeType.SEASON,
amount: 200,
enabled: true,
createdAt: new Date('2024-01-02'),
updatedAt: new Date('2024-01-02'),
},
payments: [],
};
presenter.present(firstResult);
presenter.present(secondResult);
expect(presenter.getResponseModel()).toEqual(secondResult);
});
});
describe('getResponseModel', () => {
it('should return null when not presented', () => {
expect(presenter.getResponseModel()).toBeNull();
});
it('should return the result after present()', () => {
const result: GetMembershipFeesResultDTO = {
fee: {
id: 'fee-123',
leagueId: 'league-123',
type: MembershipFeeType.MONTHLY,
amount: 100,
enabled: true,
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-01'),
},
payments: [],
};
presenter.present(result);
expect(presenter.getResponseModel()).toEqual(result);
});
});
describe('reset', () => {
it('should clear the result', () => {
const result: GetMembershipFeesResultDTO = {
fee: {
id: 'fee-123',
leagueId: 'league-123',
type: MembershipFeeType.MONTHLY,
amount: 100,
enabled: true,
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-01'),
},
payments: [],
};
presenter.present(result);
presenter.reset();
expect(presenter.getResponseModel()).toBeNull();
});
it('should allow presenting again after reset', () => {
const firstResult: GetMembershipFeesResultDTO = {
fee: {
id: 'fee-123',
leagueId: 'league-123',
type: MembershipFeeType.MONTHLY,
amount: 100,
enabled: true,
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-01'),
},
payments: [],
};
const secondResult: GetMembershipFeesResultDTO = {
fee: {
id: 'fee-456',
leagueId: 'league-456',
type: MembershipFeeType.SEASON,
amount: 200,
enabled: true,
createdAt: new Date('2024-01-02'),
updatedAt: new Date('2024-01-02'),
},
payments: [],
};
presenter.present(firstResult);
presenter.reset();
presenter.present(secondResult);
expect(presenter.getResponseModel()).toEqual(secondResult);
});
});
describe('viewModel', () => {
it('should return the result', () => {
const result: GetMembershipFeesResultDTO = {
fee: {
id: 'fee-123',
leagueId: 'league-123',
type: MembershipFeeType.MONTHLY,
amount: 100,
enabled: true,
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-01'),
},
payments: [],
};
presenter.present(result);
expect(presenter.viewModel).toEqual(result);
});
it('should throw error when accessed before present()', () => {
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
it('should throw error after reset', () => {
const result: GetMembershipFeesResultDTO = {
fee: {
id: 'fee-123',
leagueId: 'league-123',
type: MembershipFeeType.MONTHLY,
amount: 100,
enabled: true,
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-01'),
},
payments: [],
};
presenter.present(result);
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
});
});

View File

@@ -0,0 +1,299 @@
import { GetPaymentsPresenter } from './GetPaymentsPresenter';
import { GetPaymentsOutput } from '../dtos/PaymentsDto';
describe('GetPaymentsPresenter', () => {
let presenter: GetPaymentsPresenter;
beforeEach(() => {
presenter = new GetPaymentsPresenter();
});
describe('present', () => {
it('should map result to response model', () => {
const result = {
payments: [
{
id: 'payment-123',
type: 'membership',
amount: 100,
platformFee: 5,
netAmount: 95,
payerId: 'user-123',
payerType: 'driver',
leagueId: 'league-123',
status: 'pending',
createdAt: new Date('2024-01-01'),
},
],
};
presenter.present(result);
const responseModel = presenter.getResponseModel();
expect(responseModel).toEqual({
payments: [
{
id: 'payment-123',
type: 'membership',
amount: 100,
platformFee: 5,
netAmount: 95,
payerId: 'user-123',
payerType: 'driver',
leagueId: 'league-123',
status: 'pending',
createdAt: new Date('2024-01-01'),
},
],
});
});
it('should include seasonId when provided', () => {
const result = {
payments: [
{
id: 'payment-123',
type: 'membership',
amount: 100,
platformFee: 5,
netAmount: 95,
payerId: 'user-123',
payerType: 'driver',
leagueId: 'league-123',
seasonId: 'season-123',
status: 'pending',
createdAt: new Date('2024-01-01'),
},
],
};
presenter.present(result);
const responseModel = presenter.getResponseModel();
expect(responseModel.payments[0].seasonId).toBe('season-123');
});
it('should include completedAt when provided', () => {
const result = {
payments: [
{
id: 'payment-123',
type: 'membership',
amount: 100,
platformFee: 5,
netAmount: 95,
payerId: 'user-123',
payerType: 'driver',
leagueId: 'league-123',
status: 'completed',
createdAt: new Date('2024-01-01'),
completedAt: new Date('2024-01-02'),
},
],
};
presenter.present(result);
const responseModel = presenter.getResponseModel();
expect(responseModel.payments[0].completedAt).toEqual(new Date('2024-01-02'));
});
it('should not include seasonId when not provided', () => {
const result = {
payments: [
{
id: 'payment-123',
type: 'membership',
amount: 100,
platformFee: 5,
netAmount: 95,
payerId: 'user-123',
payerType: 'driver',
leagueId: 'league-123',
status: 'pending',
createdAt: new Date('2024-01-01'),
},
],
};
presenter.present(result);
const responseModel = presenter.getResponseModel();
expect(responseModel.payments[0].seasonId).toBeUndefined();
});
it('should not include completedAt when not provided', () => {
const result = {
payments: [
{
id: 'payment-123',
type: 'membership',
amount: 100,
platformFee: 5,
netAmount: 95,
payerId: 'user-123',
payerType: 'driver',
leagueId: 'league-123',
status: 'pending',
createdAt: new Date('2024-01-01'),
},
],
};
presenter.present(result);
const responseModel = presenter.getResponseModel();
expect(responseModel.payments[0].completedAt).toBeUndefined();
});
it('should handle empty payments list', () => {
const result = {
payments: [],
};
presenter.present(result);
const responseModel = presenter.getResponseModel();
expect(responseModel.payments).toEqual([]);
});
it('should handle multiple payments', () => {
const result = {
payments: [
{
id: 'payment-123',
type: 'membership',
amount: 100,
platformFee: 5,
netAmount: 95,
payerId: 'user-123',
payerType: 'driver',
leagueId: 'league-123',
status: 'pending',
createdAt: new Date('2024-01-01'),
},
{
id: 'payment-456',
type: 'membership',
amount: 200,
platformFee: 10,
netAmount: 190,
payerId: 'user-456',
payerType: 'driver',
leagueId: 'league-456',
status: 'completed',
createdAt: new Date('2024-01-02'),
completedAt: new Date('2024-01-03'),
},
],
};
presenter.present(result);
const responseModel = presenter.getResponseModel();
expect(responseModel.payments).toHaveLength(2);
expect(responseModel.payments[0].id).toBe('payment-123');
expect(responseModel.payments[1].id).toBe('payment-456');
});
});
describe('getResponseModel', () => {
it('should throw error when accessed before present()', () => {
expect(() => presenter.getResponseModel()).toThrow('Presenter not presented');
});
it('should return model after present()', () => {
const result = {
payments: [
{
id: 'payment-123',
type: 'membership',
amount: 100,
platformFee: 5,
netAmount: 95,
payerId: 'user-123',
payerType: 'driver',
leagueId: 'league-123',
status: 'pending',
createdAt: new Date('2024-01-01'),
},
],
};
presenter.present(result);
const responseModel = presenter.getResponseModel();
expect(responseModel).toBeDefined();
expect(responseModel.payments[0].id).toBe('payment-123');
});
});
describe('reset', () => {
it('should clear the response model', () => {
const result = {
payments: [
{
id: 'payment-123',
type: 'membership',
amount: 100,
platformFee: 5,
netAmount: 95,
payerId: 'user-123',
payerType: 'driver',
leagueId: 'league-123',
status: 'pending',
createdAt: new Date('2024-01-01'),
},
],
};
presenter.present(result);
presenter.reset();
expect(() => presenter.getResponseModel()).toThrow('Presenter not presented');
});
it('should allow presenting again after reset', () => {
const firstResult = {
payments: [
{
id: 'payment-123',
type: 'membership',
amount: 100,
platformFee: 5,
netAmount: 95,
payerId: 'user-123',
payerType: 'driver',
leagueId: 'league-123',
status: 'pending',
createdAt: new Date('2024-01-01'),
},
],
};
const secondResult = {
payments: [
{
id: 'payment-456',
type: 'membership',
amount: 200,
platformFee: 10,
netAmount: 190,
payerId: 'user-456',
payerType: 'driver',
leagueId: 'league-456',
status: 'pending',
createdAt: new Date('2024-01-02'),
},
],
};
presenter.present(firstResult);
presenter.reset();
presenter.present(secondResult);
const responseModel = presenter.getResponseModel();
expect(responseModel.payments[0].id).toBe('payment-456');
});
});
});

View File

@@ -0,0 +1,191 @@
import { GetPrizesPresenter } from './GetPrizesPresenter';
import { GetPrizesResultDTO } from '../dtos/GetPrizesDTO';
import { PrizeType } from '../dtos/PrizeType';
describe('GetPrizesPresenter', () => {
let presenter: GetPrizesPresenter;
beforeEach(() => {
presenter = new GetPrizesPresenter();
});
describe('present', () => {
it('should store the result', () => {
const result: GetPrizesResultDTO = {
prizes: [
{
id: 'prize-123',
name: 'Test Prize',
description: 'Test Description',
type: PrizeType.CASH,
amount: 100,
leagueId: 'league-123',
},
],
};
presenter.present(result);
expect(presenter.getResponseModel()).toEqual(result);
});
it('should overwrite previous result', () => {
const firstResult: GetPrizesResultDTO = {
prizes: [
{
id: 'prize-123',
name: 'Test Prize',
description: 'Test Description',
type: PrizeType.CASH,
amount: 100,
leagueId: 'league-123',
},
],
};
const secondResult: GetPrizesResultDTO = {
prizes: [
{
id: 'prize-456',
name: 'Test Prize 2',
description: 'Test Description 2',
type: PrizeType.MERCHANDISE,
amount: 200,
leagueId: 'league-456',
},
],
};
presenter.present(firstResult);
presenter.present(secondResult);
expect(presenter.getResponseModel()).toEqual(secondResult);
});
});
describe('getResponseModel', () => {
it('should return null when not presented', () => {
expect(presenter.getResponseModel()).toBeNull();
});
it('should return the result after present()', () => {
const result: GetPrizesResultDTO = {
prizes: [
{
id: 'prize-123',
name: 'Test Prize',
description: 'Test Description',
type: PrizeType.CASH,
amount: 100,
leagueId: 'league-123',
},
],
};
presenter.present(result);
expect(presenter.getResponseModel()).toEqual(result);
});
});
describe('reset', () => {
it('should clear the result', () => {
const result: GetPrizesResultDTO = {
prizes: [
{
id: 'prize-123',
name: 'Test Prize',
description: 'Test Description',
type: PrizeType.CASH,
amount: 100,
leagueId: 'league-123',
},
],
};
presenter.present(result);
presenter.reset();
expect(presenter.getResponseModel()).toBeNull();
});
it('should allow presenting again after reset', () => {
const firstResult: GetPrizesResultDTO = {
prizes: [
{
id: 'prize-123',
name: 'Test Prize',
description: 'Test Description',
type: PrizeType.CASH,
amount: 100,
leagueId: 'league-123',
},
],
};
const secondResult: GetPrizesResultDTO = {
prizes: [
{
id: 'prize-456',
name: 'Test Prize 2',
description: 'Test Description 2',
type: PrizeType.MERCHANDISE,
amount: 200,
leagueId: 'league-456',
},
],
};
presenter.present(firstResult);
presenter.reset();
presenter.present(secondResult);
expect(presenter.getResponseModel()).toEqual(secondResult);
});
});
describe('viewModel', () => {
it('should return the result', () => {
const result: GetPrizesResultDTO = {
prizes: [
{
id: 'prize-123',
name: 'Test Prize',
description: 'Test Description',
type: PrizeType.CASH,
amount: 100,
leagueId: 'league-123',
},
],
};
presenter.present(result);
expect(presenter.viewModel).toEqual(result);
});
it('should throw error when accessed before present()', () => {
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
it('should throw error after reset', () => {
const result: GetPrizesResultDTO = {
prizes: [
{
id: 'prize-123',
name: 'Test Prize',
description: 'Test Description',
type: PrizeType.CASH,
amount: 100,
leagueId: 'league-123',
},
],
};
presenter.present(result);
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
});
});

View File

@@ -0,0 +1,199 @@
import { GetWalletPresenter } from './GetWalletPresenter';
import { GetWalletResultDTO } from '../dtos/GetWalletDTO';
describe('GetWalletPresenter', () => {
let presenter: GetWalletPresenter;
beforeEach(() => {
presenter = new GetWalletPresenter();
});
describe('present', () => {
it('should store the result', () => {
const result: GetWalletResultDTO = {
wallet: {
id: 'wallet-123',
leagueId: 'league-123',
balance: 1000,
totalRevenue: 5000,
totalPlatformFees: 250,
totalWithdrawn: 3000,
createdAt: new Date('2024-01-01'),
currency: 'USD',
},
transactions: [],
};
presenter.present(result);
expect(presenter.getResponseModel()).toEqual(result);
});
it('should overwrite previous result', () => {
const firstResult: GetWalletResultDTO = {
wallet: {
id: 'wallet-123',
leagueId: 'league-123',
balance: 1000,
totalRevenue: 5000,
totalPlatformFees: 250,
totalWithdrawn: 3000,
createdAt: new Date('2024-01-01'),
currency: 'USD',
},
transactions: [],
};
const secondResult: GetWalletResultDTO = {
wallet: {
id: 'wallet-456',
leagueId: 'league-456',
balance: 2000,
totalRevenue: 10000,
totalPlatformFees: 500,
totalWithdrawn: 6000,
createdAt: new Date('2024-01-02'),
currency: 'EUR',
},
transactions: [],
};
presenter.present(firstResult);
presenter.present(secondResult);
expect(presenter.getResponseModel()).toEqual(secondResult);
});
});
describe('getResponseModel', () => {
it('should return null when not presented', () => {
expect(presenter.getResponseModel()).toBeNull();
});
it('should return the result after present()', () => {
const result: GetWalletResultDTO = {
wallet: {
id: 'wallet-123',
leagueId: 'league-123',
balance: 1000,
totalRevenue: 5000,
totalPlatformFees: 250,
totalWithdrawn: 3000,
createdAt: new Date('2024-01-01'),
currency: 'USD',
},
transactions: [],
};
presenter.present(result);
expect(presenter.getResponseModel()).toEqual(result);
});
});
describe('reset', () => {
it('should clear the result', () => {
const result: GetWalletResultDTO = {
wallet: {
id: 'wallet-123',
leagueId: 'league-123',
balance: 1000,
totalRevenue: 5000,
totalPlatformFees: 250,
totalWithdrawn: 3000,
createdAt: new Date('2024-01-01'),
currency: 'USD',
},
transactions: [],
};
presenter.present(result);
presenter.reset();
expect(presenter.getResponseModel()).toBeNull();
});
it('should allow presenting again after reset', () => {
const firstResult: GetWalletResultDTO = {
wallet: {
id: 'wallet-123',
leagueId: 'league-123',
balance: 1000,
totalRevenue: 5000,
totalPlatformFees: 250,
totalWithdrawn: 3000,
createdAt: new Date('2024-01-01'),
currency: 'USD',
},
transactions: [],
};
const secondResult: GetWalletResultDTO = {
wallet: {
id: 'wallet-456',
leagueId: 'league-456',
balance: 2000,
totalRevenue: 10000,
totalPlatformFees: 500,
totalWithdrawn: 6000,
createdAt: new Date('2024-01-02'),
currency: 'EUR',
},
transactions: [],
};
presenter.present(firstResult);
presenter.reset();
presenter.present(secondResult);
expect(presenter.getResponseModel()).toEqual(secondResult);
});
});
describe('viewModel', () => {
it('should return the result', () => {
const result: GetWalletResultDTO = {
wallet: {
id: 'wallet-123',
leagueId: 'league-123',
balance: 1000,
totalRevenue: 5000,
totalPlatformFees: 250,
totalWithdrawn: 3000,
createdAt: new Date('2024-01-01'),
currency: 'USD',
},
transactions: [],
};
presenter.present(result);
expect(presenter.viewModel).toEqual(result);
});
it('should throw error when accessed before present()', () => {
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
it('should throw error after reset', () => {
const result: GetWalletResultDTO = {
wallet: {
id: 'wallet-123',
leagueId: 'league-123',
balance: 1000,
totalRevenue: 5000,
totalPlatformFees: 250,
totalWithdrawn: 3000,
createdAt: new Date('2024-01-01'),
currency: 'USD',
},
transactions: [],
};
presenter.present(result);
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
});
});

View File

@@ -0,0 +1,263 @@
import { ProcessWalletTransactionPresenter } from './ProcessWalletTransactionPresenter';
import { ProcessWalletTransactionResultDTO } from '../dtos/ProcessWalletTransactionDTO';
import { TransactionType } from '../dtos/TransactionType';
describe('ProcessWalletTransactionPresenter', () => {
let presenter: ProcessWalletTransactionPresenter;
beforeEach(() => {
presenter = new ProcessWalletTransactionPresenter();
});
describe('present', () => {
it('should store the result', () => {
const result: ProcessWalletTransactionResultDTO = {
wallet: {
id: 'wallet-123',
leagueId: 'league-123',
balance: 1000,
totalRevenue: 5000,
totalPlatformFees: 250,
totalWithdrawn: 3000,
createdAt: new Date('2024-01-01'),
currency: 'USD',
},
transaction: {
id: 'transaction-123',
walletId: 'wallet-123',
type: TransactionType.DEPOSIT,
amount: 100,
description: 'Test deposit',
createdAt: new Date('2024-01-02'),
},
};
presenter.present(result);
expect(presenter.getResponseModel()).toEqual(result);
});
it('should overwrite previous result', () => {
const firstResult: ProcessWalletTransactionResultDTO = {
wallet: {
id: 'wallet-123',
leagueId: 'league-123',
balance: 1000,
totalRevenue: 5000,
totalPlatformFees: 250,
totalWithdrawn: 3000,
createdAt: new Date('2024-01-01'),
currency: 'USD',
},
transaction: {
id: 'transaction-123',
walletId: 'wallet-123',
type: TransactionType.DEPOSIT,
amount: 100,
description: 'Test deposit',
createdAt: new Date('2024-01-02'),
},
};
const secondResult: ProcessWalletTransactionResultDTO = {
wallet: {
id: 'wallet-456',
leagueId: 'league-456',
balance: 2000,
totalRevenue: 10000,
totalPlatformFees: 500,
totalWithdrawn: 6000,
createdAt: new Date('2024-01-02'),
currency: 'EUR',
},
transaction: {
id: 'transaction-456',
walletId: 'wallet-456',
type: TransactionType.WITHDRAWAL,
amount: 200,
description: 'Test withdrawal',
createdAt: new Date('2024-01-03'),
},
};
presenter.present(firstResult);
presenter.present(secondResult);
expect(presenter.getResponseModel()).toEqual(secondResult);
});
});
describe('getResponseModel', () => {
it('should return null when not presented', () => {
expect(presenter.getResponseModel()).toBeNull();
});
it('should return the result after present()', () => {
const result: ProcessWalletTransactionResultDTO = {
wallet: {
id: 'wallet-123',
leagueId: 'league-123',
balance: 1000,
totalRevenue: 5000,
totalPlatformFees: 250,
totalWithdrawn: 3000,
createdAt: new Date('2024-01-01'),
currency: 'USD',
},
transaction: {
id: 'transaction-123',
walletId: 'wallet-123',
type: TransactionType.DEPOSIT,
amount: 100,
description: 'Test deposit',
createdAt: new Date('2024-01-02'),
},
};
presenter.present(result);
expect(presenter.getResponseModel()).toEqual(result);
});
});
describe('reset', () => {
it('should clear the result', () => {
const result: ProcessWalletTransactionResultDTO = {
wallet: {
id: 'wallet-123',
leagueId: 'league-123',
balance: 1000,
totalRevenue: 5000,
totalPlatformFees: 250,
totalWithdrawn: 3000,
createdAt: new Date('2024-01-01'),
currency: 'USD',
},
transaction: {
id: 'transaction-123',
walletId: 'wallet-123',
type: TransactionType.DEPOSIT,
amount: 100,
description: 'Test deposit',
createdAt: new Date('2024-01-02'),
},
};
presenter.present(result);
presenter.reset();
expect(presenter.getResponseModel()).toBeNull();
});
it('should allow presenting again after reset', () => {
const firstResult: ProcessWalletTransactionResultDTO = {
wallet: {
id: 'wallet-123',
leagueId: 'league-123',
balance: 1000,
totalRevenue: 5000,
totalPlatformFees: 250,
totalWithdrawn: 3000,
createdAt: new Date('2024-01-01'),
currency: 'USD',
},
transaction: {
id: 'transaction-123',
walletId: 'wallet-123',
type: TransactionType.DEPOSIT,
amount: 100,
description: 'Test deposit',
createdAt: new Date('2024-01-02'),
},
};
const secondResult: ProcessWalletTransactionResultDTO = {
wallet: {
id: 'wallet-456',
leagueId: 'league-456',
balance: 2000,
totalRevenue: 10000,
totalPlatformFees: 500,
totalWithdrawn: 6000,
createdAt: new Date('2024-01-02'),
currency: 'EUR',
},
transaction: {
id: 'transaction-456',
walletId: 'wallet-456',
type: TransactionType.WITHDRAWAL,
amount: 200,
description: 'Test withdrawal',
createdAt: new Date('2024-01-03'),
},
};
presenter.present(firstResult);
presenter.reset();
presenter.present(secondResult);
expect(presenter.getResponseModel()).toEqual(secondResult);
});
});
describe('viewModel', () => {
it('should return the result', () => {
const result: ProcessWalletTransactionResultDTO = {
wallet: {
id: 'wallet-123',
leagueId: 'league-123',
balance: 1000,
totalRevenue: 5000,
totalPlatformFees: 250,
totalWithdrawn: 3000,
createdAt: new Date('2024-01-01'),
currency: 'USD',
},
transaction: {
id: 'transaction-123',
walletId: 'wallet-123',
type: TransactionType.DEPOSIT,
amount: 100,
description: 'Test deposit',
createdAt: new Date('2024-01-02'),
},
};
presenter.present(result);
expect(presenter.viewModel).toEqual(result);
});
it('should throw error when accessed before present()', () => {
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
it('should throw error after reset', () => {
const result: ProcessWalletTransactionResultDTO = {
wallet: {
id: 'wallet-123',
leagueId: 'league-123',
balance: 1000,
totalRevenue: 5000,
totalPlatformFees: 250,
totalWithdrawn: 3000,
createdAt: new Date('2024-01-01'),
currency: 'USD',
},
transaction: {
id: 'transaction-123',
walletId: 'wallet-123',
type: TransactionType.DEPOSIT,
amount: 100,
description: 'Test deposit',
createdAt: new Date('2024-01-02'),
},
};
presenter.present(result);
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
});
});

View File

@@ -0,0 +1,198 @@
import { UpdateMemberPaymentPresenter } from './UpdateMemberPaymentPresenter';
import { UpdateMemberPaymentResultDTO } from '../dtos/UpdateMemberPaymentDTO';
import { MemberPaymentStatus } from '../dtos/MemberPaymentStatus';
describe('UpdateMemberPaymentPresenter', () => {
let presenter: UpdateMemberPaymentPresenter;
beforeEach(() => {
presenter = new UpdateMemberPaymentPresenter();
});
describe('present', () => {
it('should store the result', () => {
const result: UpdateMemberPaymentResultDTO = {
payment: {
id: 'payment-123',
feeId: 'fee-123',
driverId: 'driver-123',
amount: 100,
platformFee: 5,
netAmount: 95,
status: MemberPaymentStatus.PAID,
dueDate: new Date('2024-01-01'),
paidAt: new Date('2024-01-02'),
},
};
presenter.present(result);
expect(presenter.getResponseModel()).toEqual(result);
});
it('should overwrite previous result', () => {
const firstResult: UpdateMemberPaymentResultDTO = {
payment: {
id: 'payment-123',
feeId: 'fee-123',
driverId: 'driver-123',
amount: 100,
platformFee: 5,
netAmount: 95,
status: MemberPaymentStatus.PAID,
dueDate: new Date('2024-01-01'),
paidAt: new Date('2024-01-02'),
},
};
const secondResult: UpdateMemberPaymentResultDTO = {
payment: {
id: 'payment-456',
feeId: 'fee-456',
driverId: 'driver-456',
amount: 200,
platformFee: 10,
netAmount: 190,
status: MemberPaymentStatus.OVERDUE,
dueDate: new Date('2024-01-03'),
},
};
presenter.present(firstResult);
presenter.present(secondResult);
expect(presenter.getResponseModel()).toEqual(secondResult);
});
});
describe('getResponseModel', () => {
it('should return null when not presented', () => {
expect(presenter.getResponseModel()).toBeNull();
});
it('should return the result after present()', () => {
const result: UpdateMemberPaymentResultDTO = {
payment: {
id: 'payment-123',
feeId: 'fee-123',
driverId: 'driver-123',
amount: 100,
platformFee: 5,
netAmount: 95,
status: MemberPaymentStatus.PAID,
dueDate: new Date('2024-01-01'),
paidAt: new Date('2024-01-02'),
},
};
presenter.present(result);
expect(presenter.getResponseModel()).toEqual(result);
});
});
describe('reset', () => {
it('should clear the result', () => {
const result: UpdateMemberPaymentResultDTO = {
payment: {
id: 'payment-123',
feeId: 'fee-123',
driverId: 'driver-123',
amount: 100,
platformFee: 5,
netAmount: 95,
status: MemberPaymentStatus.PAID,
dueDate: new Date('2024-01-01'),
paidAt: new Date('2024-01-02'),
},
};
presenter.present(result);
presenter.reset();
expect(presenter.getResponseModel()).toBeNull();
});
it('should allow presenting again after reset', () => {
const firstResult: UpdateMemberPaymentResultDTO = {
payment: {
id: 'payment-123',
feeId: 'fee-123',
driverId: 'driver-123',
amount: 100,
platformFee: 5,
netAmount: 95,
status: MemberPaymentStatus.PAID,
dueDate: new Date('2024-01-01'),
paidAt: new Date('2024-01-02'),
},
};
const secondResult: UpdateMemberPaymentResultDTO = {
payment: {
id: 'payment-456',
feeId: 'fee-456',
driverId: 'driver-456',
amount: 200,
platformFee: 10,
netAmount: 190,
status: MemberPaymentStatus.OVERDUE,
dueDate: new Date('2024-01-03'),
},
};
presenter.present(firstResult);
presenter.reset();
presenter.present(secondResult);
expect(presenter.getResponseModel()).toEqual(secondResult);
});
});
describe('viewModel', () => {
it('should return the result', () => {
const result: UpdateMemberPaymentResultDTO = {
payment: {
id: 'payment-123',
feeId: 'fee-123',
driverId: 'driver-123',
amount: 100,
platformFee: 5,
netAmount: 95,
status: MemberPaymentStatus.PAID,
dueDate: new Date('2024-01-01'),
paidAt: new Date('2024-01-02'),
},
};
presenter.present(result);
expect(presenter.viewModel).toEqual(result);
});
it('should throw error when accessed before present()', () => {
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
it('should throw error after reset', () => {
const result: UpdateMemberPaymentResultDTO = {
payment: {
id: 'payment-123',
feeId: 'fee-123',
driverId: 'driver-123',
amount: 100,
platformFee: 5,
netAmount: 95,
status: MemberPaymentStatus.PAID,
dueDate: new Date('2024-01-01'),
paidAt: new Date('2024-01-02'),
},
};
presenter.present(result);
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
});
});

View File

@@ -0,0 +1,236 @@
import { UpdatePaymentStatusPresenter } from './UpdatePaymentStatusPresenter';
import { UpdatePaymentStatusOutput } from '../dtos/PaymentsDto';
describe('UpdatePaymentStatusPresenter', () => {
let presenter: UpdatePaymentStatusPresenter;
beforeEach(() => {
presenter = new UpdatePaymentStatusPresenter();
});
describe('present', () => {
it('should map result to response model', () => {
const result = {
payment: {
id: 'payment-123',
type: 'membership_fee',
amount: 100,
platformFee: 5,
netAmount: 95,
payerId: 'user-123',
payerType: 'driver',
leagueId: 'league-123',
status: 'completed',
createdAt: new Date('2024-01-01'),
completedAt: new Date('2024-01-02'),
},
};
presenter.present(result);
const responseModel = presenter.getResponseModel();
expect(responseModel).toEqual({
payment: {
id: 'payment-123',
type: 'membership_fee',
amount: 100,
platformFee: 5,
netAmount: 95,
payerId: 'user-123',
payerType: 'driver',
leagueId: 'league-123',
status: 'completed',
createdAt: new Date('2024-01-01'),
completedAt: new Date('2024-01-02'),
},
});
});
it('should include seasonId when provided', () => {
const result = {
payment: {
id: 'payment-123',
type: 'membership_fee',
amount: 100,
platformFee: 5,
netAmount: 95,
payerId: 'user-123',
payerType: 'driver',
leagueId: 'league-123',
seasonId: 'season-123',
status: 'completed',
createdAt: new Date('2024-01-01'),
completedAt: new Date('2024-01-02'),
},
};
presenter.present(result);
const responseModel = presenter.getResponseModel();
expect(responseModel.payment.seasonId).toBe('season-123');
});
it('should include completedAt when provided', () => {
const result = {
payment: {
id: 'payment-123',
type: 'membership_fee',
amount: 100,
platformFee: 5,
netAmount: 95,
payerId: 'user-123',
payerType: 'driver',
leagueId: 'league-123',
status: 'completed',
createdAt: new Date('2024-01-01'),
completedAt: new Date('2024-01-02'),
},
};
presenter.present(result);
const responseModel = presenter.getResponseModel();
expect(responseModel.payment.completedAt).toEqual(new Date('2024-01-02'));
});
it('should not include seasonId when not provided', () => {
const result = {
payment: {
id: 'payment-123',
type: 'membership_fee',
amount: 100,
platformFee: 5,
netAmount: 95,
payerId: 'user-123',
payerType: 'driver',
leagueId: 'league-123',
status: 'completed',
createdAt: new Date('2024-01-01'),
},
};
presenter.present(result);
const responseModel = presenter.getResponseModel();
expect(responseModel.payment.seasonId).toBeUndefined();
});
it('should not include completedAt when not provided', () => {
const result = {
payment: {
id: 'payment-123',
type: 'membership_fee',
amount: 100,
platformFee: 5,
netAmount: 95,
payerId: 'user-123',
payerType: 'driver',
leagueId: 'league-123',
status: 'pending',
createdAt: new Date('2024-01-01'),
},
};
presenter.present(result);
const responseModel = presenter.getResponseModel();
expect(responseModel.payment.completedAt).toBeUndefined();
});
});
describe('getResponseModel', () => {
it('should throw error when accessed before present()', () => {
expect(() => presenter.getResponseModel()).toThrow('Presenter not presented');
});
it('should return model after present()', () => {
const result = {
payment: {
id: 'payment-123',
type: 'membership_fee',
amount: 100,
platformFee: 5,
netAmount: 95,
payerId: 'user-123',
payerType: 'driver',
leagueId: 'league-123',
status: 'completed',
createdAt: new Date('2024-01-01'),
completedAt: new Date('2024-01-02'),
},
};
presenter.present(result);
const responseModel = presenter.getResponseModel();
expect(responseModel).toBeDefined();
expect(responseModel.payment.id).toBe('payment-123');
});
});
describe('reset', () => {
it('should clear the response model', () => {
const result = {
payment: {
id: 'payment-123',
type: 'membership_fee',
amount: 100,
platformFee: 5,
netAmount: 95,
payerId: 'user-123',
payerType: 'driver',
leagueId: 'league-123',
status: 'completed',
createdAt: new Date('2024-01-01'),
completedAt: new Date('2024-01-02'),
},
};
presenter.present(result);
presenter.reset();
expect(() => presenter.getResponseModel()).toThrow('Presenter not presented');
});
it('should allow presenting again after reset', () => {
const firstResult = {
payment: {
id: 'payment-123',
type: 'membership_fee',
amount: 100,
platformFee: 5,
netAmount: 95,
payerId: 'user-123',
payerType: 'driver',
leagueId: 'league-123',
status: 'completed',
createdAt: new Date('2024-01-01'),
completedAt: new Date('2024-01-02'),
},
};
const secondResult = {
payment: {
id: 'payment-456',
type: 'membership_fee',
amount: 200,
platformFee: 10,
netAmount: 190,
payerId: 'user-456',
payerType: 'driver',
leagueId: 'league-456',
status: 'completed',
createdAt: new Date('2024-01-02'),
completedAt: new Date('2024-01-03'),
},
};
presenter.present(firstResult);
presenter.reset();
presenter.present(secondResult);
const responseModel = presenter.getResponseModel();
expect(responseModel.payment.id).toBe('payment-456');
});
});
});

View File

@@ -0,0 +1,182 @@
import { UpsertMembershipFeePresenter } from './UpsertMembershipFeePresenter';
import { UpsertMembershipFeeResultDTO } from '../dtos/UpsertMembershipFeeDTO';
import { MembershipFeeType } from '../dtos/MembershipFeeType';
describe('UpsertMembershipFeePresenter', () => {
let presenter: UpsertMembershipFeePresenter;
beforeEach(() => {
presenter = new UpsertMembershipFeePresenter();
});
describe('present', () => {
it('should store the result', () => {
const result: UpsertMembershipFeeResultDTO = {
fee: {
id: 'fee-123',
leagueId: 'league-123',
type: MembershipFeeType.MONTHLY,
amount: 100,
enabled: true,
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-02'),
},
};
presenter.present(result);
expect(presenter.getResponseModel()).toEqual(result);
});
it('should overwrite previous result', () => {
const firstResult: UpsertMembershipFeeResultDTO = {
fee: {
id: 'fee-123',
leagueId: 'league-123',
type: MembershipFeeType.MONTHLY,
amount: 100,
enabled: true,
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-02'),
},
};
const secondResult: UpsertMembershipFeeResultDTO = {
fee: {
id: 'fee-456',
leagueId: 'league-456',
type: MembershipFeeType.SEASON,
amount: 200,
enabled: false,
createdAt: new Date('2024-01-03'),
updatedAt: new Date('2024-01-04'),
},
};
presenter.present(firstResult);
presenter.present(secondResult);
expect(presenter.getResponseModel()).toEqual(secondResult);
});
});
describe('getResponseModel', () => {
it('should return null when not presented', () => {
expect(presenter.getResponseModel()).toBeNull();
});
it('should return the result after present()', () => {
const result: UpsertMembershipFeeResultDTO = {
fee: {
id: 'fee-123',
leagueId: 'league-123',
type: MembershipFeeType.MONTHLY,
amount: 100,
enabled: true,
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-02'),
},
};
presenter.present(result);
expect(presenter.getResponseModel()).toEqual(result);
});
});
describe('reset', () => {
it('should clear the result', () => {
const result: UpsertMembershipFeeResultDTO = {
fee: {
id: 'fee-123',
leagueId: 'league-123',
type: MembershipFeeType.MONTHLY,
amount: 100,
enabled: true,
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-02'),
},
};
presenter.present(result);
presenter.reset();
expect(presenter.getResponseModel()).toBeNull();
});
it('should allow presenting again after reset', () => {
const firstResult: UpsertMembershipFeeResultDTO = {
fee: {
id: 'fee-123',
leagueId: 'league-123',
type: MembershipFeeType.MONTHLY,
amount: 100,
enabled: true,
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-02'),
},
};
const secondResult: UpsertMembershipFeeResultDTO = {
fee: {
id: 'fee-456',
leagueId: 'league-456',
type: MembershipFeeType.SEASON,
amount: 200,
enabled: false,
createdAt: new Date('2024-01-03'),
updatedAt: new Date('2024-01-04'),
},
};
presenter.present(firstResult);
presenter.reset();
presenter.present(secondResult);
expect(presenter.getResponseModel()).toEqual(secondResult);
});
});
describe('viewModel', () => {
it('should return the result', () => {
const result: UpsertMembershipFeeResultDTO = {
fee: {
id: 'fee-123',
leagueId: 'league-123',
type: MembershipFeeType.MONTHLY,
amount: 100,
enabled: true,
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-02'),
},
};
presenter.present(result);
expect(presenter.viewModel).toEqual(result);
});
it('should throw error when accessed before present()', () => {
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
it('should throw error after reset', () => {
const result: UpsertMembershipFeeResultDTO = {
fee: {
id: 'fee-123',
leagueId: 'league-123',
type: MembershipFeeType.MONTHLY,
amount: 100,
enabled: true,
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-02'),
},
};
presenter.present(result);
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
});
});

View File

@@ -0,0 +1,36 @@
import { Test, TestingModule } from '@nestjs/testing';
import { InMemoryPersistenceModule } from './InMemoryPersistenceModule';
describe('InMemoryPersistenceModule', () => {
let module: TestingModule;
beforeEach(async () => {
module = await Test.createTestingModule({
imports: [InMemoryPersistenceModule],
}).compile();
});
it('should compile the module', () => {
expect(module).toBeDefined();
});
it('should import InMemoryRacingPersistenceModule', () => {
// The module should be able to resolve dependencies from InMemoryRacingPersistenceModule
expect(module).toBeDefined();
});
it('should import InMemorySocialPersistenceModule', () => {
// The module should be able to resolve dependencies from InMemorySocialPersistenceModule
expect(module).toBeDefined();
});
it('should export InMemoryRacingPersistenceModule', () => {
// The module should export InMemoryRacingPersistenceModule
expect(module).toBeDefined();
});
it('should export InMemorySocialPersistenceModule', () => {
// The module should export InMemorySocialPersistenceModule
expect(module).toBeDefined();
});
});

View File

@@ -0,0 +1,272 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ExecutionContext, NotFoundException, ServiceUnavailableException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { FeatureAvailabilityGuard, inferActionTypeFromHttpMethod } from './FeatureAvailabilityGuard';
import { PolicyService } from './PolicyService';
import { FEATURE_AVAILABILITY_METADATA_KEY, FeatureAvailabilityMetadata } from './RequireCapability';
class MockReflector {
getAllAndOverride = vi.fn();
}
class MockPolicyService {
getSnapshot = vi.fn();
}
describe('FeatureAvailabilityGuard', () => {
let guard: FeatureAvailabilityGuard;
let reflector: MockReflector;
let policyService: MockPolicyService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
FeatureAvailabilityGuard,
{
provide: Reflector,
useClass: MockReflector,
},
{
provide: PolicyService,
useClass: MockPolicyService,
},
],
}).compile();
guard = module.get<FeatureAvailabilityGuard>(FeatureAvailabilityGuard);
reflector = module.get<Reflector>(Reflector) as unknown as MockReflector;
policyService = module.get<PolicyService>(PolicyService) as unknown as MockPolicyService;
});
describe('canActivate', () => {
it('should return true when no metadata is found', async () => {
const mockContext = {
getHandler: () => () => {},
getClass: () => class {},
} as unknown as ExecutionContext;
reflector.getAllAndOverride.mockReturnValue(undefined);
const result = await guard.canActivate(mockContext);
expect(result).toBe(true);
expect(reflector.getAllAndOverride).toHaveBeenCalledWith(
FEATURE_AVAILABILITY_METADATA_KEY,
[mockContext.getHandler(), mockContext.getClass()]
);
});
it('should return true when feature is enabled', async () => {
const mockContext = {
getHandler: () => () => {},
getClass: () => class {},
} as unknown as ExecutionContext;
const metadata: FeatureAvailabilityMetadata = {
capabilityKey: 'test-feature',
actionType: 'view',
};
reflector.getAllAndOverride.mockReturnValue(metadata);
policyService.getSnapshot.mockResolvedValue({
policyVersion: 1,
operationalMode: 'normal',
capabilities: { 'test-feature': 'enabled' },
maintenanceAllowlist: { mutate: [], view: [] },
loadedFrom: 'defaults',
loadedAtIso: new Date().toISOString(),
});
const result = await guard.canActivate(mockContext);
expect(result).toBe(true);
});
it('should throw ServiceUnavailableException when in maintenance mode and not in allowlist', async () => {
const mockContext = {
getHandler: () => () => {},
getClass: () => class {},
} as unknown as ExecutionContext;
const metadata: FeatureAvailabilityMetadata = {
capabilityKey: 'test-feature',
actionType: 'mutate',
};
reflector.getAllAndOverride.mockReturnValue(metadata);
policyService.getSnapshot.mockResolvedValue({
policyVersion: 1,
operationalMode: 'maintenance',
capabilities: { 'test-feature': 'enabled' },
maintenanceAllowlist: { mutate: [], view: [] },
loadedFrom: 'defaults',
loadedAtIso: new Date().toISOString(),
});
await expect(guard.canActivate(mockContext)).rejects.toThrow(ServiceUnavailableException);
await expect(guard.canActivate(mockContext)).rejects.toThrow('Service temporarily unavailable');
});
it('should return true when in maintenance mode but in allowlist', async () => {
const mockContext = {
getHandler: () => () => {},
getClass: () => class {},
} as unknown as ExecutionContext;
const metadata: FeatureAvailabilityMetadata = {
capabilityKey: 'test-feature',
actionType: 'mutate',
};
reflector.getAllAndOverride.mockReturnValue(metadata);
policyService.getSnapshot.mockResolvedValue({
policyVersion: 1,
operationalMode: 'maintenance',
capabilities: { 'test-feature': 'enabled' },
maintenanceAllowlist: { mutate: ['test-feature'], view: [] },
loadedFrom: 'defaults',
loadedAtIso: new Date().toISOString(),
});
const result = await guard.canActivate(mockContext);
expect(result).toBe(true);
});
it('should throw NotFoundException when feature is disabled', async () => {
const mockContext = {
getHandler: () => () => {},
getClass: () => class {},
} as unknown as ExecutionContext;
const metadata: FeatureAvailabilityMetadata = {
capabilityKey: 'test-feature',
actionType: 'view',
};
reflector.getAllAndOverride.mockReturnValue(metadata);
policyService.getSnapshot.mockResolvedValue({
policyVersion: 1,
operationalMode: 'normal',
capabilities: { 'test-feature': 'disabled' },
maintenanceAllowlist: { mutate: [], view: [] },
loadedFrom: 'defaults',
loadedAtIso: new Date().toISOString(),
});
await expect(guard.canActivate(mockContext)).rejects.toThrow(NotFoundException);
await expect(guard.canActivate(mockContext)).rejects.toThrow('Not Found');
});
it('should throw NotFoundException when feature is hidden', async () => {
const mockContext = {
getHandler: () => () => {},
getClass: () => class {},
} as unknown as ExecutionContext;
const metadata: FeatureAvailabilityMetadata = {
capabilityKey: 'test-feature',
actionType: 'view',
};
reflector.getAllAndOverride.mockReturnValue(metadata);
policyService.getSnapshot.mockResolvedValue({
policyVersion: 1,
operationalMode: 'normal',
capabilities: { 'test-feature': 'hidden' },
maintenanceAllowlist: { mutate: [], view: [] },
loadedFrom: 'defaults',
loadedAtIso: new Date().toISOString(),
});
await expect(guard.canActivate(mockContext)).rejects.toThrow(NotFoundException);
await expect(guard.canActivate(mockContext)).rejects.toThrow('Not Found');
});
it('should throw NotFoundException when feature is coming_soon', async () => {
const mockContext = {
getHandler: () => () => {},
getClass: () => class {},
} as unknown as ExecutionContext;
const metadata: FeatureAvailabilityMetadata = {
capabilityKey: 'test-feature',
actionType: 'view',
};
reflector.getAllAndOverride.mockReturnValue(metadata);
policyService.getSnapshot.mockResolvedValue({
policyVersion: 1,
operationalMode: 'normal',
capabilities: { 'test-feature': 'coming_soon' },
maintenanceAllowlist: { mutate: [], view: [] },
loadedFrom: 'defaults',
loadedAtIso: new Date().toISOString(),
});
await expect(guard.canActivate(mockContext)).rejects.toThrow(NotFoundException);
await expect(guard.canActivate(mockContext)).rejects.toThrow('Not Found');
});
it('should throw NotFoundException when feature is not configured', async () => {
const mockContext = {
getHandler: () => () => {},
getClass: () => class {},
} as unknown as ExecutionContext;
const metadata: FeatureAvailabilityMetadata = {
capabilityKey: 'test-feature',
actionType: 'view',
};
reflector.getAllAndOverride.mockReturnValue(metadata);
policyService.getSnapshot.mockResolvedValue({
policyVersion: 1,
operationalMode: 'normal',
capabilities: {},
maintenanceAllowlist: { mutate: [], view: [] },
loadedFrom: 'defaults',
loadedAtIso: new Date().toISOString(),
});
await expect(guard.canActivate(mockContext)).rejects.toThrow(NotFoundException);
await expect(guard.canActivate(mockContext)).rejects.toThrow('Not Found');
});
});
describe('inferActionTypeFromHttpMethod', () => {
it('should return "view" for GET requests', () => {
expect(inferActionTypeFromHttpMethod('GET')).toBe('view');
});
it('should return "view" for HEAD requests', () => {
expect(inferActionTypeFromHttpMethod('HEAD')).toBe('view');
});
it('should return "view" for OPTIONS requests', () => {
expect(inferActionTypeFromHttpMethod('OPTIONS')).toBe('view');
});
it('should return "mutate" for POST requests', () => {
expect(inferActionTypeFromHttpMethod('POST')).toBe('mutate');
});
it('should return "mutate" for PUT requests', () => {
expect(inferActionTypeFromHttpMethod('PUT')).toBe('mutate');
});
it('should return "mutate" for PATCH requests', () => {
expect(inferActionTypeFromHttpMethod('PATCH')).toBe('mutate');
});
it('should return "mutate" for DELETE requests', () => {
expect(inferActionTypeFromHttpMethod('DELETE')).toBe('mutate');
});
it('should handle lowercase HTTP methods', () => {
expect(inferActionTypeFromHttpMethod('get')).toBe('view');
expect(inferActionTypeFromHttpMethod('post')).toBe('mutate');
});
});
});

View File

@@ -9,7 +9,7 @@ import { Reflector } from '@nestjs/core';
import { ActionType, FeatureState, PolicyService } from './PolicyService';
import { FEATURE_AVAILABILITY_METADATA_KEY, FeatureAvailabilityMetadata } from './RequireCapability';
type Evaluation = { allow: true } | { allow: false; reason: 'maintenance' | FeatureState | 'not_configured' };
type Evaluation = { allow: true; reason?: undefined } | { allow: false; reason: 'maintenance' | FeatureState | 'not_configured' };
@Injectable()
export class FeatureAvailabilityGuard implements CanActivate {

View File

@@ -0,0 +1,37 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PolicyController } from './PolicyController';
import { PolicyModule } from './PolicyModule';
import { PolicyService } from './PolicyService';
import { FeatureAvailabilityGuard } from './FeatureAvailabilityGuard';
describe('PolicyModule', () => {
let module: TestingModule;
beforeEach(async () => {
module = await Test.createTestingModule({
imports: [PolicyModule],
}).compile();
});
it('should compile the module', () => {
expect(module).toBeDefined();
});
it('should provide PolicyController', () => {
const controller = module.get<PolicyController>(PolicyController);
expect(controller).toBeDefined();
expect(controller).toBeInstanceOf(PolicyController);
});
it('should provide PolicyService', () => {
const service = module.get<PolicyService>(PolicyService);
expect(service).toBeDefined();
expect(service).toBeInstanceOf(PolicyService);
});
it('should provide FeatureAvailabilityGuard', () => {
const guard = module.get<FeatureAvailabilityGuard>(FeatureAvailabilityGuard);
expect(guard).toBeDefined();
expect(guard).toBeInstanceOf(FeatureAvailabilityGuard);
});
});

View File

@@ -0,0 +1,68 @@
import { SetMetadata } from '@nestjs/common';
import { RequireCapability, FEATURE_AVAILABILITY_METADATA_KEY, FeatureAvailabilityMetadata } from './RequireCapability';
import { ActionType } from './PolicyService';
// Mock SetMetadata
vi.mock('@nestjs/common', () => ({
SetMetadata: vi.fn(),
}));
describe('RequireCapability', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should call SetMetadata with correct key and metadata', () => {
const capabilityKey = 'test-feature';
const actionType: ActionType = 'view';
RequireCapability(capabilityKey, actionType);
expect(SetMetadata).toHaveBeenCalledWith(
FEATURE_AVAILABILITY_METADATA_KEY,
{
capabilityKey,
actionType,
} satisfies FeatureAvailabilityMetadata
);
});
it('should work with mutate action type', () => {
const capabilityKey = 'test-feature';
const actionType: ActionType = 'mutate';
RequireCapability(capabilityKey, actionType);
expect(SetMetadata).toHaveBeenCalledWith(
FEATURE_AVAILABILITY_METADATA_KEY,
{
capabilityKey,
actionType,
} satisfies FeatureAvailabilityMetadata
);
});
it('should work with different capability keys', () => {
const capabilityKey = 'another-feature';
const actionType: ActionType = 'view';
RequireCapability(capabilityKey, actionType);
expect(SetMetadata).toHaveBeenCalledWith(
FEATURE_AVAILABILITY_METADATA_KEY,
{
capabilityKey,
actionType,
} satisfies FeatureAvailabilityMetadata
);
});
it('should return a decorator function', () => {
const capabilityKey = 'test-feature';
const actionType: ActionType = 'view';
const decorator = RequireCapability(capabilityKey, actionType);
expect(typeof decorator).toBe('function');
});
});