add tests
This commit is contained in:
30
apps/api/src/domain/admin/AdminModule.test.ts
Normal file
30
apps/api/src/domain/admin/AdminModule.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
40
apps/api/src/domain/admin/RequireSystemAdmin.test.ts
Normal file
40
apps/api/src/domain/admin/RequireSystemAdmin.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
234
apps/api/src/domain/analytics/AnalyticsProviders.test.ts
Normal file
234
apps/api/src/domain/analytics/AnalyticsProviders.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
153
apps/api/src/domain/auth/AuthorizationService.test.ts
Normal file
153
apps/api/src/domain/auth/AuthorizationService.test.ts
Normal 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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
40
apps/api/src/domain/auth/Public.test.ts
Normal file
40
apps/api/src/domain/auth/Public.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
40
apps/api/src/domain/auth/RequireAuthenticatedUser.test.ts
Normal file
40
apps/api/src/domain/auth/RequireAuthenticatedUser.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
69
apps/api/src/domain/auth/RequireRoles.test.ts
Normal file
69
apps/api/src/domain/auth/RequireRoles.test.ts
Normal 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'],
|
||||
});
|
||||
});
|
||||
});
|
||||
156
apps/api/src/domain/auth/getActorFromRequestContext.test.ts
Normal file
156
apps/api/src/domain/auth/getActorFromRequestContext.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
89
apps/api/src/domain/database/DatabaseModule.test.ts
Normal file
89
apps/api/src/domain/database/DatabaseModule.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
30
apps/api/src/domain/hello/HelloModule.test.ts
Normal file
30
apps/api/src/domain/hello/HelloModule.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
210
apps/api/src/domain/league/LeagueAuthorization.test.ts
Normal file
210
apps/api/src/domain/league/LeagueAuthorization.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
30
apps/api/src/domain/league/LeagueModule.test.ts
Normal file
30
apps/api/src/domain/league/LeagueModule.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
32
apps/api/src/domain/logging/LoggingModule.test.ts
Normal file
32
apps/api/src/domain/logging/LoggingModule.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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: [] });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
211
apps/api/src/domain/notifications/NotificationsService.test.ts
Normal file
211
apps/api/src/domain/notifications/NotificationsService.test.ts
Normal 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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
272
apps/api/src/domain/policy/FeatureAvailabilityGuard.test.ts
Normal file
272
apps/api/src/domain/policy/FeatureAvailabilityGuard.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 {
|
||||
|
||||
37
apps/api/src/domain/policy/PolicyModule.test.ts
Normal file
37
apps/api/src/domain/policy/PolicyModule.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
68
apps/api/src/domain/policy/RequireCapability.test.ts
Normal file
68
apps/api/src/domain/policy/RequireCapability.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user