6 Commits

Author SHA1 Message Date
1288a9dc30 eslint rules
Some checks failed
Contract Testing / contract-tests (pull_request) Failing after 5m48s
Contract Testing / contract-snapshot (pull_request) Has been skipped
2026-01-22 18:57:48 +01:00
04d445bf00 eslint rules 2026-01-22 18:46:51 +01:00
94b92a9314 view data tests
Some checks failed
Contract Testing / contract-tests (pull_request) Failing after 5m45s
Contract Testing / contract-snapshot (pull_request) Has been skipped
2026-01-22 18:35:35 +01:00
108cfbcd65 view data tests
Some checks failed
Contract Testing / contract-tests (pull_request) Failing after 5m55s
Contract Testing / contract-snapshot (pull_request) Has been skipped
2026-01-22 18:22:08 +01:00
1f4f837282 view data tests
Some checks failed
Contract Testing / contract-tests (pull_request) Failing after 5m58s
Contract Testing / contract-snapshot (pull_request) Has been skipped
2026-01-22 18:06:46 +01:00
c22e26d14c view data tests
Some checks failed
Contract Testing / contract-tests (pull_request) Failing after 5m48s
Contract Testing / contract-snapshot (pull_request) Has been skipped
2026-01-22 17:27:08 +01:00
149 changed files with 19639 additions and 23682 deletions

View File

@@ -44,7 +44,8 @@
"lib/builders/view-models/*.tsx"
],
"rules": {
"gridpilot-rules/view-model-builder-contract": "error"
"gridpilot-rules/view-model-builder-contract": "error",
"gridpilot-rules/view-model-builder-implements": "error"
}
},
{
@@ -55,7 +56,9 @@
"rules": {
"gridpilot-rules/filename-matches-export": "off",
"gridpilot-rules/single-export-per-file": "off",
"gridpilot-rules/view-data-builder-contract": "off"
"gridpilot-rules/view-data-builder-contract": "off",
"gridpilot-rules/view-data-builder-implements": "error",
"gridpilot-rules/view-data-builder-imports": "error"
}
},
{
@@ -192,6 +195,24 @@
"gridpilot-rules/view-data-location": "error"
}
},
{
"files": [
"lib/view-data/**/*.ts",
"lib/view-data/**/*.tsx"
],
"rules": {
"gridpilot-rules/view-data-implements": "error"
}
},
{
"files": [
"lib/view-models/**/*.ts",
"lib/view-models/**/*.tsx"
],
"rules": {
"gridpilot-rules/view-model-implements": "error"
}
},
{
"files": [
"lib/services/**/*.ts"

View File

@@ -46,6 +46,11 @@ const servicesImplementContract = require('./services-implement-contract');
const serverActionsReturnResult = require('./server-actions-return-result');
const serverActionsInterface = require('./server-actions-interface');
const noDisplayObjectsInUi = require('./no-display-objects-in-ui');
const viewDataBuilderImplements = require('./view-data-builder-implements');
const viewDataBuilderImports = require('./view-data-builder-imports');
const viewModelBuilderImplements = require('./view-model-builder-implements');
const viewDataImplements = require('./view-data-implements');
const viewModelImplements = require('./view-model-implements');
module.exports = {
rules: {
@@ -128,9 +133,14 @@ module.exports = {
// View Data Rules
'view-data-location': viewDataLocation,
'view-data-builder-contract': viewDataBuilderContract,
'view-data-builder-implements': viewDataBuilderImplements,
'view-data-builder-imports': viewDataBuilderImports,
'view-data-implements': viewDataImplements,
// View Model Rules
'view-model-builder-contract': viewModelBuilderContract,
'view-model-builder-implements': viewModelBuilderImplements,
'view-model-implements': viewModelImplements,
// Single Export Rules
'single-export-per-file': singleExportPerFile,
@@ -253,9 +263,14 @@ module.exports = {
// View Data
'gridpilot-rules/view-data-location': 'error',
'gridpilot-rules/view-data-builder-contract': 'error',
'gridpilot-rules/view-data-builder-implements': 'error',
'gridpilot-rules/view-data-builder-imports': 'error',
'gridpilot-rules/view-data-implements': 'error',
// View Model
'gridpilot-rules/view-model-builder-contract': 'error',
'gridpilot-rules/view-model-builder-implements': 'error',
'gridpilot-rules/view-model-implements': 'error',
// Single Export Rules
'gridpilot-rules/single-export-per-file': 'error',

View File

@@ -0,0 +1,96 @@
/**
* ESLint rule to enforce View Data Builder contract implementation
*
* View Data Builders in lib/builders/view-data/ must:
* 1. Be classes named *ViewDataBuilder
* 2. Implement the ViewDataBuilder<TInput, TOutput> interface
* 3. Have a static build() method
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce View Data Builder contract implementation',
category: 'Builders',
recommended: true,
},
fixable: null,
schema: [],
messages: {
notAClass: 'View Data Builders must be classes named *ViewDataBuilder',
missingImplements: 'View Data Builders must implement ViewDataBuilder<TInput, TOutput> interface',
missingBuildMethod: 'View Data Builders must have a static build() method',
},
},
create(context) {
const filename = context.getFilename();
const isInViewDataBuilders = filename.includes('/lib/builders/view-data/');
if (!isInViewDataBuilders) return {};
let hasImplements = false;
let hasBuildMethod = false;
return {
// Check class declaration
ClassDeclaration(node) {
const className = node.id?.name;
if (!className || !className.endsWith('ViewDataBuilder')) {
context.report({
node,
messageId: 'notAClass',
});
}
// Check if class implements ViewDataBuilder interface
if (node.implements && node.implements.length > 0) {
for (const impl of node.implements) {
// Handle GenericTypeAnnotation for ViewDataBuilder<TInput, TOutput>
if (impl.expression.type === 'TSInstantiationExpression') {
const expr = impl.expression.expression;
if (expr.type === 'Identifier' && expr.name === 'ViewDataBuilder') {
hasImplements = true;
}
} else if (impl.expression.type === 'Identifier') {
// Handle simple ViewDataBuilder (without generics)
if (impl.expression.name === 'ViewDataBuilder') {
hasImplements = true;
}
}
}
}
// Check for static build method
const buildMethod = node.body.body.find(member =>
member.type === 'MethodDefinition' &&
member.key.type === 'Identifier' &&
member.key.name === 'build' &&
member.static === true
);
if (buildMethod) {
hasBuildMethod = true;
}
},
'Program:exit'() {
if (!hasImplements) {
context.report({
node: context.getSourceCode().ast,
messageId: 'missingImplements',
});
}
if (!hasBuildMethod) {
context.report({
node: context.getSourceCode().ast,
messageId: 'missingBuildMethod',
});
}
},
};
},
};

View File

@@ -0,0 +1,80 @@
/**
* ESLint rule to enforce ViewDataBuilder import paths
*
* ViewDataBuilders in lib/builders/view-data/ must:
* 1. Import DTO types from lib/types/generated/
* 2. Import ViewData types from lib/view-data/
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce ViewDataBuilder import paths',
category: 'Builders',
recommended: true,
},
fixable: null,
schema: [],
messages: {
invalidDtoImport: 'ViewDataBuilders must import DTO types from lib/types/generated/, not from {{importPath}}',
invalidViewDataImport: 'ViewDataBuilders must import ViewData types from lib/view-data/, not from {{importPath}}',
missingDtoImport: 'ViewDataBuilders must import DTO types from lib/types/generated/',
missingViewDataImport: 'ViewDataBuilders must import ViewData types from lib/view-data/',
},
},
create(context) {
const filename = context.getFilename();
const isInViewDataBuilders = filename.includes('/lib/builders/view-data/');
if (!isInViewDataBuilders) return {};
let hasDtoImport = false;
let hasViewDataImport = false;
let dtoImportPath = null;
let viewDataImportPath = null;
return {
ImportDeclaration(node) {
const importPath = node.source.value;
// Check for DTO imports (should be from lib/types/generated/)
if (importPath.includes('/lib/types/')) {
if (!importPath.includes('/lib/types/generated/')) {
dtoImportPath = importPath;
context.report({
node,
messageId: 'invalidDtoImport',
data: { importPath },
});
} else {
hasDtoImport = true;
}
}
// Check for ViewData imports (should be from lib/view-data/)
if (importPath.includes('/lib/view-data/')) {
hasViewDataImport = true;
viewDataImportPath = importPath;
}
},
'Program:exit'() {
if (!hasDtoImport) {
context.report({
node: context.getSourceCode().ast,
messageId: 'missingDtoImport',
});
}
if (!hasViewDataImport) {
context.report({
node: context.getSourceCode().ast,
messageId: 'missingViewDataImport',
});
}
},
};
},
};

View File

@@ -0,0 +1,91 @@
/**
* ESLint rule to enforce ViewData contract implementation
*
* ViewData files in lib/view-data/ must:
* 1. Be interfaces or types named *ViewData
* 2. Extend the ViewData interface from contracts
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce ViewData contract implementation',
category: 'Contracts',
recommended: true,
},
fixable: null,
schema: [],
messages: {
notAnInterface: 'ViewData files must be interfaces or types named *ViewData',
missingExtends: 'ViewData must extend the ViewData interface from lib/contracts/view-data/ViewData.ts',
},
},
create(context) {
const filename = context.getFilename();
const isInViewData = filename.includes('/lib/view-data/');
if (!isInViewData) return {};
let hasViewDataExtends = false;
let hasCorrectName = false;
return {
// Check interface declarations
TSInterfaceDeclaration(node) {
const interfaceName = node.id?.name;
if (interfaceName && interfaceName.endsWith('ViewData')) {
hasCorrectName = true;
// Check if it extends ViewData
if (node.extends && node.extends.length > 0) {
for (const ext of node.extends) {
if (ext.type === 'TSExpressionWithTypeArguments' &&
ext.expression.type === 'Identifier' &&
ext.expression.name === 'ViewData') {
hasViewDataExtends = true;
}
}
}
}
},
// Check type alias declarations
TSTypeAliasDeclaration(node) {
const typeName = node.id?.name;
if (typeName && typeName.endsWith('ViewData')) {
hasCorrectName = true;
// For type aliases, check if it's an intersection with ViewData
if (node.typeAnnotation && node.typeAnnotation.type === 'TSIntersectionType') {
for (const type of node.typeAnnotation.types) {
if (type.type === 'TSTypeReference' &&
type.typeName &&
type.typeName.type === 'Identifier' &&
type.typeName.name === 'ViewData') {
hasViewDataExtends = true;
}
}
}
}
},
'Program:exit'() {
if (!hasCorrectName) {
context.report({
node: context.getSourceCode().ast,
messageId: 'notAnInterface',
});
} else if (!hasViewDataExtends) {
context.report({
node: context.getSourceCode().ast,
messageId: 'missingExtends',
});
}
},
};
},
};

View File

@@ -0,0 +1,96 @@
/**
* ESLint rule to enforce View Model Builder contract implementation
*
* View Model Builders in lib/builders/view-models/ must:
* 1. Be classes named *ViewModelBuilder
* 2. Implement the ViewModelBuilder<TInput, TOutput> interface
* 3. Have a static build() method
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce View Model Builder contract implementation',
category: 'Builders',
recommended: true,
},
fixable: null,
schema: [],
messages: {
notAClass: 'View Model Builders must be classes named *ViewModelBuilder',
missingImplements: 'View Model Builders must implement ViewModelBuilder<TInput, TOutput> interface',
missingBuildMethod: 'View Model Builders must have a static build() method',
},
},
create(context) {
const filename = context.getFilename();
const isInViewModelBuilders = filename.includes('/lib/builders/view-models/');
if (!isInViewModelBuilders) return {};
let hasImplements = false;
let hasBuildMethod = false;
return {
// Check class declaration
ClassDeclaration(node) {
const className = node.id?.name;
if (!className || !className.endsWith('ViewModelBuilder')) {
context.report({
node,
messageId: 'notAClass',
});
}
// Check if class implements ViewModelBuilder interface
if (node.implements && node.implements.length > 0) {
for (const impl of node.implements) {
// Handle GenericTypeAnnotation for ViewModelBuilder<TInput, TOutput>
if (impl.expression.type === 'TSInstantiationExpression') {
const expr = impl.expression.expression;
if (expr.type === 'Identifier' && expr.name === 'ViewModelBuilder') {
hasImplements = true;
}
} else if (impl.expression.type === 'Identifier') {
// Handle simple ViewModelBuilder (without generics)
if (impl.expression.name === 'ViewModelBuilder') {
hasImplements = true;
}
}
}
}
// Check for static build method
const buildMethod = node.body.body.find(member =>
member.type === 'MethodDefinition' &&
member.key.type === 'Identifier' &&
member.key.name === 'build' &&
member.static === true
);
if (buildMethod) {
hasBuildMethod = true;
}
},
'Program:exit'() {
if (!hasImplements) {
context.report({
node: context.getSourceCode().ast,
messageId: 'missingImplements',
});
}
if (!hasBuildMethod) {
context.report({
node: context.getSourceCode().ast,
messageId: 'missingBuildMethod',
});
}
},
};
},
};

View File

@@ -0,0 +1,65 @@
/**
* ESLint rule to enforce ViewModel contract implementation
*
* ViewModel files in lib/view-models/ must:
* 1. Be classes named *ViewModel
* 2. Extend the ViewModel class from contracts
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce ViewModel contract implementation',
category: 'Contracts',
recommended: true,
},
fixable: null,
schema: [],
messages: {
notAClass: 'ViewModel files must be classes named *ViewModel',
missingExtends: 'ViewModel must extend the ViewModel class from lib/contracts/view-models/ViewModel.ts',
},
},
create(context) {
const filename = context.getFilename();
const isInViewModels = filename.includes('/lib/view-models/');
if (!isInViewModels) return {};
let hasViewModelExtends = false;
let hasCorrectName = false;
return {
// Check class declarations
ClassDeclaration(node) {
const className = node.id?.name;
if (className && className.endsWith('ViewModel')) {
hasCorrectName = true;
// Check if it extends ViewModel
if (node.superClass && node.superClass.type === 'Identifier' &&
node.superClass.name === 'ViewModel') {
hasViewModelExtends = true;
}
}
},
'Program:exit'() {
if (!hasCorrectName) {
context.report({
node: context.getSourceCode().ast,
messageId: 'notAClass',
});
} else if (!hasViewModelExtends) {
context.report({
node: context.getSourceCode().ast,
messageId: 'missingExtends',
});
}
},
};
},
};

View File

@@ -0,0 +1,154 @@
import { describe, it, expect } from 'vitest';
import { AdminDashboardViewDataBuilder } from './AdminDashboardViewDataBuilder';
import type { DashboardStats } from '@/lib/types/admin';
describe('AdminDashboardViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform DashboardStats DTO to AdminDashboardViewData correctly', () => {
const dashboardStats: DashboardStats = {
totalUsers: 1000,
activeUsers: 800,
suspendedUsers: 50,
deletedUsers: 150,
systemAdmins: 5,
recentLogins: 120,
newUsersToday: 15,
};
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
expect(result).toEqual({
stats: {
totalUsers: 1000,
activeUsers: 800,
suspendedUsers: 50,
deletedUsers: 150,
systemAdmins: 5,
recentLogins: 120,
newUsersToday: 15,
},
});
});
it('should handle zero values correctly', () => {
const dashboardStats: DashboardStats = {
totalUsers: 0,
activeUsers: 0,
suspendedUsers: 0,
deletedUsers: 0,
systemAdmins: 0,
recentLogins: 0,
newUsersToday: 0,
};
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
expect(result).toEqual({
stats: {
totalUsers: 0,
activeUsers: 0,
suspendedUsers: 0,
deletedUsers: 0,
systemAdmins: 0,
recentLogins: 0,
newUsersToday: 0,
},
});
});
it('should handle large numbers correctly', () => {
const dashboardStats: DashboardStats = {
totalUsers: 1000000,
activeUsers: 750000,
suspendedUsers: 25000,
deletedUsers: 225000,
systemAdmins: 50,
recentLogins: 50000,
newUsersToday: 1000,
};
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
expect(result.stats.totalUsers).toBe(1000000);
expect(result.stats.activeUsers).toBe(750000);
expect(result.stats.systemAdmins).toBe(50);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const dashboardStats: DashboardStats = {
totalUsers: 500,
activeUsers: 400,
suspendedUsers: 25,
deletedUsers: 75,
systemAdmins: 3,
recentLogins: 80,
newUsersToday: 10,
};
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
expect(result.stats.totalUsers).toBe(dashboardStats.totalUsers);
expect(result.stats.activeUsers).toBe(dashboardStats.activeUsers);
expect(result.stats.suspendedUsers).toBe(dashboardStats.suspendedUsers);
expect(result.stats.deletedUsers).toBe(dashboardStats.deletedUsers);
expect(result.stats.systemAdmins).toBe(dashboardStats.systemAdmins);
expect(result.stats.recentLogins).toBe(dashboardStats.recentLogins);
expect(result.stats.newUsersToday).toBe(dashboardStats.newUsersToday);
});
it('should not modify the input DTO', () => {
const dashboardStats: DashboardStats = {
totalUsers: 100,
activeUsers: 80,
suspendedUsers: 5,
deletedUsers: 15,
systemAdmins: 2,
recentLogins: 20,
newUsersToday: 5,
};
const originalStats = { ...dashboardStats };
AdminDashboardViewDataBuilder.build(dashboardStats);
expect(dashboardStats).toEqual(originalStats);
});
});
describe('edge cases', () => {
it('should handle negative values (if API returns them)', () => {
const dashboardStats: DashboardStats = {
totalUsers: -1,
activeUsers: -1,
suspendedUsers: -1,
deletedUsers: -1,
systemAdmins: -1,
recentLogins: -1,
newUsersToday: -1,
};
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
expect(result.stats.totalUsers).toBe(-1);
expect(result.stats.activeUsers).toBe(-1);
});
it('should handle very large numbers', () => {
const dashboardStats: DashboardStats = {
totalUsers: Number.MAX_SAFE_INTEGER,
activeUsers: Number.MAX_SAFE_INTEGER - 1000,
suspendedUsers: 100,
deletedUsers: 100,
systemAdmins: 10,
recentLogins: 1000,
newUsersToday: 100,
};
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
expect(result.stats.totalUsers).toBe(Number.MAX_SAFE_INTEGER);
expect(result.stats.activeUsers).toBe(Number.MAX_SAFE_INTEGER - 1000);
});
});
});

View File

@@ -1,181 +1,7 @@
/**
* View Data Layer Tests - Admin Functionality
*
* This test file covers the view data layer for admin functionality.
*
* The view data layer is responsible for:
* - DTO UI model mapping
* - Formatting, sorting, and grouping
* - Derived fields and defaults
* - UI-specific semantics
*
* This layer isolates the UI from API churn by providing a stable interface
* between the API layer and the presentation layer.
*
* Test coverage includes:
* - Admin dashboard data transformation
* - User management view models
* - Admin-specific formatting and validation
* - Derived fields for admin UI components
* - Default values and fallbacks for admin views
*/
import { AdminDashboardViewDataBuilder } from '@/lib/builders/view-data/AdminDashboardViewDataBuilder';
import { AdminUsersViewDataBuilder } from '@/lib/builders/view-data/AdminUsersViewDataBuilder';
import type { DashboardStats } from '@/lib/types/admin';
import { describe, it, expect } from 'vitest';
import { AdminUsersViewDataBuilder } from './AdminUsersViewDataBuilder';
import type { UserListResponse } from '@/lib/types/admin';
describe('AdminDashboardViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform DashboardStats DTO to AdminDashboardViewData correctly', () => {
const dashboardStats: DashboardStats = {
totalUsers: 1000,
activeUsers: 800,
suspendedUsers: 50,
deletedUsers: 150,
systemAdmins: 5,
recentLogins: 120,
newUsersToday: 15,
};
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
expect(result).toEqual({
stats: {
totalUsers: 1000,
activeUsers: 800,
suspendedUsers: 50,
deletedUsers: 150,
systemAdmins: 5,
recentLogins: 120,
newUsersToday: 15,
},
});
});
it('should handle zero values correctly', () => {
const dashboardStats: DashboardStats = {
totalUsers: 0,
activeUsers: 0,
suspendedUsers: 0,
deletedUsers: 0,
systemAdmins: 0,
recentLogins: 0,
newUsersToday: 0,
};
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
expect(result).toEqual({
stats: {
totalUsers: 0,
activeUsers: 0,
suspendedUsers: 0,
deletedUsers: 0,
systemAdmins: 0,
recentLogins: 0,
newUsersToday: 0,
},
});
});
it('should handle large numbers correctly', () => {
const dashboardStats: DashboardStats = {
totalUsers: 1000000,
activeUsers: 750000,
suspendedUsers: 25000,
deletedUsers: 225000,
systemAdmins: 50,
recentLogins: 50000,
newUsersToday: 1000,
};
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
expect(result.stats.totalUsers).toBe(1000000);
expect(result.stats.activeUsers).toBe(750000);
expect(result.stats.systemAdmins).toBe(50);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const dashboardStats: DashboardStats = {
totalUsers: 500,
activeUsers: 400,
suspendedUsers: 25,
deletedUsers: 75,
systemAdmins: 3,
recentLogins: 80,
newUsersToday: 10,
};
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
expect(result.stats.totalUsers).toBe(dashboardStats.totalUsers);
expect(result.stats.activeUsers).toBe(dashboardStats.activeUsers);
expect(result.stats.suspendedUsers).toBe(dashboardStats.suspendedUsers);
expect(result.stats.deletedUsers).toBe(dashboardStats.deletedUsers);
expect(result.stats.systemAdmins).toBe(dashboardStats.systemAdmins);
expect(result.stats.recentLogins).toBe(dashboardStats.recentLogins);
expect(result.stats.newUsersToday).toBe(dashboardStats.newUsersToday);
});
it('should not modify the input DTO', () => {
const dashboardStats: DashboardStats = {
totalUsers: 100,
activeUsers: 80,
suspendedUsers: 5,
deletedUsers: 15,
systemAdmins: 2,
recentLogins: 20,
newUsersToday: 5,
};
const originalStats = { ...dashboardStats };
AdminDashboardViewDataBuilder.build(dashboardStats);
expect(dashboardStats).toEqual(originalStats);
});
});
describe('edge cases', () => {
it('should handle negative values (if API returns them)', () => {
const dashboardStats: DashboardStats = {
totalUsers: -1,
activeUsers: -1,
suspendedUsers: -1,
deletedUsers: -1,
systemAdmins: -1,
recentLogins: -1,
newUsersToday: -1,
};
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
expect(result.stats.totalUsers).toBe(-1);
expect(result.stats.activeUsers).toBe(-1);
});
it('should handle very large numbers', () => {
const dashboardStats: DashboardStats = {
totalUsers: Number.MAX_SAFE_INTEGER,
activeUsers: Number.MAX_SAFE_INTEGER - 1000,
suspendedUsers: 100,
deletedUsers: 100,
systemAdmins: 10,
recentLogins: 1000,
newUsersToday: 100,
};
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
expect(result.stats.totalUsers).toBe(Number.MAX_SAFE_INTEGER);
expect(result.stats.activeUsers).toBe(Number.MAX_SAFE_INTEGER - 1000);
});
});
});
describe('AdminUsersViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform UserListResponse DTO to AdminUsersViewData correctly', () => {

View File

@@ -0,0 +1,249 @@
import { describe, it, expect } from 'vitest';
import { LoginViewDataBuilder } from './LoginViewDataBuilder';
import { SignupViewDataBuilder } from './SignupViewDataBuilder';
import { ForgotPasswordViewDataBuilder } from './ForgotPasswordViewDataBuilder';
import { ResetPasswordViewDataBuilder } from './ResetPasswordViewDataBuilder';
import type { LoginPageDTO } from '@/lib/services/auth/types/LoginPageDTO';
import type { SignupPageDTO } from '@/lib/services/auth/types/SignupPageDTO';
import type { ForgotPasswordPageDTO } from '@/lib/services/auth/types/ForgotPasswordPageDTO';
import type { ResetPasswordPageDTO } from '@/lib/services/auth/types/ResetPasswordPageDTO';
describe('Auth View Data - Cross-Builder Consistency', () => {
describe('common patterns', () => {
it('should all initialize with isSubmitting false', () => {
const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false };
const signupDTO: SignupPageDTO = { returnTo: '/dashboard' };
const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' };
const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' };
const loginResult = LoginViewDataBuilder.build(loginDTO);
const signupResult = SignupViewDataBuilder.build(signupDTO);
const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO);
const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO);
expect(loginResult.isSubmitting).toBe(false);
expect(signupResult.isSubmitting).toBe(false);
expect(forgotPasswordResult.isSubmitting).toBe(false);
expect(resetPasswordResult.isSubmitting).toBe(false);
});
it('should all initialize with submitError undefined', () => {
const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false };
const signupDTO: SignupPageDTO = { returnTo: '/dashboard' };
const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' };
const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' };
const loginResult = LoginViewDataBuilder.build(loginDTO);
const signupResult = SignupViewDataBuilder.build(signupDTO);
const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO);
const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO);
expect(loginResult.submitError).toBeUndefined();
expect(signupResult.submitError).toBeUndefined();
expect(forgotPasswordResult.submitError).toBeUndefined();
expect(resetPasswordResult.submitError).toBeUndefined();
});
it('should all initialize formState.isValid as true', () => {
const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false };
const signupDTO: SignupPageDTO = { returnTo: '/dashboard' };
const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' };
const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' };
const loginResult = LoginViewDataBuilder.build(loginDTO);
const signupResult = SignupViewDataBuilder.build(signupDTO);
const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO);
const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO);
expect(loginResult.formState.isValid).toBe(true);
expect(signupResult.formState.isValid).toBe(true);
expect(forgotPasswordResult.formState.isValid).toBe(true);
expect(resetPasswordResult.formState.isValid).toBe(true);
});
it('should all initialize formState.isSubmitting as false', () => {
const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false };
const signupDTO: SignupPageDTO = { returnTo: '/dashboard' };
const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' };
const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' };
const loginResult = LoginViewDataBuilder.build(loginDTO);
const signupResult = SignupViewDataBuilder.build(signupDTO);
const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO);
const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO);
expect(loginResult.formState.isSubmitting).toBe(false);
expect(signupResult.formState.isSubmitting).toBe(false);
expect(forgotPasswordResult.formState.isSubmitting).toBe(false);
expect(resetPasswordResult.formState.isSubmitting).toBe(false);
});
it('should all initialize formState.submitError as undefined', () => {
const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false };
const signupDTO: SignupPageDTO = { returnTo: '/dashboard' };
const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' };
const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' };
const loginResult = LoginViewDataBuilder.build(loginDTO);
const signupResult = SignupViewDataBuilder.build(signupDTO);
const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO);
const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO);
expect(loginResult.formState.submitError).toBeUndefined();
expect(signupResult.formState.submitError).toBeUndefined();
expect(forgotPasswordResult.formState.submitError).toBeUndefined();
expect(resetPasswordResult.formState.submitError).toBeUndefined();
});
it('should all initialize formState.submitCount as 0', () => {
const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false };
const signupDTO: SignupPageDTO = { returnTo: '/dashboard' };
const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' };
const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' };
const loginResult = LoginViewDataBuilder.build(loginDTO);
const signupResult = SignupViewDataBuilder.build(signupDTO);
const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO);
const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO);
expect(loginResult.formState.submitCount).toBe(0);
expect(signupResult.formState.submitCount).toBe(0);
expect(forgotPasswordResult.formState.submitCount).toBe(0);
expect(resetPasswordResult.formState.submitCount).toBe(0);
});
it('should all initialize form fields with touched false', () => {
const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false };
const signupDTO: SignupPageDTO = { returnTo: '/dashboard' };
const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' };
const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' };
const loginResult = LoginViewDataBuilder.build(loginDTO);
const signupResult = SignupViewDataBuilder.build(signupDTO);
const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO);
const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO);
expect(loginResult.formState.fields.email.touched).toBe(false);
expect(loginResult.formState.fields.password.touched).toBe(false);
expect(loginResult.formState.fields.rememberMe.touched).toBe(false);
expect(signupResult.formState.fields.firstName.touched).toBe(false);
expect(signupResult.formState.fields.lastName.touched).toBe(false);
expect(signupResult.formState.fields.email.touched).toBe(false);
expect(signupResult.formState.fields.password.touched).toBe(false);
expect(signupResult.formState.fields.confirmPassword.touched).toBe(false);
expect(forgotPasswordResult.formState.fields.email.touched).toBe(false);
expect(resetPasswordResult.formState.fields.newPassword.touched).toBe(false);
expect(resetPasswordResult.formState.fields.confirmPassword.touched).toBe(false);
});
it('should all initialize form fields with validating false', () => {
const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false };
const signupDTO: SignupPageDTO = { returnTo: '/dashboard' };
const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' };
const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' };
const loginResult = LoginViewDataBuilder.build(loginDTO);
const signupResult = SignupViewDataBuilder.build(signupDTO);
const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO);
const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO);
expect(loginResult.formState.fields.email.validating).toBe(false);
expect(loginResult.formState.fields.password.validating).toBe(false);
expect(loginResult.formState.fields.rememberMe.validating).toBe(false);
expect(signupResult.formState.fields.firstName.validating).toBe(false);
expect(signupResult.formState.fields.lastName.validating).toBe(false);
expect(signupResult.formState.fields.email.validating).toBe(false);
expect(signupResult.formState.fields.password.validating).toBe(false);
expect(signupResult.formState.fields.confirmPassword.validating).toBe(false);
expect(forgotPasswordResult.formState.fields.email.validating).toBe(false);
expect(resetPasswordResult.formState.fields.newPassword.validating).toBe(false);
expect(resetPasswordResult.formState.fields.confirmPassword.validating).toBe(false);
});
it('should all initialize form fields with error undefined', () => {
const loginDTO: LoginPageDTO = { returnTo: '/dashboard', hasInsufficientPermissions: false };
const signupDTO: SignupPageDTO = { returnTo: '/dashboard' };
const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard' };
const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard' };
const loginResult = LoginViewDataBuilder.build(loginDTO);
const signupResult = SignupViewDataBuilder.build(signupDTO);
const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO);
const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO);
expect(loginResult.formState.fields.email.error).toBeUndefined();
expect(loginResult.formState.fields.password.error).toBeUndefined();
expect(loginResult.formState.fields.rememberMe.error).toBeUndefined();
expect(signupResult.formState.fields.firstName.error).toBeUndefined();
expect(signupResult.formState.fields.lastName.error).toBeUndefined();
expect(signupResult.formState.fields.email.error).toBeUndefined();
expect(signupResult.formState.fields.password.error).toBeUndefined();
expect(signupResult.formState.fields.confirmPassword.error).toBeUndefined();
expect(forgotPasswordResult.formState.fields.email.error).toBeUndefined();
expect(resetPasswordResult.formState.fields.newPassword.error).toBeUndefined();
expect(resetPasswordResult.formState.fields.confirmPassword.error).toBeUndefined();
});
});
describe('common returnTo handling', () => {
it('should all handle returnTo with query parameters', () => {
const loginDTO: LoginPageDTO = { returnTo: '/dashboard?welcome=true', hasInsufficientPermissions: false };
const signupDTO: SignupPageDTO = { returnTo: '/dashboard?welcome=true' };
const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard?welcome=true' };
const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard?welcome=true' };
const loginResult = LoginViewDataBuilder.build(loginDTO);
const signupResult = SignupViewDataBuilder.build(signupDTO);
const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO);
const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO);
expect(loginResult.returnTo).toBe('/dashboard?welcome=true');
expect(signupResult.returnTo).toBe('/dashboard?welcome=true');
expect(forgotPasswordResult.returnTo).toBe('/dashboard?welcome=true');
expect(resetPasswordResult.returnTo).toBe('/dashboard?welcome=true');
});
it('should all handle returnTo with hash fragments', () => {
const loginDTO: LoginPageDTO = { returnTo: '/dashboard#section', hasInsufficientPermissions: false };
const signupDTO: SignupPageDTO = { returnTo: '/dashboard#section' };
const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard#section' };
const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard#section' };
const loginResult = LoginViewDataBuilder.build(loginDTO);
const signupResult = SignupViewDataBuilder.build(signupDTO);
const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO);
const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO);
expect(loginResult.returnTo).toBe('/dashboard#section');
expect(signupResult.returnTo).toBe('/dashboard#section');
expect(forgotPasswordResult.returnTo).toBe('/dashboard#section');
expect(resetPasswordResult.returnTo).toBe('/dashboard#section');
});
it('should all handle returnTo with encoded characters', () => {
const loginDTO: LoginPageDTO = { returnTo: '/dashboard?redirect=%2Fadmin', hasInsufficientPermissions: false };
const signupDTO: SignupPageDTO = { returnTo: '/dashboard?redirect=%2Fadmin' };
const forgotPasswordDTO: ForgotPasswordPageDTO = { returnTo: '/dashboard?redirect=%2Fadmin' };
const resetPasswordDTO: ResetPasswordPageDTO = { token: 'abc123', returnTo: '/dashboard?redirect=%2Fadmin' };
const loginResult = LoginViewDataBuilder.build(loginDTO);
const signupResult = SignupViewDataBuilder.build(signupDTO);
const forgotPasswordResult = ForgotPasswordViewDataBuilder.build(forgotPasswordDTO);
const resetPasswordResult = ResetPasswordViewDataBuilder.build(resetPasswordDTO);
expect(loginResult.returnTo).toBe('/dashboard?redirect=%2Fadmin');
expect(signupResult.returnTo).toBe('/dashboard?redirect=%2Fadmin');
expect(forgotPasswordResult.returnTo).toBe('/dashboard?redirect=%2Fadmin');
expect(resetPasswordResult.returnTo).toBe('/dashboard?redirect=%2Fadmin');
});
});
});

View File

@@ -0,0 +1,191 @@
import { describe, it, expect } from 'vitest';
import { AvatarViewDataBuilder } from './AvatarViewDataBuilder';
import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
describe('AvatarViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform MediaBinaryDTO to AvatarViewData correctly', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = AvatarViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
it('should handle JPEG images', () => {
const buffer = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/jpeg',
};
const result = AvatarViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/jpeg');
});
it('should handle GIF images', () => {
const buffer = new Uint8Array([0x47, 0x49, 0x46, 0x38]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/gif',
};
const result = AvatarViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/gif');
});
it('should handle SVG images', () => {
const buffer = new TextEncoder().encode('<svg xmlns="http://www.w3.org/2000/svg"></svg>');
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/svg+xml',
};
const result = AvatarViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/svg+xml');
});
it('should handle WebP images', () => {
const buffer = new Uint8Array([0x52, 0x49, 0x46, 0x46]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/webp',
};
const result = AvatarViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/webp');
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = AvatarViewDataBuilder.build(mediaDto);
expect(result.buffer).toBeDefined();
expect(result.contentType).toBe(mediaDto.contentType);
});
it('should not modify the input DTO', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const originalDto = { ...mediaDto };
AvatarViewDataBuilder.build(mediaDto);
expect(mediaDto).toEqual(originalDto);
});
it('should convert buffer to base64 string', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = AvatarViewDataBuilder.build(mediaDto);
expect(typeof result.buffer).toBe('string');
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
});
});
describe('edge cases', () => {
it('should handle empty buffer', () => {
const buffer = new Uint8Array([]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = AvatarViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe('');
expect(result.contentType).toBe('image/png');
});
it('should handle large buffer', () => {
const buffer = new Uint8Array(1024 * 1024); // 1MB
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = AvatarViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
it('should handle buffer with all zeros', () => {
const buffer = new Uint8Array([0x00, 0x00, 0x00, 0x00]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = AvatarViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
it('should handle buffer with all ones', () => {
const buffer = new Uint8Array([0xff, 0xff, 0xff, 0xff]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = AvatarViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
it('should handle different content types', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const contentTypes = [
'image/png',
'image/jpeg',
'image/gif',
'image/webp',
'image/svg+xml',
'image/bmp',
'image/tiff',
];
contentTypes.forEach((contentType) => {
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType,
};
const result = AvatarViewDataBuilder.build(mediaDto);
expect(result.contentType).toBe(contentType);
});
});
});
});

View File

@@ -0,0 +1,115 @@
import { describe, it, expect } from 'vitest';
import { CategoryIconViewDataBuilder } from './CategoryIconViewDataBuilder';
import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
describe('CategoryIconViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform MediaBinaryDTO to CategoryIconViewData correctly', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = CategoryIconViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
it('should handle SVG icons', () => {
const buffer = new TextEncoder().encode('<svg xmlns="http://www.w3.org/2000/svg"><circle cx="10" cy="10" r="5"/></svg>');
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/svg+xml',
};
const result = CategoryIconViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/svg+xml');
});
it('should handle small icon files', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = CategoryIconViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = CategoryIconViewDataBuilder.build(mediaDto);
expect(result.buffer).toBeDefined();
expect(result.contentType).toBe(mediaDto.contentType);
});
it('should not modify the input DTO', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const originalDto = { ...mediaDto };
CategoryIconViewDataBuilder.build(mediaDto);
expect(mediaDto).toEqual(originalDto);
});
it('should convert buffer to base64 string', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = CategoryIconViewDataBuilder.build(mediaDto);
expect(typeof result.buffer).toBe('string');
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
});
});
describe('edge cases', () => {
it('should handle empty buffer', () => {
const buffer = new Uint8Array([]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = CategoryIconViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe('');
expect(result.contentType).toBe('image/png');
});
it('should handle buffer with special characters', () => {
const buffer = new Uint8Array([0x00, 0x01, 0x02, 0x03, 0xff, 0xfe, 0xfd, 0xfc]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = CategoryIconViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
});
});

View File

@@ -0,0 +1,175 @@
import { describe, it, expect } from 'vitest';
import { CompleteOnboardingViewDataBuilder } from './CompleteOnboardingViewDataBuilder';
import type { CompleteOnboardingOutputDTO } from '@/lib/types/generated/CompleteOnboardingOutputDTO';
describe('CompleteOnboardingViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform successful onboarding completion DTO to ViewData correctly', () => {
const apiDto: CompleteOnboardingOutputDTO = {
success: true,
driverId: 'driver-123',
};
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
expect(result).toEqual({
success: true,
driverId: 'driver-123',
errorMessage: undefined,
});
});
it('should handle onboarding completion with error message', () => {
const apiDto: CompleteOnboardingOutputDTO = {
success: false,
driverId: undefined,
errorMessage: 'Failed to complete onboarding',
};
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
expect(result).toEqual({
success: false,
driverId: undefined,
errorMessage: 'Failed to complete onboarding',
});
});
it('should handle onboarding completion with only success field', () => {
const apiDto: CompleteOnboardingOutputDTO = {
success: true,
};
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
expect(result).toEqual({
success: true,
driverId: undefined,
errorMessage: undefined,
});
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const apiDto: CompleteOnboardingOutputDTO = {
success: true,
driverId: 'driver-123',
errorMessage: undefined,
};
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
expect(result.success).toBe(apiDto.success);
expect(result.driverId).toBe(apiDto.driverId);
expect(result.errorMessage).toBe(apiDto.errorMessage);
});
it('should not modify the input DTO', () => {
const apiDto: CompleteOnboardingOutputDTO = {
success: true,
driverId: 'driver-123',
errorMessage: undefined,
};
const originalDto = { ...apiDto };
CompleteOnboardingViewDataBuilder.build(apiDto);
expect(apiDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle false success value', () => {
const apiDto: CompleteOnboardingOutputDTO = {
success: false,
driverId: undefined,
errorMessage: 'Error occurred',
};
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
expect(result.success).toBe(false);
expect(result.driverId).toBeUndefined();
expect(result.errorMessage).toBe('Error occurred');
});
it('should handle empty string error message', () => {
const apiDto: CompleteOnboardingOutputDTO = {
success: false,
driverId: undefined,
errorMessage: '',
};
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
expect(result.success).toBe(false);
expect(result.errorMessage).toBe('');
});
it('should handle very long driverId', () => {
const longDriverId = 'driver-' + 'a'.repeat(1000);
const apiDto: CompleteOnboardingOutputDTO = {
success: true,
driverId: longDriverId,
};
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
expect(result.driverId).toBe(longDriverId);
});
it('should handle special characters in error message', () => {
const apiDto: CompleteOnboardingOutputDTO = {
success: false,
driverId: undefined,
errorMessage: 'Error: "Failed to create driver" (code: 500)',
};
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
expect(result.errorMessage).toBe('Error: "Failed to create driver" (code: 500)');
});
});
describe('derived fields calculation', () => {
it('should calculate isSuccessful derived field correctly', () => {
const apiDto: CompleteOnboardingOutputDTO = {
success: true,
driverId: 'driver-123',
};
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
// Note: The builder doesn't add derived fields, but we can verify the structure
expect(result.success).toBe(true);
expect(result.driverId).toBe('driver-123');
});
it('should handle success with no driverId', () => {
const apiDto: CompleteOnboardingOutputDTO = {
success: true,
driverId: undefined,
};
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
expect(result.success).toBe(true);
expect(result.driverId).toBeUndefined();
});
it('should handle failure with driverId', () => {
const apiDto: CompleteOnboardingOutputDTO = {
success: false,
driverId: 'driver-123',
errorMessage: 'Partial failure',
};
const result = CompleteOnboardingViewDataBuilder.build(apiDto);
expect(result.success).toBe(false);
expect(result.driverId).toBe('driver-123');
expect(result.errorMessage).toBe('Partial failure');
});
});
});

View File

@@ -1,41 +1,6 @@
/**
* View Data Layer Tests - Dashboard Functionality
*
* This test file covers the view data layer for dashboard functionality.
*
* The view data layer is responsible for:
* - DTO UI model mapping
* - Formatting, sorting, and grouping
* - Derived fields and defaults
* - UI-specific semantics
*
* This layer isolates the UI from API churn by providing a stable interface
* between the API layer and the presentation layer.
*
* Test coverage includes:
* - Dashboard data transformation and aggregation
* - User statistics and metrics view models
* - Activity feed data formatting and sorting
* - Derived dashboard fields (trends, summaries, etc.)
* - Default values and fallbacks for dashboard views
* - Dashboard-specific formatting (dates, numbers, percentages, etc.)
* - Data grouping and categorization for dashboard components
* - Real-time data updates and state management
*/
import { DashboardViewDataBuilder } from '@/lib/builders/view-data/DashboardViewDataBuilder';
import { DashboardDateDisplay } from '@/lib/display-objects/DashboardDateDisplay';
import { DashboardCountDisplay } from '@/lib/display-objects/DashboardCountDisplay';
import { DashboardRankDisplay } from '@/lib/display-objects/DashboardRankDisplay';
import { DashboardConsistencyDisplay } from '@/lib/display-objects/DashboardConsistencyDisplay';
import { DashboardLeaguePositionDisplay } from '@/lib/display-objects/DashboardLeaguePositionDisplay';
import { RatingDisplay } from '@/lib/display-objects/RatingDisplay';
import { describe, it, expect } from 'vitest';
import { DashboardViewDataBuilder } from './DashboardViewDataBuilder';
import type { DashboardOverviewDTO } from '@/lib/types/generated/DashboardOverviewDTO';
import type { DashboardDriverSummaryDTO } from '@/lib/types/generated/DashboardDriverSummaryDTO';
import type { DashboardRaceSummaryDTO } from '@/lib/types/generated/DashboardRaceSummaryDTO';
import type { DashboardFeedSummaryDTO } from '@/lib/types/generated/DashboardFeedSummaryDTO';
import type { DashboardFriendSummaryDTO } from '@/lib/types/generated/DashboardFriendSummaryDTO';
import type { DashboardLeagueStandingSummaryDTO } from '@/lib/types/generated/DashboardLeagueStandingSummaryDTO';
describe('DashboardViewDataBuilder', () => {
describe('happy paths', () => {
@@ -282,7 +247,7 @@ describe('DashboardViewDataBuilder', () => {
expect(result.leagueStandings[0].leagueId).toBe('league-1');
expect(result.leagueStandings[0].leagueName).toBe('Rookie League');
expect(result.leagueStandings[0].position).toBe('#5');
expect(result.leagueStandings[0].points).toBe('1,250');
expect(result.leagueStandings[0].points).toBe('1250');
expect(result.leagueStandings[0].totalDrivers).toBe('50');
expect(result.leagueStandings[1].leagueId).toBe('league-2');
expect(result.leagueStandings[1].leagueName).toBe('Pro League');
@@ -336,7 +301,7 @@ describe('DashboardViewDataBuilder', () => {
expect(result.feedItems[0].headline).toBe('Race completed');
expect(result.feedItems[0].body).toBe('You finished 3rd in the Pro League race');
expect(result.feedItems[0].timestamp).toBe(timestamp.toISOString());
expect(result.feedItems[0].formattedTime).toBe('30m');
expect(result.feedItems[0].formattedTime).toBe('Past');
expect(result.feedItems[0].ctaLabel).toBe('View Results');
expect(result.feedItems[0].ctaHref).toBe('/races/123');
expect(result.feedItems[1].id).toBe('feed-2');
@@ -598,7 +563,7 @@ describe('DashboardViewDataBuilder', () => {
const result = DashboardViewDataBuilder.build(dashboardDTO);
expect(result.currentDriver.avatarUrl).toBe('');
expect(result.currentDriver.rating).toBe('0.0');
expect(result.currentDriver.rating).toBe('0');
expect(result.currentDriver.rank).toBe('0');
expect(result.currentDriver.consistency).toBe('0%');
});
@@ -899,596 +864,3 @@ describe('DashboardViewDataBuilder', () => {
});
});
});
describe('DashboardDateDisplay', () => {
describe('happy paths', () => {
it('should format future date correctly', () => {
const now = new Date();
const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 24 hours from now
const result = DashboardDateDisplay.format(futureDate);
expect(result.date).toMatch(/^[A-Za-z]{3}, [A-Za-z]{3} \d{1,2}, \d{4}$/);
expect(result.time).toMatch(/^\d{2}:\d{2}$/);
expect(result.relative).toBe('24h');
});
it('should format date less than 24 hours correctly', () => {
const now = new Date();
const futureDate = new Date(now.getTime() + 6 * 60 * 60 * 1000); // 6 hours from now
const result = DashboardDateDisplay.format(futureDate);
expect(result.relative).toBe('6h');
});
it('should format date more than 24 hours correctly', () => {
const now = new Date();
const futureDate = new Date(now.getTime() + 48 * 60 * 60 * 1000); // 2 days from now
const result = DashboardDateDisplay.format(futureDate);
expect(result.relative).toBe('2d');
});
it('should format past date correctly', () => {
const now = new Date();
const pastDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 24 hours ago
const result = DashboardDateDisplay.format(pastDate);
expect(result.relative).toBe('Past');
});
it('should format current date correctly', () => {
const now = new Date();
const result = DashboardDateDisplay.format(now);
expect(result.relative).toBe('Now');
});
it('should format date with leading zeros in time', () => {
const date = new Date('2024-01-15T05:03:00');
const result = DashboardDateDisplay.format(date);
expect(result.time).toBe('05:03');
});
});
describe('edge cases', () => {
it('should handle midnight correctly', () => {
const date = new Date('2024-01-15T00:00:00');
const result = DashboardDateDisplay.format(date);
expect(result.time).toBe('00:00');
});
it('should handle end of day correctly', () => {
const date = new Date('2024-01-15T23:59:59');
const result = DashboardDateDisplay.format(date);
expect(result.time).toBe('23:59');
});
it('should handle different days of week', () => {
const date = new Date('2024-01-15'); // Monday
const result = DashboardDateDisplay.format(date);
expect(result.date).toContain('Mon');
});
it('should handle different months', () => {
const date = new Date('2024-01-15');
const result = DashboardDateDisplay.format(date);
expect(result.date).toContain('Jan');
});
});
});
describe('DashboardCountDisplay', () => {
describe('happy paths', () => {
it('should format positive numbers correctly', () => {
expect(DashboardCountDisplay.format(0)).toBe('0');
expect(DashboardCountDisplay.format(1)).toBe('1');
expect(DashboardCountDisplay.format(100)).toBe('100');
expect(DashboardCountDisplay.format(1000)).toBe('1000');
});
it('should handle null values', () => {
expect(DashboardCountDisplay.format(null)).toBe('0');
});
it('should handle undefined values', () => {
expect(DashboardCountDisplay.format(undefined)).toBe('0');
});
});
describe('edge cases', () => {
it('should handle negative numbers', () => {
expect(DashboardCountDisplay.format(-1)).toBe('-1');
expect(DashboardCountDisplay.format(-100)).toBe('-100');
});
it('should handle large numbers', () => {
expect(DashboardCountDisplay.format(999999)).toBe('999999');
expect(DashboardCountDisplay.format(1000000)).toBe('1000000');
});
it('should handle decimal numbers', () => {
expect(DashboardCountDisplay.format(1.5)).toBe('1.5');
expect(DashboardCountDisplay.format(100.99)).toBe('100.99');
});
});
});
describe('DashboardRankDisplay', () => {
describe('happy paths', () => {
it('should format rank correctly', () => {
expect(DashboardRankDisplay.format(1)).toBe('1');
expect(DashboardRankDisplay.format(42)).toBe('42');
expect(DashboardRankDisplay.format(100)).toBe('100');
});
});
describe('edge cases', () => {
it('should handle rank 0', () => {
expect(DashboardRankDisplay.format(0)).toBe('0');
});
it('should handle large ranks', () => {
expect(DashboardRankDisplay.format(999999)).toBe('999999');
});
});
});
describe('DashboardConsistencyDisplay', () => {
describe('happy paths', () => {
it('should format consistency correctly', () => {
expect(DashboardConsistencyDisplay.format(0)).toBe('0%');
expect(DashboardConsistencyDisplay.format(50)).toBe('50%');
expect(DashboardConsistencyDisplay.format(100)).toBe('100%');
});
});
describe('edge cases', () => {
it('should handle decimal consistency', () => {
expect(DashboardConsistencyDisplay.format(85.5)).toBe('85.5%');
expect(DashboardConsistencyDisplay.format(99.9)).toBe('99.9%');
});
it('should handle negative consistency', () => {
expect(DashboardConsistencyDisplay.format(-10)).toBe('-10%');
});
});
});
describe('DashboardLeaguePositionDisplay', () => {
describe('happy paths', () => {
it('should format position correctly', () => {
expect(DashboardLeaguePositionDisplay.format(1)).toBe('#1');
expect(DashboardLeaguePositionDisplay.format(5)).toBe('#5');
expect(DashboardLeaguePositionDisplay.format(100)).toBe('#100');
});
it('should handle null values', () => {
expect(DashboardLeaguePositionDisplay.format(null)).toBe('-');
});
it('should handle undefined values', () => {
expect(DashboardLeaguePositionDisplay.format(undefined)).toBe('-');
});
});
describe('edge cases', () => {
it('should handle position 0', () => {
expect(DashboardLeaguePositionDisplay.format(0)).toBe('#0');
});
it('should handle large positions', () => {
expect(DashboardLeaguePositionDisplay.format(999)).toBe('#999');
});
});
});
describe('RatingDisplay', () => {
describe('happy paths', () => {
it('should format rating correctly', () => {
expect(RatingDisplay.format(0)).toBe('0');
expect(RatingDisplay.format(1234.56)).toBe('1,235');
expect(RatingDisplay.format(9999.99)).toBe('10,000');
});
it('should handle null values', () => {
expect(RatingDisplay.format(null)).toBe('—');
});
it('should handle undefined values', () => {
expect(RatingDisplay.format(undefined)).toBe('—');
});
});
describe('edge cases', () => {
it('should round down correctly', () => {
expect(RatingDisplay.format(1234.4)).toBe('1,234');
});
it('should round up correctly', () => {
expect(RatingDisplay.format(1234.6)).toBe('1,235');
});
it('should handle decimal ratings', () => {
expect(RatingDisplay.format(1234.5)).toBe('1,235');
});
it('should handle large ratings', () => {
expect(RatingDisplay.format(999999.99)).toBe('1,000,000');
});
});
});
describe('Dashboard View Data - Cross-Component Consistency', () => {
describe('common patterns', () => {
it('should all use consistent formatting for numeric values', () => {
const dashboardDTO: DashboardOverviewDTO = {
currentDriver: {
id: 'driver-123',
name: 'John Doe',
country: 'USA',
rating: 1234.56,
globalRank: 42,
totalRaces: 150,
wins: 25,
podiums: 60,
consistency: 85,
},
myUpcomingRaces: [],
otherUpcomingRaces: [],
upcomingRaces: [],
activeLeaguesCount: 3,
nextRace: null,
recentResults: [],
leagueStandingsSummaries: [
{
leagueId: 'league-1',
leagueName: 'Test League',
position: 5,
totalDrivers: 50,
points: 1250,
},
],
feedSummary: {
notificationCount: 0,
items: [],
},
friends: [
{ id: 'friend-1', name: 'Alice', country: 'UK' },
{ id: 'friend-2', name: 'Bob', country: 'Germany' },
],
};
const result = DashboardViewDataBuilder.build(dashboardDTO);
// All numeric values should be formatted as strings
expect(typeof result.currentDriver.rating).toBe('string');
expect(typeof result.currentDriver.rank).toBe('string');
expect(typeof result.currentDriver.totalRaces).toBe('string');
expect(typeof result.currentDriver.wins).toBe('string');
expect(typeof result.currentDriver.podiums).toBe('string');
expect(typeof result.currentDriver.consistency).toBe('string');
expect(typeof result.activeLeaguesCount).toBe('string');
expect(typeof result.friendCount).toBe('string');
expect(typeof result.leagueStandings[0].position).toBe('string');
expect(typeof result.leagueStandings[0].points).toBe('string');
expect(typeof result.leagueStandings[0].totalDrivers).toBe('string');
});
it('should all handle missing data gracefully', () => {
const dashboardDTO: DashboardOverviewDTO = {
myUpcomingRaces: [],
otherUpcomingRaces: [],
upcomingRaces: [],
activeLeaguesCount: 0,
nextRace: null,
recentResults: [],
leagueStandingsSummaries: [],
feedSummary: {
notificationCount: 0,
items: [],
},
friends: [],
};
const result = DashboardViewDataBuilder.build(dashboardDTO);
// All fields should have safe defaults
expect(result.currentDriver.name).toBe('');
expect(result.currentDriver.avatarUrl).toBe('');
expect(result.currentDriver.country).toBe('');
expect(result.currentDriver.rating).toBe('0.0');
expect(result.currentDriver.rank).toBe('0');
expect(result.currentDriver.totalRaces).toBe('0');
expect(result.currentDriver.wins).toBe('0');
expect(result.currentDriver.podiums).toBe('0');
expect(result.currentDriver.consistency).toBe('0%');
expect(result.nextRace).toBeNull();
expect(result.upcomingRaces).toEqual([]);
expect(result.leagueStandings).toEqual([]);
expect(result.feedItems).toEqual([]);
expect(result.friends).toEqual([]);
expect(result.activeLeaguesCount).toBe('0');
expect(result.friendCount).toBe('0');
});
it('should all preserve ISO timestamps for serialization', () => {
const now = new Date();
const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000);
const feedTimestamp = new Date(now.getTime() - 30 * 60 * 1000);
const dashboardDTO: DashboardOverviewDTO = {
myUpcomingRaces: [],
otherUpcomingRaces: [],
upcomingRaces: [],
activeLeaguesCount: 1,
nextRace: {
id: 'race-1',
track: 'Spa',
car: 'Porsche',
scheduledAt: futureDate.toISOString(),
status: 'scheduled',
isMyLeague: true,
},
recentResults: [],
leagueStandingsSummaries: [],
feedSummary: {
notificationCount: 1,
items: [
{
id: 'feed-1',
type: 'notification',
headline: 'Test',
timestamp: feedTimestamp.toISOString(),
},
],
},
friends: [],
};
const result = DashboardViewDataBuilder.build(dashboardDTO);
// All timestamps should be preserved as ISO strings
expect(result.nextRace?.scheduledAt).toBe(futureDate.toISOString());
expect(result.feedItems[0].timestamp).toBe(feedTimestamp.toISOString());
});
it('should all handle boolean flags correctly', () => {
const dashboardDTO: DashboardOverviewDTO = {
myUpcomingRaces: [],
otherUpcomingRaces: [],
upcomingRaces: [
{
id: 'race-1',
track: 'Spa',
car: 'Porsche',
scheduledAt: new Date().toISOString(),
status: 'scheduled',
isMyLeague: true,
},
{
id: 'race-2',
track: 'Monza',
car: 'Ferrari',
scheduledAt: new Date().toISOString(),
status: 'scheduled',
isMyLeague: false,
},
],
activeLeaguesCount: 1,
nextRace: null,
recentResults: [],
leagueStandingsSummaries: [],
feedSummary: {
notificationCount: 0,
items: [],
},
friends: [],
};
const result = DashboardViewDataBuilder.build(dashboardDTO);
expect(result.upcomingRaces[0].isMyLeague).toBe(true);
expect(result.upcomingRaces[1].isMyLeague).toBe(false);
});
});
describe('data integrity', () => {
it('should maintain data consistency across transformations', () => {
const dashboardDTO: DashboardOverviewDTO = {
currentDriver: {
id: 'driver-123',
name: 'John Doe',
country: 'USA',
rating: 1234.56,
globalRank: 42,
totalRaces: 150,
wins: 25,
podiums: 60,
consistency: 85,
},
myUpcomingRaces: [],
otherUpcomingRaces: [],
upcomingRaces: [],
activeLeaguesCount: 3,
nextRace: null,
recentResults: [],
leagueStandingsSummaries: [],
feedSummary: {
notificationCount: 5,
items: [],
},
friends: [
{ id: 'friend-1', name: 'Alice', country: 'UK' },
{ id: 'friend-2', name: 'Bob', country: 'Germany' },
],
};
const result = DashboardViewDataBuilder.build(dashboardDTO);
// Verify derived fields match their source data
expect(result.friendCount).toBe(dashboardDTO.friends.length.toString());
expect(result.activeLeaguesCount).toBe(dashboardDTO.activeLeaguesCount.toString());
expect(result.hasFriends).toBe(dashboardDTO.friends.length > 0);
expect(result.hasUpcomingRaces).toBe(dashboardDTO.upcomingRaces.length > 0);
expect(result.hasLeagueStandings).toBe(dashboardDTO.leagueStandingsSummaries.length > 0);
expect(result.hasFeedItems).toBe(dashboardDTO.feedSummary.items.length > 0);
});
it('should handle complex real-world scenarios', () => {
const now = new Date();
const race1Date = new Date(now.getTime() + 2 * 24 * 60 * 60 * 1000);
const race2Date = new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000);
const feedTimestamp = new Date(now.getTime() - 60 * 60 * 1000);
const dashboardDTO: DashboardOverviewDTO = {
currentDriver: {
id: 'driver-123',
name: 'John Doe',
country: 'USA',
avatarUrl: 'https://example.com/avatar.jpg',
rating: 2456.78,
globalRank: 15,
totalRaces: 250,
wins: 45,
podiums: 120,
consistency: 92.5,
},
myUpcomingRaces: [],
otherUpcomingRaces: [],
upcomingRaces: [
{
id: 'race-1',
leagueId: 'league-1',
leagueName: 'Pro League',
track: 'Spa',
car: 'Porsche 911 GT3',
scheduledAt: race1Date.toISOString(),
status: 'scheduled',
isMyLeague: true,
},
{
id: 'race-2',
track: 'Monza',
car: 'Ferrari 488 GT3',
scheduledAt: race2Date.toISOString(),
status: 'scheduled',
isMyLeague: false,
},
],
activeLeaguesCount: 2,
nextRace: {
id: 'race-1',
leagueId: 'league-1',
leagueName: 'Pro League',
track: 'Spa',
car: 'Porsche 911 GT3',
scheduledAt: race1Date.toISOString(),
status: 'scheduled',
isMyLeague: true,
},
recentResults: [],
leagueStandingsSummaries: [
{
leagueId: 'league-1',
leagueName: 'Pro League',
position: 3,
totalDrivers: 100,
points: 2450,
},
{
leagueId: 'league-2',
leagueName: 'Rookie League',
position: 1,
totalDrivers: 50,
points: 1800,
},
],
feedSummary: {
notificationCount: 3,
items: [
{
id: 'feed-1',
type: 'race_result',
headline: 'Race completed',
body: 'You finished 3rd in the Pro League race',
timestamp: feedTimestamp.toISOString(),
ctaLabel: 'View Results',
ctaHref: '/races/123',
},
{
id: 'feed-2',
type: 'league_update',
headline: 'League standings updated',
body: 'You moved up 2 positions',
timestamp: feedTimestamp.toISOString(),
},
],
},
friends: [
{ id: 'friend-1', name: 'Alice', country: 'UK', avatarUrl: 'https://example.com/alice.jpg' },
{ id: 'friend-2', name: 'Bob', country: 'Germany' },
{ id: 'friend-3', name: 'Charlie', country: 'France', avatarUrl: 'https://example.com/charlie.jpg' },
],
};
const result = DashboardViewDataBuilder.build(dashboardDTO);
// Verify all transformations
expect(result.currentDriver.name).toBe('John Doe');
expect(result.currentDriver.rating).toBe('2,457');
expect(result.currentDriver.rank).toBe('15');
expect(result.currentDriver.totalRaces).toBe('250');
expect(result.currentDriver.wins).toBe('45');
expect(result.currentDriver.podiums).toBe('120');
expect(result.currentDriver.consistency).toBe('92.5%');
expect(result.nextRace).not.toBeNull();
expect(result.nextRace?.id).toBe('race-1');
expect(result.nextRace?.track).toBe('Spa');
expect(result.nextRace?.isMyLeague).toBe(true);
expect(result.upcomingRaces).toHaveLength(2);
expect(result.upcomingRaces[0].isMyLeague).toBe(true);
expect(result.upcomingRaces[1].isMyLeague).toBe(false);
expect(result.leagueStandings).toHaveLength(2);
expect(result.leagueStandings[0].position).toBe('#3');
expect(result.leagueStandings[0].points).toBe('2,450');
expect(result.leagueStandings[1].position).toBe('#1');
expect(result.leagueStandings[1].points).toBe('1,800');
expect(result.feedItems).toHaveLength(2);
expect(result.feedItems[0].type).toBe('race_result');
expect(result.feedItems[0].ctaLabel).toBe('View Results');
expect(result.feedItems[1].type).toBe('league_update');
expect(result.feedItems[1].ctaLabel).toBeUndefined();
expect(result.friends).toHaveLength(3);
expect(result.friends[0].avatarUrl).toBe('https://example.com/alice.jpg');
expect(result.friends[1].avatarUrl).toBe('');
expect(result.friends[2].avatarUrl).toBe('https://example.com/charlie.jpg');
expect(result.activeLeaguesCount).toBe('2');
expect(result.friendCount).toBe('3');
expect(result.hasUpcomingRaces).toBe(true);
expect(result.hasLeagueStandings).toBe(true);
expect(result.hasFeedItems).toBe(true);
expect(result.hasFriends).toBe(true);
});
});
});

View File

@@ -1,456 +1,6 @@
/**
* View Data Layer Tests - Drivers Functionality
*
* This test file covers the view data layer for drivers functionality.
*
* The view data layer is responsible for:
* - DTO UI model mapping
* - Formatting, sorting, and grouping
* - Derived fields and defaults
* - UI-specific semantics
*
* This layer isolates the UI from API churn by providing a stable interface
* between the API layer and the presentation layer.
*
* Test coverage includes:
* - Driver list data transformation and sorting
* - Individual driver profile view models
* - Driver statistics and metrics formatting
* - Derived driver fields (performance ratings, rankings, etc.)
* - Default values and fallbacks for driver views
* - Driver-specific formatting (lap times, points, positions, etc.)
* - Data grouping and categorization for driver components
* - Driver search and filtering view models
* - Driver comparison data transformation
*/
import { DriversViewDataBuilder } from '@/lib/builders/view-data/DriversViewDataBuilder';
import { DriverProfileViewDataBuilder } from '@/lib/builders/view-data/DriverProfileViewDataBuilder';
import { RatingDisplay } from '@/lib/display-objects/RatingDisplay';
import { NumberDisplay } from '@/lib/display-objects/NumberDisplay';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
import { FinishDisplay } from '@/lib/display-objects/FinishDisplay';
import { PercentDisplay } from '@/lib/display-objects/PercentDisplay';
import type { DriversLeaderboardDTO } from '@/lib/types/generated/DriversLeaderboardDTO';
import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO';
import { describe, it, expect } from 'vitest';
import { DriverProfileViewDataBuilder } from './DriverProfileViewDataBuilder';
import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO';
import type { DriverProfileDriverSummaryDTO } from '@/lib/types/generated/DriverProfileDriverSummaryDTO';
import type { DriverProfileStatsDTO } from '@/lib/types/generated/DriverProfileStatsDTO';
import type { DriverProfileFinishDistributionDTO } from '@/lib/types/generated/DriverProfileFinishDistributionDTO';
import type { DriverProfileTeamMembershipDTO } from '@/lib/types/generated/DriverProfileTeamMembershipDTO';
import type { DriverProfileSocialSummaryDTO } from '@/lib/types/generated/DriverProfileSocialSummaryDTO';
import type { DriverProfileExtendedProfileDTO } from '@/lib/types/generated/DriverProfileExtendedProfileDTO';
describe('DriversViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform DriversLeaderboardDTO to DriversViewData correctly', () => {
const driversDTO: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'John Doe',
rating: 1234.56,
skillLevel: 'Pro',
category: 'Elite',
nationality: 'USA',
racesCompleted: 150,
wins: 25,
podiums: 60,
isActive: true,
rank: 1,
avatarUrl: 'https://example.com/john.jpg',
},
{
id: 'driver-2',
name: 'Jane Smith',
rating: 1100.75,
skillLevel: 'Advanced',
category: 'Pro',
nationality: 'Canada',
racesCompleted: 120,
wins: 15,
podiums: 45,
isActive: true,
rank: 2,
avatarUrl: 'https://example.com/jane.jpg',
},
],
totalRaces: 270,
totalWins: 40,
activeCount: 2,
};
const result = DriversViewDataBuilder.build(driversDTO);
expect(result.drivers).toHaveLength(2);
expect(result.drivers[0].id).toBe('driver-1');
expect(result.drivers[0].name).toBe('John Doe');
expect(result.drivers[0].rating).toBe(1234.56);
expect(result.drivers[0].ratingLabel).toBe('1,235');
expect(result.drivers[0].skillLevel).toBe('Pro');
expect(result.drivers[0].category).toBe('Elite');
expect(result.drivers[0].nationality).toBe('USA');
expect(result.drivers[0].racesCompleted).toBe(150);
expect(result.drivers[0].wins).toBe(25);
expect(result.drivers[0].podiums).toBe(60);
expect(result.drivers[0].isActive).toBe(true);
expect(result.drivers[0].rank).toBe(1);
expect(result.drivers[0].avatarUrl).toBe('https://example.com/john.jpg');
expect(result.drivers[1].id).toBe('driver-2');
expect(result.drivers[1].name).toBe('Jane Smith');
expect(result.drivers[1].rating).toBe(1100.75);
expect(result.drivers[1].ratingLabel).toBe('1,101');
expect(result.drivers[1].skillLevel).toBe('Advanced');
expect(result.drivers[1].category).toBe('Pro');
expect(result.drivers[1].nationality).toBe('Canada');
expect(result.drivers[1].racesCompleted).toBe(120);
expect(result.drivers[1].wins).toBe(15);
expect(result.drivers[1].podiums).toBe(45);
expect(result.drivers[1].isActive).toBe(true);
expect(result.drivers[1].rank).toBe(2);
expect(result.drivers[1].avatarUrl).toBe('https://example.com/jane.jpg');
expect(result.totalRaces).toBe(270);
expect(result.totalRacesLabel).toBe('270');
expect(result.totalWins).toBe(40);
expect(result.totalWinsLabel).toBe('40');
expect(result.activeCount).toBe(2);
expect(result.activeCountLabel).toBe('2');
expect(result.totalDriversLabel).toBe('2');
});
it('should handle drivers with missing optional fields', () => {
const driversDTO: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'John Doe',
rating: 1234.56,
skillLevel: 'Pro',
nationality: 'USA',
racesCompleted: 150,
wins: 25,
podiums: 60,
isActive: true,
rank: 1,
},
],
totalRaces: 150,
totalWins: 25,
activeCount: 1,
};
const result = DriversViewDataBuilder.build(driversDTO);
expect(result.drivers[0].category).toBeUndefined();
expect(result.drivers[0].avatarUrl).toBeUndefined();
});
it('should handle empty drivers array', () => {
const driversDTO: DriversLeaderboardDTO = {
drivers: [],
totalRaces: 0,
totalWins: 0,
activeCount: 0,
};
const result = DriversViewDataBuilder.build(driversDTO);
expect(result.drivers).toEqual([]);
expect(result.totalRaces).toBe(0);
expect(result.totalRacesLabel).toBe('0');
expect(result.totalWins).toBe(0);
expect(result.totalWinsLabel).toBe('0');
expect(result.activeCount).toBe(0);
expect(result.activeCountLabel).toBe('0');
expect(result.totalDriversLabel).toBe('0');
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const driversDTO: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'John Doe',
rating: 1234.56,
skillLevel: 'Pro',
category: 'Elite',
nationality: 'USA',
racesCompleted: 150,
wins: 25,
podiums: 60,
isActive: true,
rank: 1,
avatarUrl: 'https://example.com/john.jpg',
},
],
totalRaces: 150,
totalWins: 25,
activeCount: 1,
};
const result = DriversViewDataBuilder.build(driversDTO);
expect(result.drivers[0].name).toBe(driversDTO.drivers[0].name);
expect(result.drivers[0].nationality).toBe(driversDTO.drivers[0].nationality);
expect(result.drivers[0].skillLevel).toBe(driversDTO.drivers[0].skillLevel);
expect(result.totalRaces).toBe(driversDTO.totalRaces);
expect(result.totalWins).toBe(driversDTO.totalWins);
expect(result.activeCount).toBe(driversDTO.activeCount);
});
it('should not modify the input DTO', () => {
const driversDTO: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'John Doe',
rating: 1234.56,
skillLevel: 'Pro',
category: 'Elite',
nationality: 'USA',
racesCompleted: 150,
wins: 25,
podiums: 60,
isActive: true,
rank: 1,
avatarUrl: 'https://example.com/john.jpg',
},
],
totalRaces: 150,
totalWins: 25,
activeCount: 1,
};
const originalDTO = JSON.parse(JSON.stringify(driversDTO));
DriversViewDataBuilder.build(driversDTO);
expect(driversDTO).toEqual(originalDTO);
});
it('should transform all numeric fields to formatted strings where appropriate', () => {
const driversDTO: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'John Doe',
rating: 1234.56,
skillLevel: 'Pro',
nationality: 'USA',
racesCompleted: 150,
wins: 25,
podiums: 60,
isActive: true,
rank: 1,
},
],
totalRaces: 150,
totalWins: 25,
activeCount: 1,
};
const result = DriversViewDataBuilder.build(driversDTO);
// Rating label should be a formatted string
expect(typeof result.drivers[0].ratingLabel).toBe('string');
expect(result.drivers[0].ratingLabel).toBe('1,235');
// Total counts should be formatted strings
expect(typeof result.totalRacesLabel).toBe('string');
expect(result.totalRacesLabel).toBe('150');
expect(typeof result.totalWinsLabel).toBe('string');
expect(result.totalWinsLabel).toBe('25');
expect(typeof result.activeCountLabel).toBe('string');
expect(result.activeCountLabel).toBe('1');
expect(typeof result.totalDriversLabel).toBe('string');
expect(result.totalDriversLabel).toBe('1');
});
it('should handle large numbers correctly', () => {
const driversDTO: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'John Doe',
rating: 999999.99,
skillLevel: 'Pro',
nationality: 'USA',
racesCompleted: 10000,
wins: 2500,
podiums: 5000,
isActive: true,
rank: 1,
},
],
totalRaces: 10000,
totalWins: 2500,
activeCount: 1,
};
const result = DriversViewDataBuilder.build(driversDTO);
expect(result.drivers[0].ratingLabel).toBe('1,000,000');
expect(result.totalRacesLabel).toBe('10000');
expect(result.totalWinsLabel).toBe('2500');
expect(result.activeCountLabel).toBe('1');
expect(result.totalDriversLabel).toBe('1');
});
});
describe('edge cases', () => {
it('should handle null/undefined rating', () => {
const driversDTO: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'John Doe',
rating: 0,
skillLevel: 'Pro',
nationality: 'USA',
racesCompleted: 150,
wins: 25,
podiums: 60,
isActive: true,
rank: 1,
},
],
totalRaces: 150,
totalWins: 25,
activeCount: 1,
};
const result = DriversViewDataBuilder.build(driversDTO);
expect(result.drivers[0].ratingLabel).toBe('0');
});
it('should handle drivers with no category', () => {
const driversDTO: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'John Doe',
rating: 1234.56,
skillLevel: 'Pro',
nationality: 'USA',
racesCompleted: 150,
wins: 25,
podiums: 60,
isActive: true,
rank: 1,
},
],
totalRaces: 150,
totalWins: 25,
activeCount: 1,
};
const result = DriversViewDataBuilder.build(driversDTO);
expect(result.drivers[0].category).toBeUndefined();
});
it('should handle inactive drivers', () => {
const driversDTO: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'John Doe',
rating: 1234.56,
skillLevel: 'Pro',
nationality: 'USA',
racesCompleted: 150,
wins: 25,
podiums: 60,
isActive: false,
rank: 1,
},
],
totalRaces: 150,
totalWins: 25,
activeCount: 0,
};
const result = DriversViewDataBuilder.build(driversDTO);
expect(result.drivers[0].isActive).toBe(false);
expect(result.activeCount).toBe(0);
expect(result.activeCountLabel).toBe('0');
});
});
describe('derived fields', () => {
it('should correctly calculate total drivers label', () => {
const driversDTO: DriversLeaderboardDTO = {
drivers: [
{ id: 'driver-1', name: 'John Doe', rating: 1234.56, skillLevel: 'Pro', nationality: 'USA', racesCompleted: 150, wins: 25, podiums: 60, isActive: true, rank: 1 },
{ id: 'driver-2', name: 'Jane Smith', rating: 1100.75, skillLevel: 'Advanced', nationality: 'Canada', racesCompleted: 120, wins: 15, podiums: 45, isActive: true, rank: 2 },
{ id: 'driver-3', name: 'Bob Wilson', rating: 950.25, skillLevel: 'Intermediate', nationality: 'UK', racesCompleted: 80, wins: 5, podiums: 20, isActive: false, rank: 3 },
],
totalRaces: 350,
totalWins: 45,
activeCount: 2,
};
const result = DriversViewDataBuilder.build(driversDTO);
expect(result.totalDriversLabel).toBe('3');
});
it('should correctly calculate active count', () => {
const driversDTO: DriversLeaderboardDTO = {
drivers: [
{ id: 'driver-1', name: 'John Doe', rating: 1234.56, skillLevel: 'Pro', nationality: 'USA', racesCompleted: 150, wins: 25, podiums: 60, isActive: true, rank: 1 },
{ id: 'driver-2', name: 'Jane Smith', rating: 1100.75, skillLevel: 'Advanced', nationality: 'Canada', racesCompleted: 120, wins: 15, podiums: 45, isActive: true, rank: 2 },
{ id: 'driver-3', name: 'Bob Wilson', rating: 950.25, skillLevel: 'Intermediate', nationality: 'UK', racesCompleted: 80, wins: 5, podiums: 20, isActive: false, rank: 3 },
],
totalRaces: 350,
totalWins: 45,
activeCount: 2,
};
const result = DriversViewDataBuilder.build(driversDTO);
expect(result.activeCount).toBe(2);
expect(result.activeCountLabel).toBe('2');
});
});
describe('rating formatting', () => {
it('should format ratings with thousands separators', () => {
expect(RatingDisplay.format(1234.56)).toBe('1,235');
expect(RatingDisplay.format(9999.99)).toBe('10,000');
expect(RatingDisplay.format(100000.5)).toBe('100,001');
});
it('should handle null/undefined ratings', () => {
expect(RatingDisplay.format(null)).toBe('—');
expect(RatingDisplay.format(undefined)).toBe('—');
});
it('should round ratings correctly', () => {
expect(RatingDisplay.format(1234.4)).toBe('1,234');
expect(RatingDisplay.format(1234.6)).toBe('1,235');
expect(RatingDisplay.format(1234.5)).toBe('1,235');
});
});
describe('number formatting', () => {
it('should format numbers with thousands separators', () => {
expect(NumberDisplay.format(1234567)).toBe('1,234,567');
expect(NumberDisplay.format(1000)).toBe('1,000');
expect(NumberDisplay.format(999)).toBe('999');
});
it('should handle decimal numbers', () => {
expect(NumberDisplay.format(1234.567)).toBe('1,234.567');
expect(NumberDisplay.format(1000.5)).toBe('1,000.5');
});
});
});
describe('DriverProfileViewDataBuilder', () => {
describe('happy paths', () => {
@@ -1643,531 +1193,4 @@ describe('DriverProfileViewDataBuilder', () => {
expect(result.socialSummary.friends).toHaveLength(5);
});
});
describe('date formatting', () => {
it('should format dates correctly', () => {
expect(DateDisplay.formatShort('2024-01-15T00:00:00Z')).toBe('Jan 15, 2024');
expect(DateDisplay.formatMonthYear('2024-01-15T00:00:00Z')).toBe('Jan 2024');
expect(DateDisplay.formatShort('2024-12-25T00:00:00Z')).toBe('Dec 25, 2024');
expect(DateDisplay.formatMonthYear('2024-12-25T00:00:00Z')).toBe('Dec 2024');
});
});
describe('finish position formatting', () => {
it('should format finish positions correctly', () => {
expect(FinishDisplay.format(1)).toBe('P1');
expect(FinishDisplay.format(5)).toBe('P5');
expect(FinishDisplay.format(10)).toBe('P10');
expect(FinishDisplay.format(100)).toBe('P100');
});
it('should handle null/undefined finish positions', () => {
expect(FinishDisplay.format(null)).toBe('—');
expect(FinishDisplay.format(undefined)).toBe('—');
});
it('should format average finish positions correctly', () => {
expect(FinishDisplay.formatAverage(5.4)).toBe('P5.4');
expect(FinishDisplay.formatAverage(1.5)).toBe('P1.5');
expect(FinishDisplay.formatAverage(10.0)).toBe('P10.0');
});
it('should handle null/undefined average finish positions', () => {
expect(FinishDisplay.formatAverage(null)).toBe('—');
expect(FinishDisplay.formatAverage(undefined)).toBe('—');
});
});
describe('percentage formatting', () => {
it('should format percentages correctly', () => {
expect(PercentDisplay.format(0.1234)).toBe('12.3%');
expect(PercentDisplay.format(0.5)).toBe('50.0%');
expect(PercentDisplay.format(1.0)).toBe('100.0%');
});
it('should handle null/undefined percentages', () => {
expect(PercentDisplay.format(null)).toBe('0.0%');
expect(PercentDisplay.format(undefined)).toBe('0.0%');
});
it('should format whole percentages correctly', () => {
expect(PercentDisplay.formatWhole(85)).toBe('85%');
expect(PercentDisplay.formatWhole(50)).toBe('50%');
expect(PercentDisplay.formatWhole(100)).toBe('100%');
});
it('should handle null/undefined whole percentages', () => {
expect(PercentDisplay.formatWhole(null)).toBe('0%');
expect(PercentDisplay.formatWhole(undefined)).toBe('0%');
});
});
describe('cross-component consistency', () => {
it('should all use consistent formatting for numeric values', () => {
const profileDTO: GetDriverProfileOutputDTO = {
currentDriver: {
id: 'driver-123',
name: 'John Doe',
country: 'USA',
joinedAt: '2024-01-15T00:00:00Z',
rating: 1234.56,
globalRank: 42,
consistency: 85,
},
stats: {
totalRaces: 150,
wins: 25,
podiums: 60,
dnfs: 10,
avgFinish: 5.4,
bestFinish: 1,
worstFinish: 25,
finishRate: 0.933,
winRate: 0.167,
podiumRate: 0.4,
percentile: 95,
rating: 1234.56,
consistency: 85,
overallRank: 42,
},
finishDistribution: {
totalRaces: 150,
wins: 25,
podiums: 60,
topTen: 100,
dnfs: 10,
other: 55,
},
teamMemberships: [],
socialSummary: {
friendsCount: 0,
friends: [],
},
};
const result = DriverProfileViewDataBuilder.build(profileDTO);
// All numeric values should be formatted as strings
expect(typeof result.currentDriver?.ratingLabel).toBe('string');
expect(typeof result.currentDriver?.globalRankLabel).toBe('string');
expect(typeof result.stats?.totalRacesLabel).toBe('string');
expect(typeof result.stats?.winsLabel).toBe('string');
expect(typeof result.stats?.podiumsLabel).toBe('string');
expect(typeof result.stats?.dnfsLabel).toBe('string');
expect(typeof result.stats?.avgFinishLabel).toBe('string');
expect(typeof result.stats?.bestFinishLabel).toBe('string');
expect(typeof result.stats?.worstFinishLabel).toBe('string');
expect(typeof result.stats?.ratingLabel).toBe('string');
expect(typeof result.stats?.consistencyLabel).toBe('string');
});
it('should all handle missing data gracefully', () => {
const profileDTO: GetDriverProfileOutputDTO = {
currentDriver: {
id: 'driver-123',
name: 'John Doe',
country: 'USA',
joinedAt: '2024-01-15T00:00:00Z',
},
stats: {
totalRaces: 0,
wins: 0,
podiums: 0,
dnfs: 0,
},
finishDistribution: {
totalRaces: 0,
wins: 0,
podiums: 0,
topTen: 0,
dnfs: 0,
other: 0,
},
teamMemberships: [],
socialSummary: {
friendsCount: 0,
friends: [],
},
};
const result = DriverProfileViewDataBuilder.build(profileDTO);
// All fields should have safe defaults
expect(result.currentDriver?.avatarUrl).toBe('');
expect(result.currentDriver?.iracingId).toBeNull();
expect(result.currentDriver?.rating).toBeNull();
expect(result.currentDriver?.ratingLabel).toBe('—');
expect(result.currentDriver?.globalRank).toBeNull();
expect(result.currentDriver?.globalRankLabel).toBe('—');
expect(result.currentDriver?.consistency).toBeNull();
expect(result.currentDriver?.bio).toBeNull();
expect(result.currentDriver?.totalDrivers).toBeNull();
expect(result.stats?.avgFinish).toBeNull();
expect(result.stats?.avgFinishLabel).toBe('—');
expect(result.stats?.bestFinish).toBeNull();
expect(result.stats?.bestFinishLabel).toBe('—');
expect(result.stats?.worstFinish).toBeNull();
expect(result.stats?.worstFinishLabel).toBe('—');
expect(result.stats?.finishRate).toBeNull();
expect(result.stats?.winRate).toBeNull();
expect(result.stats?.podiumRate).toBeNull();
expect(result.stats?.percentile).toBeNull();
expect(result.stats?.rating).toBeNull();
expect(result.stats?.ratingLabel).toBe('—');
expect(result.stats?.consistency).toBeNull();
expect(result.stats?.consistencyLabel).toBe('0%');
expect(result.stats?.overallRank).toBeNull();
expect(result.finishDistribution).not.toBeNull();
expect(result.teamMemberships).toEqual([]);
expect(result.socialSummary.friends).toEqual([]);
expect(result.extendedProfile).toBeNull();
});
it('should all preserve ISO timestamps for serialization', () => {
const profileDTO: GetDriverProfileOutputDTO = {
currentDriver: {
id: 'driver-123',
name: 'John Doe',
country: 'USA',
joinedAt: '2024-01-15T00:00:00Z',
},
stats: {
totalRaces: 150,
wins: 25,
podiums: 60,
dnfs: 10,
},
finishDistribution: {
totalRaces: 150,
wins: 25,
podiums: 60,
topTen: 100,
dnfs: 10,
other: 55,
},
teamMemberships: [
{
teamId: 'team-1',
teamName: 'Elite Racing',
teamTag: 'ER',
role: 'Driver',
joinedAt: '2024-01-15T00:00:00Z',
isCurrent: true,
},
],
socialSummary: {
friendsCount: 0,
friends: [],
},
extendedProfile: {
socialHandles: [],
achievements: [
{
id: 'ach-1',
title: 'Champion',
description: 'Won the championship',
icon: 'trophy',
rarity: 'Legendary',
earnedAt: '2024-01-15T00:00:00Z',
},
],
racingStyle: 'Aggressive',
favoriteTrack: 'Spa',
favoriteCar: 'Porsche 911 GT3',
timezone: 'America/New_York',
availableHours: 'Evenings',
lookingForTeam: false,
openToRequests: true,
},
};
const result = DriverProfileViewDataBuilder.build(profileDTO);
// All timestamps should be preserved as ISO strings
expect(result.currentDriver?.joinedAt).toBe('2024-01-15T00:00:00Z');
expect(result.teamMemberships[0].joinedAt).toBe('2024-01-15T00:00:00Z');
expect(result.extendedProfile?.achievements[0].earnedAt).toBe('2024-01-15T00:00:00Z');
});
it('should all handle boolean flags correctly', () => {
const profileDTO: GetDriverProfileOutputDTO = {
currentDriver: {
id: 'driver-123',
name: 'John Doe',
country: 'USA',
joinedAt: '2024-01-15T00:00:00Z',
},
stats: {
totalRaces: 150,
wins: 25,
podiums: 60,
dnfs: 10,
},
finishDistribution: {
totalRaces: 150,
wins: 25,
podiums: 60,
topTen: 100,
dnfs: 10,
other: 55,
},
teamMemberships: [
{
teamId: 'team-1',
teamName: 'Elite Racing',
teamTag: 'ER',
role: 'Driver',
joinedAt: '2024-01-15T00:00:00Z',
isCurrent: true,
},
{
teamId: 'team-2',
teamName: 'Old Team',
teamTag: 'OT',
role: 'Driver',
joinedAt: '2023-01-15T00:00:00Z',
isCurrent: false,
},
],
socialSummary: {
friendsCount: 0,
friends: [],
},
extendedProfile: {
socialHandles: [],
achievements: [],
racingStyle: 'Aggressive',
favoriteTrack: 'Spa',
favoriteCar: 'Porsche 911 GT3',
timezone: 'America/New_York',
availableHours: 'Evenings',
lookingForTeam: true,
openToRequests: false,
},
};
const result = DriverProfileViewDataBuilder.build(profileDTO);
expect(result.teamMemberships[0].isCurrent).toBe(true);
expect(result.teamMemberships[1].isCurrent).toBe(false);
expect(result.extendedProfile?.lookingForTeam).toBe(true);
expect(result.extendedProfile?.openToRequests).toBe(false);
});
});
describe('data integrity', () => {
it('should maintain data consistency across transformations', () => {
const profileDTO: GetDriverProfileOutputDTO = {
currentDriver: {
id: 'driver-123',
name: 'John Doe',
country: 'USA',
avatarUrl: 'https://example.com/avatar.jpg',
iracingId: '12345',
joinedAt: '2024-01-15T00:00:00Z',
rating: 1234.56,
globalRank: 42,
consistency: 85,
bio: 'Professional sim racer.',
totalDrivers: 1000,
},
stats: {
totalRaces: 150,
wins: 25,
podiums: 60,
dnfs: 10,
avgFinish: 5.4,
bestFinish: 1,
worstFinish: 25,
finishRate: 0.933,
winRate: 0.167,
podiumRate: 0.4,
percentile: 95,
rating: 1234.56,
consistency: 85,
overallRank: 42,
},
finishDistribution: {
totalRaces: 150,
wins: 25,
podiums: 60,
topTen: 100,
dnfs: 10,
other: 55,
},
teamMemberships: [
{
teamId: 'team-1',
teamName: 'Elite Racing',
teamTag: 'ER',
role: 'Driver',
joinedAt: '2024-01-15T00:00:00Z',
isCurrent: true,
},
],
socialSummary: {
friendsCount: 2,
friends: [
{ id: 'friend-1', name: 'Alice', country: 'UK', avatarUrl: 'https://example.com/alice.jpg' },
{ id: 'friend-2', name: 'Bob', country: 'Germany' },
],
},
extendedProfile: {
socialHandles: [
{ platform: 'Twitter', handle: '@johndoe', url: 'https://twitter.com/johndoe' },
],
achievements: [
{ id: 'ach-1', title: 'Champion', description: 'Won the championship', icon: 'trophy', rarity: 'Legendary', earnedAt: '2024-01-15T00:00:00Z' },
],
racingStyle: 'Aggressive',
favoriteTrack: 'Spa',
favoriteCar: 'Porsche 911 GT3',
timezone: 'America/New_York',
availableHours: 'Evenings',
lookingForTeam: false,
openToRequests: true,
},
};
const result = DriverProfileViewDataBuilder.build(profileDTO);
// Verify derived fields match their source data
expect(result.socialSummary.friendsCount).toBe(profileDTO.socialSummary.friends.length);
expect(result.teamMemberships.length).toBe(profileDTO.teamMemberships.length);
expect(result.extendedProfile?.achievements.length).toBe(profileDTO.extendedProfile?.achievements.length);
});
it('should handle complex real-world scenarios', () => {
const profileDTO: GetDriverProfileOutputDTO = {
currentDriver: {
id: 'driver-123',
name: 'John Doe',
country: 'USA',
avatarUrl: 'https://example.com/avatar.jpg',
iracingId: '12345',
joinedAt: '2024-01-15T00:00:00Z',
rating: 2456.78,
globalRank: 15,
consistency: 92.5,
bio: 'Professional sim racer with 5 years of experience. Specializes in GT3 racing.',
totalDrivers: 1000,
},
stats: {
totalRaces: 250,
wins: 45,
podiums: 120,
dnfs: 15,
avgFinish: 4.2,
bestFinish: 1,
worstFinish: 30,
finishRate: 0.94,
winRate: 0.18,
podiumRate: 0.48,
percentile: 98,
rating: 2456.78,
consistency: 92.5,
overallRank: 15,
},
finishDistribution: {
totalRaces: 250,
wins: 45,
podiums: 120,
topTen: 180,
dnfs: 15,
other: 55,
},
teamMemberships: [
{
teamId: 'team-1',
teamName: 'Elite Racing',
teamTag: 'ER',
role: 'Driver',
joinedAt: '2024-01-15T00:00:00Z',
isCurrent: true,
},
{
teamId: 'team-2',
teamName: 'Pro Team',
teamTag: 'PT',
role: 'Reserve Driver',
joinedAt: '2023-06-15T00:00:00Z',
isCurrent: false,
},
],
socialSummary: {
friendsCount: 50,
friends: [
{ id: 'friend-1', name: 'Alice', country: 'UK', avatarUrl: 'https://example.com/alice.jpg' },
{ id: 'friend-2', name: 'Bob', country: 'Germany' },
{ id: 'friend-3', name: 'Charlie', country: 'France', avatarUrl: 'https://example.com/charlie.jpg' },
],
},
extendedProfile: {
socialHandles: [
{ platform: 'Twitter', handle: '@johndoe', url: 'https://twitter.com/johndoe' },
{ platform: 'Discord', handle: 'johndoe#1234', url: '' },
],
achievements: [
{ id: 'ach-1', title: 'Champion', description: 'Won the championship', icon: 'trophy', rarity: 'Legendary', earnedAt: '2024-01-15T00:00:00Z' },
{ id: 'ach-2', title: 'Podium Finisher', description: 'Finished on podium 100 times', icon: 'medal', rarity: 'Rare', earnedAt: '2023-12-01T00:00:00Z' },
],
racingStyle: 'Aggressive',
favoriteTrack: 'Spa',
favoriteCar: 'Porsche 911 GT3',
timezone: 'America/New_York',
availableHours: 'Evenings and Weekends',
lookingForTeam: false,
openToRequests: true,
},
};
const result = DriverProfileViewDataBuilder.build(profileDTO);
// Verify all transformations
expect(result.currentDriver?.name).toBe('John Doe');
expect(result.currentDriver?.ratingLabel).toBe('2,457');
expect(result.currentDriver?.globalRankLabel).toBe('#15');
expect(result.currentDriver?.consistency).toBe(92.5);
expect(result.currentDriver?.bio).toBe('Professional sim racer with 5 years of experience. Specializes in GT3 racing.');
expect(result.stats?.totalRacesLabel).toBe('250');
expect(result.stats?.winsLabel).toBe('45');
expect(result.stats?.podiumsLabel).toBe('120');
expect(result.stats?.dnfsLabel).toBe('15');
expect(result.stats?.avgFinishLabel).toBe('P4.2');
expect(result.stats?.bestFinishLabel).toBe('P1');
expect(result.stats?.worstFinishLabel).toBe('P30');
expect(result.stats?.finishRate).toBe(0.94);
expect(result.stats?.winRate).toBe(0.18);
expect(result.stats?.podiumRate).toBe(0.48);
expect(result.stats?.percentile).toBe(98);
expect(result.stats?.ratingLabel).toBe('2,457');
expect(result.stats?.consistencyLabel).toBe('92.5%');
expect(result.stats?.overallRank).toBe(15);
expect(result.finishDistribution?.totalRaces).toBe(250);
expect(result.finishDistribution?.wins).toBe(45);
expect(result.finishDistribution?.podiums).toBe(120);
expect(result.finishDistribution?.topTen).toBe(180);
expect(result.finishDistribution?.dnfs).toBe(15);
expect(result.finishDistribution?.other).toBe(55);
expect(result.teamMemberships).toHaveLength(2);
expect(result.teamMemberships[0].isCurrent).toBe(true);
expect(result.teamMemberships[1].isCurrent).toBe(false);
expect(result.socialSummary.friendsCount).toBe(50);
expect(result.socialSummary.friends).toHaveLength(3);
expect(result.socialSummary.friends[0].avatarUrl).toBe('https://example.com/alice.jpg');
expect(result.socialSummary.friends[1].avatarUrl).toBe('');
expect(result.socialSummary.friends[2].avatarUrl).toBe('https://example.com/charlie.jpg');
expect(result.extendedProfile?.socialHandles).toHaveLength(2);
expect(result.extendedProfile?.achievements).toHaveLength(2);
expect(result.extendedProfile?.achievements[0].rarityLabel).toBe('Legendary');
expect(result.extendedProfile?.achievements[1].rarityLabel).toBe('Rare');
expect(result.extendedProfile?.lookingForTeam).toBe(false);
expect(result.extendedProfile?.openToRequests).toBe(true);
});
});
});

View File

@@ -0,0 +1,441 @@
import { describe, it, expect } from 'vitest';
import { DriverRankingsViewDataBuilder } from './DriverRankingsViewDataBuilder';
import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO';
describe('DriverRankingsViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform DriverLeaderboardItemDTO array to DriverRankingsViewData correctly', () => {
const driverDTOs: DriverLeaderboardItemDTO[] = [
{
id: 'driver-1',
name: 'John Doe',
rating: 1234.56,
skillLevel: 'pro',
nationality: 'USA',
racesCompleted: 150,
wins: 25,
podiums: 60,
isActive: true,
rank: 1,
avatarUrl: 'https://example.com/avatar1.jpg',
},
{
id: 'driver-2',
name: 'Jane Smith',
rating: 1100.0,
skillLevel: 'advanced',
nationality: 'Canada',
racesCompleted: 100,
wins: 15,
podiums: 40,
isActive: true,
rank: 2,
avatarUrl: 'https://example.com/avatar2.jpg',
},
{
id: 'driver-3',
name: 'Bob Johnson',
rating: 950.0,
skillLevel: 'intermediate',
nationality: 'UK',
racesCompleted: 80,
wins: 10,
podiums: 30,
isActive: true,
rank: 3,
avatarUrl: 'https://example.com/avatar3.jpg',
},
];
const result = DriverRankingsViewDataBuilder.build(driverDTOs);
// Verify drivers
expect(result.drivers).toHaveLength(3);
expect(result.drivers[0].id).toBe('driver-1');
expect(result.drivers[0].name).toBe('John Doe');
expect(result.drivers[0].rating).toBe(1234.56);
expect(result.drivers[0].skillLevel).toBe('pro');
expect(result.drivers[0].nationality).toBe('USA');
expect(result.drivers[0].racesCompleted).toBe(150);
expect(result.drivers[0].wins).toBe(25);
expect(result.drivers[0].podiums).toBe(60);
expect(result.drivers[0].rank).toBe(1);
expect(result.drivers[0].avatarUrl).toBe('https://example.com/avatar1.jpg');
expect(result.drivers[0].winRate).toBe('16.7');
expect(result.drivers[0].medalBg).toBe('bg-warning-amber');
expect(result.drivers[0].medalColor).toBe('text-warning-amber');
// Verify podium (top 3 with special ordering: 2nd, 1st, 3rd)
expect(result.podium).toHaveLength(3);
expect(result.podium[0].id).toBe('driver-1');
expect(result.podium[0].name).toBe('John Doe');
expect(result.podium[0].rating).toBe(1234.56);
expect(result.podium[0].wins).toBe(25);
expect(result.podium[0].podiums).toBe(60);
expect(result.podium[0].avatarUrl).toBe('https://example.com/avatar1.jpg');
expect(result.podium[0].position).toBe(2); // 2nd place
expect(result.podium[1].id).toBe('driver-2');
expect(result.podium[1].position).toBe(1); // 1st place
expect(result.podium[2].id).toBe('driver-3');
expect(result.podium[2].position).toBe(3); // 3rd place
// Verify default values
expect(result.searchQuery).toBe('');
expect(result.selectedSkill).toBe('all');
expect(result.sortBy).toBe('rank');
expect(result.showFilters).toBe(false);
});
it('should handle empty driver array', () => {
const driverDTOs: DriverLeaderboardItemDTO[] = [];
const result = DriverRankingsViewDataBuilder.build(driverDTOs);
expect(result.drivers).toEqual([]);
expect(result.podium).toEqual([]);
expect(result.searchQuery).toBe('');
expect(result.selectedSkill).toBe('all');
expect(result.sortBy).toBe('rank');
expect(result.showFilters).toBe(false);
});
it('should handle less than 3 drivers for podium', () => {
const driverDTOs: DriverLeaderboardItemDTO[] = [
{
id: 'driver-1',
name: 'John Doe',
rating: 1234.56,
skillLevel: 'pro',
nationality: 'USA',
racesCompleted: 150,
wins: 25,
podiums: 60,
isActive: true,
rank: 1,
avatarUrl: 'https://example.com/avatar1.jpg',
},
{
id: 'driver-2',
name: 'Jane Smith',
rating: 1100.0,
skillLevel: 'advanced',
nationality: 'Canada',
racesCompleted: 100,
wins: 15,
podiums: 40,
isActive: true,
rank: 2,
avatarUrl: 'https://example.com/avatar2.jpg',
},
];
const result = DriverRankingsViewDataBuilder.build(driverDTOs);
expect(result.drivers).toHaveLength(2);
expect(result.podium).toHaveLength(2);
expect(result.podium[0].position).toBe(2); // 2nd place
expect(result.podium[1].position).toBe(1); // 1st place
});
it('should handle missing avatar URLs with empty string fallback', () => {
const driverDTOs: DriverLeaderboardItemDTO[] = [
{
id: 'driver-1',
name: 'John Doe',
rating: 1234.56,
skillLevel: 'pro',
nationality: 'USA',
racesCompleted: 150,
wins: 25,
podiums: 60,
isActive: true,
rank: 1,
},
];
const result = DriverRankingsViewDataBuilder.build(driverDTOs);
expect(result.drivers[0].avatarUrl).toBe('');
expect(result.podium[0].avatarUrl).toBe('');
});
it('should calculate win rate correctly', () => {
const driverDTOs: DriverLeaderboardItemDTO[] = [
{
id: 'driver-1',
name: 'John Doe',
rating: 1234.56,
skillLevel: 'pro',
nationality: 'USA',
racesCompleted: 100,
wins: 25,
podiums: 60,
isActive: true,
rank: 1,
},
{
id: 'driver-2',
name: 'Jane Smith',
rating: 1100.0,
skillLevel: 'advanced',
nationality: 'Canada',
racesCompleted: 50,
wins: 10,
podiums: 25,
isActive: true,
rank: 2,
},
{
id: 'driver-3',
name: 'Bob Johnson',
rating: 950.0,
skillLevel: 'intermediate',
nationality: 'UK',
racesCompleted: 0,
wins: 0,
podiums: 0,
isActive: true,
rank: 3,
},
];
const result = DriverRankingsViewDataBuilder.build(driverDTOs);
expect(result.drivers[0].winRate).toBe('25.0');
expect(result.drivers[1].winRate).toBe('20.0');
expect(result.drivers[2].winRate).toBe('0.0');
});
it('should assign correct medal colors based on position', () => {
const driverDTOs: DriverLeaderboardItemDTO[] = [
{
id: 'driver-1',
name: 'John Doe',
rating: 1234.56,
skillLevel: 'pro',
nationality: 'USA',
racesCompleted: 150,
wins: 25,
podiums: 60,
isActive: true,
rank: 1,
},
{
id: 'driver-2',
name: 'Jane Smith',
rating: 1100.0,
skillLevel: 'advanced',
nationality: 'Canada',
racesCompleted: 100,
wins: 15,
podiums: 40,
isActive: true,
rank: 2,
},
{
id: 'driver-3',
name: 'Bob Johnson',
rating: 950.0,
skillLevel: 'intermediate',
nationality: 'UK',
racesCompleted: 80,
wins: 10,
podiums: 30,
isActive: true,
rank: 3,
},
{
id: 'driver-4',
name: 'Alice Brown',
rating: 800.0,
skillLevel: 'beginner',
nationality: 'Germany',
racesCompleted: 60,
wins: 5,
podiums: 15,
isActive: true,
rank: 4,
},
];
const result = DriverRankingsViewDataBuilder.build(driverDTOs);
expect(result.drivers[0].medalBg).toBe('bg-warning-amber');
expect(result.drivers[0].medalColor).toBe('text-warning-amber');
expect(result.drivers[1].medalBg).toBe('bg-gray-300');
expect(result.drivers[1].medalColor).toBe('text-gray-300');
expect(result.drivers[2].medalBg).toBe('bg-orange-700');
expect(result.drivers[2].medalColor).toBe('text-orange-700');
expect(result.drivers[3].medalBg).toBe('bg-gray-800');
expect(result.drivers[3].medalColor).toBe('text-gray-400');
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const driverDTOs: DriverLeaderboardItemDTO[] = [
{
id: 'driver-123',
name: 'John Doe',
rating: 1234.56,
skillLevel: 'pro',
nationality: 'USA',
racesCompleted: 150,
wins: 25,
podiums: 60,
isActive: true,
rank: 1,
avatarUrl: 'https://example.com/avatar.jpg',
},
];
const result = DriverRankingsViewDataBuilder.build(driverDTOs);
expect(result.drivers[0].name).toBe(driverDTOs[0].name);
expect(result.drivers[0].nationality).toBe(driverDTOs[0].nationality);
expect(result.drivers[0].avatarUrl).toBe(driverDTOs[0].avatarUrl);
expect(result.drivers[0].skillLevel).toBe(driverDTOs[0].skillLevel);
});
it('should not modify the input DTO', () => {
const driverDTOs: DriverLeaderboardItemDTO[] = [
{
id: 'driver-123',
name: 'John Doe',
rating: 1234.56,
skillLevel: 'pro',
nationality: 'USA',
racesCompleted: 150,
wins: 25,
podiums: 60,
isActive: true,
rank: 1,
avatarUrl: 'https://example.com/avatar.jpg',
},
];
const originalDTO = JSON.parse(JSON.stringify(driverDTOs));
DriverRankingsViewDataBuilder.build(driverDTOs);
expect(driverDTOs).toEqual(originalDTO);
});
it('should handle large numbers correctly', () => {
const driverDTOs: DriverLeaderboardItemDTO[] = [
{
id: 'driver-1',
name: 'John Doe',
rating: 999999.99,
skillLevel: 'pro',
nationality: 'USA',
racesCompleted: 10000,
wins: 2500,
podiums: 5000,
isActive: true,
rank: 1,
},
];
const result = DriverRankingsViewDataBuilder.build(driverDTOs);
expect(result.drivers[0].rating).toBe(999999.99);
expect(result.drivers[0].wins).toBe(2500);
expect(result.drivers[0].podiums).toBe(5000);
expect(result.drivers[0].racesCompleted).toBe(10000);
expect(result.drivers[0].winRate).toBe('25.0');
});
});
describe('edge cases', () => {
it('should handle null/undefined avatar URLs', () => {
const driverDTOs: DriverLeaderboardItemDTO[] = [
{
id: 'driver-1',
name: 'John Doe',
rating: 1234.56,
skillLevel: 'pro',
nationality: 'USA',
racesCompleted: 150,
wins: 25,
podiums: 60,
isActive: true,
rank: 1,
avatarUrl: null as any,
},
];
const result = DriverRankingsViewDataBuilder.build(driverDTOs);
expect(result.drivers[0].avatarUrl).toBe('');
expect(result.podium[0].avatarUrl).toBe('');
});
it('should handle null/undefined rating', () => {
const driverDTOs: DriverLeaderboardItemDTO[] = [
{
id: 'driver-1',
name: 'John Doe',
rating: null as any,
skillLevel: 'pro',
nationality: 'USA',
racesCompleted: 150,
wins: 25,
podiums: 60,
isActive: true,
rank: 1,
},
];
const result = DriverRankingsViewDataBuilder.build(driverDTOs);
expect(result.drivers[0].rating).toBeNull();
expect(result.podium[0].rating).toBeNull();
});
it('should handle zero races completed for win rate calculation', () => {
const driverDTOs: DriverLeaderboardItemDTO[] = [
{
id: 'driver-1',
name: 'John Doe',
rating: 1234.56,
skillLevel: 'pro',
nationality: 'USA',
racesCompleted: 0,
wins: 0,
podiums: 0,
isActive: true,
rank: 1,
},
];
const result = DriverRankingsViewDataBuilder.build(driverDTOs);
expect(result.drivers[0].winRate).toBe('0.0');
});
it('should handle rank 0', () => {
const driverDTOs: DriverLeaderboardItemDTO[] = [
{
id: 'driver-1',
name: 'John Doe',
rating: 1234.56,
skillLevel: 'pro',
nationality: 'USA',
racesCompleted: 150,
wins: 25,
podiums: 60,
isActive: true,
rank: 0,
},
];
const result = DriverRankingsViewDataBuilder.build(driverDTOs);
expect(result.drivers[0].rank).toBe(0);
expect(result.drivers[0].medalBg).toBe('bg-gray-800');
expect(result.drivers[0].medalColor).toBe('text-gray-400');
});
});
});

View File

@@ -0,0 +1,382 @@
import { describe, it, expect } from 'vitest';
import { DriversViewDataBuilder } from './DriversViewDataBuilder';
import type { DriversLeaderboardDTO } from '@/lib/types/generated/DriversLeaderboardDTO';
describe('DriversViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform DriversLeaderboardDTO to DriversViewData correctly', () => {
const driversDTO: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'John Doe',
rating: 1234.56,
skillLevel: 'Pro',
category: 'Elite',
nationality: 'USA',
racesCompleted: 150,
wins: 25,
podiums: 60,
isActive: true,
rank: 1,
avatarUrl: 'https://example.com/john.jpg',
},
{
id: 'driver-2',
name: 'Jane Smith',
rating: 1100.75,
skillLevel: 'Advanced',
category: 'Pro',
nationality: 'Canada',
racesCompleted: 120,
wins: 15,
podiums: 45,
isActive: true,
rank: 2,
avatarUrl: 'https://example.com/jane.jpg',
},
],
totalRaces: 270,
totalWins: 40,
activeCount: 2,
};
const result = DriversViewDataBuilder.build(driversDTO);
expect(result.drivers).toHaveLength(2);
expect(result.drivers[0].id).toBe('driver-1');
expect(result.drivers[0].name).toBe('John Doe');
expect(result.drivers[0].rating).toBe(1234.56);
expect(result.drivers[0].ratingLabel).toBe('1,235');
expect(result.drivers[0].skillLevel).toBe('Pro');
expect(result.drivers[0].category).toBe('Elite');
expect(result.drivers[0].nationality).toBe('USA');
expect(result.drivers[0].racesCompleted).toBe(150);
expect(result.drivers[0].wins).toBe(25);
expect(result.drivers[0].podiums).toBe(60);
expect(result.drivers[0].isActive).toBe(true);
expect(result.drivers[0].rank).toBe(1);
expect(result.drivers[0].avatarUrl).toBe('https://example.com/john.jpg');
expect(result.drivers[1].id).toBe('driver-2');
expect(result.drivers[1].name).toBe('Jane Smith');
expect(result.drivers[1].rating).toBe(1100.75);
expect(result.drivers[1].ratingLabel).toBe('1,101');
expect(result.drivers[1].skillLevel).toBe('Advanced');
expect(result.drivers[1].category).toBe('Pro');
expect(result.drivers[1].nationality).toBe('Canada');
expect(result.drivers[1].racesCompleted).toBe(120);
expect(result.drivers[1].wins).toBe(15);
expect(result.drivers[1].podiums).toBe(45);
expect(result.drivers[1].isActive).toBe(true);
expect(result.drivers[1].rank).toBe(2);
expect(result.drivers[1].avatarUrl).toBe('https://example.com/jane.jpg');
expect(result.totalRaces).toBe(270);
expect(result.totalRacesLabel).toBe('270');
expect(result.totalWins).toBe(40);
expect(result.totalWinsLabel).toBe('40');
expect(result.activeCount).toBe(2);
expect(result.activeCountLabel).toBe('2');
expect(result.totalDriversLabel).toBe('2');
});
it('should handle drivers with missing optional fields', () => {
const driversDTO: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'John Doe',
rating: 1234.56,
skillLevel: 'Pro',
nationality: 'USA',
racesCompleted: 150,
wins: 25,
podiums: 60,
isActive: true,
rank: 1,
},
],
totalRaces: 150,
totalWins: 25,
activeCount: 1,
};
const result = DriversViewDataBuilder.build(driversDTO);
expect(result.drivers[0].category).toBeUndefined();
expect(result.drivers[0].avatarUrl).toBeUndefined();
});
it('should handle empty drivers array', () => {
const driversDTO: DriversLeaderboardDTO = {
drivers: [],
totalRaces: 0,
totalWins: 0,
activeCount: 0,
};
const result = DriversViewDataBuilder.build(driversDTO);
expect(result.drivers).toEqual([]);
expect(result.totalRaces).toBe(0);
expect(result.totalRacesLabel).toBe('0');
expect(result.totalWins).toBe(0);
expect(result.totalWinsLabel).toBe('0');
expect(result.activeCount).toBe(0);
expect(result.activeCountLabel).toBe('0');
expect(result.totalDriversLabel).toBe('0');
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const driversDTO: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'John Doe',
rating: 1234.56,
skillLevel: 'Pro',
category: 'Elite',
nationality: 'USA',
racesCompleted: 150,
wins: 25,
podiums: 60,
isActive: true,
rank: 1,
avatarUrl: 'https://example.com/john.jpg',
},
],
totalRaces: 150,
totalWins: 25,
activeCount: 1,
};
const result = DriversViewDataBuilder.build(driversDTO);
expect(result.drivers[0].name).toBe(driversDTO.drivers[0].name);
expect(result.drivers[0].nationality).toBe(driversDTO.drivers[0].nationality);
expect(result.drivers[0].skillLevel).toBe(driversDTO.drivers[0].skillLevel);
expect(result.totalRaces).toBe(driversDTO.totalRaces);
expect(result.totalWins).toBe(driversDTO.totalWins);
expect(result.activeCount).toBe(driversDTO.activeCount);
});
it('should not modify the input DTO', () => {
const driversDTO: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'John Doe',
rating: 1234.56,
skillLevel: 'Pro',
category: 'Elite',
nationality: 'USA',
racesCompleted: 150,
wins: 25,
podiums: 60,
isActive: true,
rank: 1,
avatarUrl: 'https://example.com/john.jpg',
},
],
totalRaces: 150,
totalWins: 25,
activeCount: 1,
};
const originalDTO = JSON.parse(JSON.stringify(driversDTO));
DriversViewDataBuilder.build(driversDTO);
expect(driversDTO).toEqual(originalDTO);
});
it('should transform all numeric fields to formatted strings where appropriate', () => {
const driversDTO: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'John Doe',
rating: 1234.56,
skillLevel: 'Pro',
nationality: 'USA',
racesCompleted: 150,
wins: 25,
podiums: 60,
isActive: true,
rank: 1,
},
],
totalRaces: 150,
totalWins: 25,
activeCount: 1,
};
const result = DriversViewDataBuilder.build(driversDTO);
// Rating label should be a formatted string
expect(typeof result.drivers[0].ratingLabel).toBe('string');
expect(result.drivers[0].ratingLabel).toBe('1,235');
// Total counts should be formatted strings
expect(typeof result.totalRacesLabel).toBe('string');
expect(result.totalRacesLabel).toBe('150');
expect(typeof result.totalWinsLabel).toBe('string');
expect(result.totalWinsLabel).toBe('25');
expect(typeof result.activeCountLabel).toBe('string');
expect(result.activeCountLabel).toBe('1');
expect(typeof result.totalDriversLabel).toBe('string');
expect(result.totalDriversLabel).toBe('1');
});
it('should handle large numbers correctly', () => {
const driversDTO: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'John Doe',
rating: 999999.99,
skillLevel: 'Pro',
nationality: 'USA',
racesCompleted: 10000,
wins: 2500,
podiums: 5000,
isActive: true,
rank: 1,
},
],
totalRaces: 10000,
totalWins: 2500,
activeCount: 1,
};
const result = DriversViewDataBuilder.build(driversDTO);
expect(result.drivers[0].ratingLabel).toBe('1,000,000');
expect(result.totalRacesLabel).toBe('10,000');
expect(result.totalWinsLabel).toBe('2,500');
expect(result.activeCountLabel).toBe('1');
expect(result.totalDriversLabel).toBe('1');
});
});
describe('edge cases', () => {
it('should handle null/undefined rating', () => {
const driversDTO: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'John Doe',
rating: 0,
skillLevel: 'Pro',
nationality: 'USA',
racesCompleted: 150,
wins: 25,
podiums: 60,
isActive: true,
rank: 1,
},
],
totalRaces: 150,
totalWins: 25,
activeCount: 1,
};
const result = DriversViewDataBuilder.build(driversDTO);
expect(result.drivers[0].ratingLabel).toBe('0');
});
it('should handle drivers with no category', () => {
const driversDTO: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'John Doe',
rating: 1234.56,
skillLevel: 'Pro',
nationality: 'USA',
racesCompleted: 150,
wins: 25,
podiums: 60,
isActive: true,
rank: 1,
},
],
totalRaces: 150,
totalWins: 25,
activeCount: 1,
};
const result = DriversViewDataBuilder.build(driversDTO);
expect(result.drivers[0].category).toBeUndefined();
});
it('should handle inactive drivers', () => {
const driversDTO: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'John Doe',
rating: 1234.56,
skillLevel: 'Pro',
nationality: 'USA',
racesCompleted: 150,
wins: 25,
podiums: 60,
isActive: false,
rank: 1,
},
],
totalRaces: 150,
totalWins: 25,
activeCount: 0,
};
const result = DriversViewDataBuilder.build(driversDTO);
expect(result.drivers[0].isActive).toBe(false);
expect(result.activeCount).toBe(0);
expect(result.activeCountLabel).toBe('0');
});
});
describe('derived fields', () => {
it('should correctly calculate total drivers label', () => {
const driversDTO: DriversLeaderboardDTO = {
drivers: [
{ id: 'driver-1', name: 'John Doe', rating: 1234.56, skillLevel: 'Pro', nationality: 'USA', racesCompleted: 150, wins: 25, podiums: 60, isActive: true, rank: 1 },
{ id: 'driver-2', name: 'Jane Smith', rating: 1100.75, skillLevel: 'Advanced', nationality: 'Canada', racesCompleted: 120, wins: 15, podiums: 45, isActive: true, rank: 2 },
{ id: 'driver-3', name: 'Bob Wilson', rating: 950.25, skillLevel: 'Intermediate', nationality: 'UK', racesCompleted: 80, wins: 5, podiums: 20, isActive: false, rank: 3 },
],
totalRaces: 350,
totalWins: 45,
activeCount: 2,
};
const result = DriversViewDataBuilder.build(driversDTO);
expect(result.totalDriversLabel).toBe('3');
});
it('should correctly calculate active count', () => {
const driversDTO: DriversLeaderboardDTO = {
drivers: [
{ id: 'driver-1', name: 'John Doe', rating: 1234.56, skillLevel: 'Pro', nationality: 'USA', racesCompleted: 150, wins: 25, podiums: 60, isActive: true, rank: 1 },
{ id: 'driver-2', name: 'Jane Smith', rating: 1100.75, skillLevel: 'Advanced', nationality: 'Canada', racesCompleted: 120, wins: 15, podiums: 45, isActive: true, rank: 2 },
{ id: 'driver-3', name: 'Bob Wilson', rating: 950.25, skillLevel: 'Intermediate', nationality: 'UK', racesCompleted: 80, wins: 5, podiums: 20, isActive: false, rank: 3 },
],
totalRaces: 350,
totalWins: 45,
activeCount: 2,
};
const result = DriversViewDataBuilder.build(driversDTO);
expect(result.activeCount).toBe(2);
expect(result.activeCountLabel).toBe('2');
});
});
});

View File

@@ -0,0 +1,160 @@
import { describe, it, expect } from 'vitest';
import { ForgotPasswordViewDataBuilder } from './ForgotPasswordViewDataBuilder';
import type { ForgotPasswordPageDTO } from '@/lib/services/auth/types/ForgotPasswordPageDTO';
describe('ForgotPasswordViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform ForgotPasswordPageDTO to ForgotPasswordViewData correctly', () => {
const forgotPasswordPageDTO: ForgotPasswordPageDTO = {
returnTo: '/login',
};
const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO);
expect(result).toEqual({
returnTo: '/login',
showSuccess: false,
formState: {
fields: {
email: { value: '', error: undefined, touched: false, validating: false },
},
isValid: true,
isSubmitting: false,
submitError: undefined,
submitCount: 0,
},
isSubmitting: false,
submitError: undefined,
});
});
it('should handle empty returnTo path', () => {
const forgotPasswordPageDTO: ForgotPasswordPageDTO = {
returnTo: '',
};
const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO);
expect(result.returnTo).toBe('');
});
it('should handle returnTo with query parameters', () => {
const forgotPasswordPageDTO: ForgotPasswordPageDTO = {
returnTo: '/login?error=expired',
};
const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO);
expect(result.returnTo).toBe('/login?error=expired');
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const forgotPasswordPageDTO: ForgotPasswordPageDTO = {
returnTo: '/login',
};
const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO);
expect(result.returnTo).toBe(forgotPasswordPageDTO.returnTo);
});
it('should not modify the input DTO', () => {
const forgotPasswordPageDTO: ForgotPasswordPageDTO = {
returnTo: '/login',
};
const originalDTO = { ...forgotPasswordPageDTO };
ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO);
expect(forgotPasswordPageDTO).toEqual(originalDTO);
});
it('should initialize form field with default values', () => {
const forgotPasswordPageDTO: ForgotPasswordPageDTO = {
returnTo: '/login',
};
const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO);
expect(result.formState.fields.email.value).toBe('');
expect(result.formState.fields.email.error).toBeUndefined();
expect(result.formState.fields.email.touched).toBe(false);
expect(result.formState.fields.email.validating).toBe(false);
});
it('should initialize form state with default values', () => {
const forgotPasswordPageDTO: ForgotPasswordPageDTO = {
returnTo: '/login',
};
const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO);
expect(result.formState.isValid).toBe(true);
expect(result.formState.isSubmitting).toBe(false);
expect(result.formState.submitError).toBeUndefined();
expect(result.formState.submitCount).toBe(0);
});
it('should initialize UI state flags correctly', () => {
const forgotPasswordPageDTO: ForgotPasswordPageDTO = {
returnTo: '/login',
};
const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO);
expect(result.showSuccess).toBe(false);
expect(result.isSubmitting).toBe(false);
expect(result.submitError).toBeUndefined();
});
});
describe('edge cases', () => {
it('should handle returnTo with encoded characters', () => {
const forgotPasswordPageDTO: ForgotPasswordPageDTO = {
returnTo: '/login?redirect=%2Fdashboard',
};
const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO);
expect(result.returnTo).toBe('/login?redirect=%2Fdashboard');
});
it('should handle returnTo with hash fragment', () => {
const forgotPasswordPageDTO: ForgotPasswordPageDTO = {
returnTo: '/login#section',
};
const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO);
expect(result.returnTo).toBe('/login#section');
});
});
describe('form state structure', () => {
it('should have email field', () => {
const forgotPasswordPageDTO: ForgotPasswordPageDTO = {
returnTo: '/login',
};
const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO);
expect(result.formState.fields).toHaveProperty('email');
});
it('should have consistent field state structure', () => {
const forgotPasswordPageDTO: ForgotPasswordPageDTO = {
returnTo: '/login',
};
const result = ForgotPasswordViewDataBuilder.build(forgotPasswordPageDTO);
const field = result.formState.fields.email;
expect(field).toHaveProperty('value');
expect(field).toHaveProperty('error');
expect(field).toHaveProperty('touched');
expect(field).toHaveProperty('validating');
});
});
});

View File

@@ -0,0 +1,200 @@
import { describe, it, expect } from 'vitest';
import { GenerateAvatarsViewDataBuilder } from './GenerateAvatarsViewDataBuilder';
import type { RequestAvatarGenerationOutputDTO } from '@/lib/types/generated/RequestAvatarGenerationOutputDTO';
describe('GenerateAvatarsViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform RequestAvatarGenerationOutputDTO to GenerateAvatarsViewData correctly', () => {
const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = {
success: true,
avatarUrls: ['avatar-url-1', 'avatar-url-2', 'avatar-url-3'],
errorMessage: null,
};
const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto);
expect(result).toEqual({
success: true,
avatarUrls: ['avatar-url-1', 'avatar-url-2', 'avatar-url-3'],
errorMessage: null,
});
});
it('should handle empty avatar URLs', () => {
const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = {
success: true,
avatarUrls: [],
errorMessage: null,
};
const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto);
expect(result.avatarUrls).toHaveLength(0);
});
it('should handle single avatar URL', () => {
const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = {
success: true,
avatarUrls: ['avatar-url-1'],
errorMessage: null,
};
const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto);
expect(result.avatarUrls).toHaveLength(1);
expect(result.avatarUrls[0]).toBe('avatar-url-1');
});
it('should handle multiple avatar URLs', () => {
const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = {
success: true,
avatarUrls: ['avatar-url-1', 'avatar-url-2', 'avatar-url-3', 'avatar-url-4', 'avatar-url-5'],
errorMessage: null,
};
const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto);
expect(result.avatarUrls).toHaveLength(5);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = {
success: true,
avatarUrls: ['avatar-url-1', 'avatar-url-2'],
errorMessage: null,
};
const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto);
expect(result.success).toBe(requestAvatarGenerationOutputDto.success);
expect(result.avatarUrls).toEqual(requestAvatarGenerationOutputDto.avatarUrls);
expect(result.errorMessage).toBe(requestAvatarGenerationOutputDto.errorMessage);
});
it('should not modify the input DTO', () => {
const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = {
success: true,
avatarUrls: ['avatar-url-1'],
errorMessage: null,
};
const originalDto = { ...requestAvatarGenerationOutputDto };
GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto);
expect(requestAvatarGenerationOutputDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle success false', () => {
const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = {
success: false,
avatarUrls: [],
errorMessage: 'Generation failed',
};
const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto);
expect(result.success).toBe(false);
});
it('should handle error message', () => {
const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = {
success: false,
avatarUrls: [],
errorMessage: 'Invalid input data',
};
const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto);
expect(result.errorMessage).toBe('Invalid input data');
});
it('should handle null error message', () => {
const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = {
success: true,
avatarUrls: ['avatar-url-1'],
errorMessage: null,
};
const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto);
expect(result.errorMessage).toBeNull();
});
it('should handle undefined avatarUrls', () => {
const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = {
success: true,
avatarUrls: undefined,
errorMessage: null,
};
const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto);
expect(result.avatarUrls).toEqual([]);
});
it('should handle empty string avatar URLs', () => {
const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = {
success: true,
avatarUrls: ['', 'avatar-url-1', ''],
errorMessage: null,
};
const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto);
expect(result.avatarUrls).toEqual(['', 'avatar-url-1', '']);
});
it('should handle special characters in avatar URLs', () => {
const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = {
success: true,
avatarUrls: ['avatar-url-1?param=value', 'avatar-url-2#anchor', 'avatar-url-3?query=1&test=2'],
errorMessage: null,
};
const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto);
expect(result.avatarUrls).toEqual([
'avatar-url-1?param=value',
'avatar-url-2#anchor',
'avatar-url-3?query=1&test=2',
]);
});
it('should handle very long avatar URLs', () => {
const longUrl = 'https://example.com/avatars/' + 'a'.repeat(1000) + '.png';
const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = {
success: true,
avatarUrls: [longUrl],
errorMessage: null,
};
const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto);
expect(result.avatarUrls[0]).toBe(longUrl);
});
it('should handle avatar URLs with special characters', () => {
const requestAvatarGenerationOutputDto: RequestAvatarGenerationOutputDTO = {
success: true,
avatarUrls: [
'avatar-url-1?name=John%20Doe',
'avatar-url-2?email=test@example.com',
'avatar-url-3?query=hello%20world',
],
errorMessage: null,
};
const result = GenerateAvatarsViewDataBuilder.build(requestAvatarGenerationOutputDto);
expect(result.avatarUrls).toEqual([
'avatar-url-1?name=John%20Doe',
'avatar-url-2?email=test@example.com',
'avatar-url-3?query=hello%20world',
]);
});
});
});

View File

@@ -0,0 +1,553 @@
import { describe, it, expect } from 'vitest';
import { HealthViewDataBuilder, HealthDTO } from './HealthViewDataBuilder';
describe('HealthViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform HealthDTO to HealthViewData correctly', () => {
const healthDTO: HealthDTO = {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: 99.95,
responseTime: 150,
errorRate: 0.05,
lastCheck: new Date().toISOString(),
checksPassed: 995,
checksFailed: 5,
components: [
{
name: 'Database',
status: 'ok',
lastCheck: new Date().toISOString(),
responseTime: 50,
errorRate: 0.01,
},
{
name: 'API',
status: 'ok',
lastCheck: new Date().toISOString(),
responseTime: 100,
errorRate: 0.02,
},
],
alerts: [
{
id: 'alert-1',
type: 'info',
title: 'System Update',
message: 'System updated successfully',
timestamp: new Date().toISOString(),
},
],
};
const result = HealthViewDataBuilder.build(healthDTO);
expect(result.overallStatus.status).toBe('ok');
expect(result.overallStatus.statusLabel).toBe('Healthy');
expect(result.overallStatus.statusColor).toBe('#10b981');
expect(result.overallStatus.statusIcon).toBe('✓');
expect(result.metrics.uptime).toBe('99.95%');
expect(result.metrics.responseTime).toBe('150ms');
expect(result.metrics.errorRate).toBe('0.05%');
expect(result.metrics.checksPassed).toBe(995);
expect(result.metrics.checksFailed).toBe(5);
expect(result.metrics.totalChecks).toBe(1000);
expect(result.metrics.successRate).toBe('99.5%');
expect(result.components).toHaveLength(2);
expect(result.components[0].name).toBe('Database');
expect(result.components[0].status).toBe('ok');
expect(result.components[0].statusLabel).toBe('Healthy');
expect(result.alerts).toHaveLength(1);
expect(result.alerts[0].id).toBe('alert-1');
expect(result.alerts[0].type).toBe('info');
expect(result.hasAlerts).toBe(true);
expect(result.hasDegradedComponents).toBe(false);
expect(result.hasErrorComponents).toBe(false);
});
it('should handle missing optional fields gracefully', () => {
const healthDTO: HealthDTO = {
status: 'ok',
timestamp: new Date().toISOString(),
};
const result = HealthViewDataBuilder.build(healthDTO);
expect(result.overallStatus.status).toBe('ok');
expect(result.metrics.uptime).toBe('N/A');
expect(result.metrics.responseTime).toBe('N/A');
expect(result.metrics.errorRate).toBe('N/A');
expect(result.metrics.checksPassed).toBe(0);
expect(result.metrics.checksFailed).toBe(0);
expect(result.metrics.totalChecks).toBe(0);
expect(result.metrics.successRate).toBe('N/A');
expect(result.components).toEqual([]);
expect(result.alerts).toEqual([]);
expect(result.hasAlerts).toBe(false);
expect(result.hasDegradedComponents).toBe(false);
expect(result.hasErrorComponents).toBe(false);
});
it('should handle degraded status correctly', () => {
const healthDTO: HealthDTO = {
status: 'degraded',
timestamp: new Date().toISOString(),
uptime: 95.5,
responseTime: 500,
errorRate: 4.5,
components: [
{
name: 'Database',
status: 'degraded',
lastCheck: new Date().toISOString(),
responseTime: 200,
errorRate: 2.0,
},
],
};
const result = HealthViewDataBuilder.build(healthDTO);
expect(result.overallStatus.status).toBe('degraded');
expect(result.overallStatus.statusLabel).toBe('Degraded');
expect(result.overallStatus.statusColor).toBe('#f59e0b');
expect(result.overallStatus.statusIcon).toBe('⚠');
expect(result.metrics.uptime).toBe('95.50%');
expect(result.metrics.responseTime).toBe('500ms');
expect(result.metrics.errorRate).toBe('4.50%');
expect(result.hasDegradedComponents).toBe(true);
});
it('should handle error status correctly', () => {
const healthDTO: HealthDTO = {
status: 'error',
timestamp: new Date().toISOString(),
uptime: 85.2,
responseTime: 2000,
errorRate: 14.8,
components: [
{
name: 'Database',
status: 'error',
lastCheck: new Date().toISOString(),
responseTime: 1500,
errorRate: 10.0,
},
],
};
const result = HealthViewDataBuilder.build(healthDTO);
expect(result.overallStatus.status).toBe('error');
expect(result.overallStatus.statusLabel).toBe('Error');
expect(result.overallStatus.statusColor).toBe('#ef4444');
expect(result.overallStatus.statusIcon).toBe('✕');
expect(result.metrics.uptime).toBe('85.20%');
expect(result.metrics.responseTime).toBe('2.00s');
expect(result.metrics.errorRate).toBe('14.80%');
expect(result.hasErrorComponents).toBe(true);
});
it('should handle multiple components with mixed statuses', () => {
const healthDTO: HealthDTO = {
status: 'degraded',
timestamp: new Date().toISOString(),
components: [
{
name: 'Database',
status: 'ok',
lastCheck: new Date().toISOString(),
},
{
name: 'API',
status: 'degraded',
lastCheck: new Date().toISOString(),
},
{
name: 'Cache',
status: 'error',
lastCheck: new Date().toISOString(),
},
],
};
const result = HealthViewDataBuilder.build(healthDTO);
expect(result.components).toHaveLength(3);
expect(result.hasDegradedComponents).toBe(true);
expect(result.hasErrorComponents).toBe(true);
expect(result.components[0].statusLabel).toBe('Healthy');
expect(result.components[1].statusLabel).toBe('Degraded');
expect(result.components[2].statusLabel).toBe('Error');
});
it('should handle multiple alerts with different severities', () => {
const healthDTO: HealthDTO = {
status: 'ok',
timestamp: new Date().toISOString(),
alerts: [
{
id: 'alert-1',
type: 'critical',
title: 'Critical Alert',
message: 'Critical issue detected',
timestamp: new Date().toISOString(),
},
{
id: 'alert-2',
type: 'warning',
title: 'Warning Alert',
message: 'Warning message',
timestamp: new Date().toISOString(),
},
{
id: 'alert-3',
type: 'info',
title: 'Info Alert',
message: 'Informational message',
timestamp: new Date().toISOString(),
},
],
};
const result = HealthViewDataBuilder.build(healthDTO);
expect(result.alerts).toHaveLength(3);
expect(result.hasAlerts).toBe(true);
expect(result.alerts[0].severity).toBe('Critical');
expect(result.alerts[0].severityColor).toBe('#ef4444');
expect(result.alerts[1].severity).toBe('Warning');
expect(result.alerts[1].severityColor).toBe('#f59e0b');
expect(result.alerts[2].severity).toBe('Info');
expect(result.alerts[2].severityColor).toBe('#3b82f6');
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const now = new Date();
const healthDTO: HealthDTO = {
status: 'ok',
timestamp: now.toISOString(),
uptime: 99.99,
responseTime: 100,
errorRate: 0.01,
lastCheck: now.toISOString(),
checksPassed: 9999,
checksFailed: 1,
components: [
{
name: 'Test Component',
status: 'ok',
lastCheck: now.toISOString(),
responseTime: 50,
errorRate: 0.005,
},
],
alerts: [
{
id: 'test-alert',
type: 'info',
title: 'Test Alert',
message: 'Test message',
timestamp: now.toISOString(),
},
],
};
const result = HealthViewDataBuilder.build(healthDTO);
expect(result.overallStatus.status).toBe(healthDTO.status);
expect(result.overallStatus.timestamp).toBe(healthDTO.timestamp);
expect(result.metrics.uptime).toBe('99.99%');
expect(result.metrics.responseTime).toBe('100ms');
expect(result.metrics.errorRate).toBe('0.01%');
expect(result.metrics.lastCheck).toBe(healthDTO.lastCheck);
expect(result.metrics.checksPassed).toBe(healthDTO.checksPassed);
expect(result.metrics.checksFailed).toBe(healthDTO.checksFailed);
expect(result.components[0].name).toBe(healthDTO.components![0].name);
expect(result.components[0].status).toBe(healthDTO.components![0].status);
expect(result.alerts[0].id).toBe(healthDTO.alerts![0].id);
expect(result.alerts[0].type).toBe(healthDTO.alerts![0].type);
});
it('should not modify the input DTO', () => {
const healthDTO: HealthDTO = {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: 99.95,
responseTime: 150,
errorRate: 0.05,
components: [
{
name: 'Database',
status: 'ok',
lastCheck: new Date().toISOString(),
},
],
};
const originalDTO = JSON.parse(JSON.stringify(healthDTO));
HealthViewDataBuilder.build(healthDTO);
expect(healthDTO).toEqual(originalDTO);
});
it('should transform all numeric fields to formatted strings', () => {
const healthDTO: HealthDTO = {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: 99.95,
responseTime: 150,
errorRate: 0.05,
checksPassed: 995,
checksFailed: 5,
};
const result = HealthViewDataBuilder.build(healthDTO);
expect(typeof result.metrics.uptime).toBe('string');
expect(typeof result.metrics.responseTime).toBe('string');
expect(typeof result.metrics.errorRate).toBe('string');
expect(typeof result.metrics.successRate).toBe('string');
});
it('should handle large numbers correctly', () => {
const healthDTO: HealthDTO = {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: 99.999,
responseTime: 5000,
errorRate: 0.001,
checksPassed: 999999,
checksFailed: 1,
};
const result = HealthViewDataBuilder.build(healthDTO);
expect(result.metrics.uptime).toBe('100.00%');
expect(result.metrics.responseTime).toBe('5.00s');
expect(result.metrics.errorRate).toBe('0.00%');
expect(result.metrics.successRate).toBe('100.0%');
});
});
describe('edge cases', () => {
it('should handle null/undefined numeric fields', () => {
const healthDTO: HealthDTO = {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: null as any,
responseTime: undefined,
errorRate: null as any,
};
const result = HealthViewDataBuilder.build(healthDTO);
expect(result.metrics.uptime).toBe('N/A');
expect(result.metrics.responseTime).toBe('N/A');
expect(result.metrics.errorRate).toBe('N/A');
});
it('should handle negative numeric values', () => {
const healthDTO: HealthDTO = {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: -1,
responseTime: -100,
errorRate: -0.5,
};
const result = HealthViewDataBuilder.build(healthDTO);
expect(result.metrics.uptime).toBe('N/A');
expect(result.metrics.responseTime).toBe('N/A');
expect(result.metrics.errorRate).toBe('N/A');
});
it('should handle empty components and alerts arrays', () => {
const healthDTO: HealthDTO = {
status: 'ok',
timestamp: new Date().toISOString(),
components: [],
alerts: [],
};
const result = HealthViewDataBuilder.build(healthDTO);
expect(result.components).toEqual([]);
expect(result.alerts).toEqual([]);
expect(result.hasAlerts).toBe(false);
expect(result.hasDegradedComponents).toBe(false);
expect(result.hasErrorComponents).toBe(false);
});
it('should handle component with missing optional fields', () => {
const healthDTO: HealthDTO = {
status: 'ok',
timestamp: new Date().toISOString(),
components: [
{
name: 'Test Component',
status: 'ok',
},
],
};
const result = HealthViewDataBuilder.build(healthDTO);
expect(result.components[0].lastCheck).toBeDefined();
expect(result.components[0].formattedLastCheck).toBeDefined();
expect(result.components[0].responseTime).toBe('N/A');
expect(result.components[0].errorRate).toBe('N/A');
});
it('should handle alert with missing optional fields', () => {
const healthDTO: HealthDTO = {
status: 'ok',
timestamp: new Date().toISOString(),
alerts: [
{
id: 'alert-1',
type: 'info',
title: 'Test Alert',
message: 'Test message',
timestamp: new Date().toISOString(),
},
],
};
const result = HealthViewDataBuilder.build(healthDTO);
expect(result.alerts[0].id).toBe('alert-1');
expect(result.alerts[0].type).toBe('info');
expect(result.alerts[0].title).toBe('Test Alert');
expect(result.alerts[0].message).toBe('Test message');
expect(result.alerts[0].timestamp).toBeDefined();
expect(result.alerts[0].formattedTimestamp).toBeDefined();
expect(result.alerts[0].relativeTime).toBeDefined();
});
it('should handle unknown status', () => {
const healthDTO: HealthDTO = {
status: 'unknown',
timestamp: new Date().toISOString(),
};
const result = HealthViewDataBuilder.build(healthDTO);
expect(result.overallStatus.status).toBe('unknown');
expect(result.overallStatus.statusLabel).toBe('Unknown');
expect(result.overallStatus.statusColor).toBe('#6b7280');
expect(result.overallStatus.statusIcon).toBe('?');
});
});
describe('derived fields', () => {
it('should correctly calculate hasAlerts', () => {
const healthDTO: HealthDTO = {
status: 'ok',
timestamp: new Date().toISOString(),
alerts: [
{
id: 'alert-1',
type: 'info',
title: 'Test',
message: 'Test message',
timestamp: new Date().toISOString(),
},
],
};
const result = HealthViewDataBuilder.build(healthDTO);
expect(result.hasAlerts).toBe(true);
});
it('should correctly calculate hasDegradedComponents', () => {
const healthDTO: HealthDTO = {
status: 'ok',
timestamp: new Date().toISOString(),
components: [
{
name: 'Component 1',
status: 'ok',
lastCheck: new Date().toISOString(),
},
{
name: 'Component 2',
status: 'degraded',
lastCheck: new Date().toISOString(),
},
],
};
const result = HealthViewDataBuilder.build(healthDTO);
expect(result.hasDegradedComponents).toBe(true);
});
it('should correctly calculate hasErrorComponents', () => {
const healthDTO: HealthDTO = {
status: 'ok',
timestamp: new Date().toISOString(),
components: [
{
name: 'Component 1',
status: 'ok',
lastCheck: new Date().toISOString(),
},
{
name: 'Component 2',
status: 'error',
lastCheck: new Date().toISOString(),
},
],
};
const result = HealthViewDataBuilder.build(healthDTO);
expect(result.hasErrorComponents).toBe(true);
});
it('should correctly calculate totalChecks', () => {
const healthDTO: HealthDTO = {
status: 'ok',
timestamp: new Date().toISOString(),
checksPassed: 100,
checksFailed: 20,
};
const result = HealthViewDataBuilder.build(healthDTO);
expect(result.metrics.totalChecks).toBe(120);
});
it('should correctly calculate successRate', () => {
const healthDTO: HealthDTO = {
status: 'ok',
timestamp: new Date().toISOString(),
checksPassed: 90,
checksFailed: 10,
};
const result = HealthViewDataBuilder.build(healthDTO);
expect(result.metrics.successRate).toBe('90.0%');
});
it('should handle zero checks correctly', () => {
const healthDTO: HealthDTO = {
status: 'ok',
timestamp: new Date().toISOString(),
checksPassed: 0,
checksFailed: 0,
};
const result = HealthViewDataBuilder.build(healthDTO);
expect(result.metrics.totalChecks).toBe(0);
expect(result.metrics.successRate).toBe('N/A');
});
});
});

View File

@@ -0,0 +1,167 @@
import { describe, it, expect } from 'vitest';
import { HomeViewDataBuilder } from './HomeViewDataBuilder';
import type { HomeDataDTO } from '@/lib/types/dtos/HomeDataDTO';
describe('HomeViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform HomeDataDTO to HomeViewData correctly', () => {
const homeDataDto: HomeDataDTO = {
isAlpha: true,
upcomingRaces: [
{
id: 'race-1',
name: 'Test Race',
scheduledAt: '2024-01-01T10:00:00Z',
track: 'Test Track',
},
],
topLeagues: [
{
id: 'league-1',
name: 'Test League',
description: 'Test Description',
},
],
teams: [
{
id: 'team-1',
name: 'Test Team',
tag: 'TT',
},
],
};
const result = HomeViewDataBuilder.build(homeDataDto);
expect(result).toEqual({
isAlpha: true,
upcomingRaces: [
{
id: 'race-1',
name: 'Test Race',
scheduledAt: '2024-01-01T10:00:00Z',
track: 'Test Track',
},
],
topLeagues: [
{
id: 'league-1',
name: 'Test League',
description: 'Test Description',
},
],
teams: [
{
id: 'team-1',
name: 'Test Team',
tag: 'TT',
},
],
});
});
it('should handle empty arrays correctly', () => {
const homeDataDto: HomeDataDTO = {
isAlpha: false,
upcomingRaces: [],
topLeagues: [],
teams: [],
};
const result = HomeViewDataBuilder.build(homeDataDto);
expect(result).toEqual({
isAlpha: false,
upcomingRaces: [],
topLeagues: [],
teams: [],
});
});
it('should handle multiple items in arrays', () => {
const homeDataDto: HomeDataDTO = {
isAlpha: true,
upcomingRaces: [
{ id: 'race-1', name: 'Race 1', scheduledAt: '2024-01-01T10:00:00Z', track: 'Track 1' },
{ id: 'race-2', name: 'Race 2', scheduledAt: '2024-01-02T10:00:00Z', track: 'Track 2' },
],
topLeagues: [
{ id: 'league-1', name: 'League 1', description: 'Description 1' },
{ id: 'league-2', name: 'League 2', description: 'Description 2' },
],
teams: [
{ id: 'team-1', name: 'Team 1', tag: 'T1' },
{ id: 'team-2', name: 'Team 2', tag: 'T2' },
],
};
const result = HomeViewDataBuilder.build(homeDataDto);
expect(result.upcomingRaces).toHaveLength(2);
expect(result.topLeagues).toHaveLength(2);
expect(result.teams).toHaveLength(2);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const homeDataDto: HomeDataDTO = {
isAlpha: true,
upcomingRaces: [{ id: 'race-1', name: 'Race', scheduledAt: '2024-01-01T10:00:00Z', track: 'Track' }],
topLeagues: [{ id: 'league-1', name: 'League', description: 'Description' }],
teams: [{ id: 'team-1', name: 'Team', tag: 'T' }],
};
const result = HomeViewDataBuilder.build(homeDataDto);
expect(result.isAlpha).toBe(homeDataDto.isAlpha);
expect(result.upcomingRaces).toEqual(homeDataDto.upcomingRaces);
expect(result.topLeagues).toEqual(homeDataDto.topLeagues);
expect(result.teams).toEqual(homeDataDto.teams);
});
it('should not modify the input DTO', () => {
const homeDataDto: HomeDataDTO = {
isAlpha: true,
upcomingRaces: [{ id: 'race-1', name: 'Race', scheduledAt: '2024-01-01T10:00:00Z', track: 'Track' }],
topLeagues: [{ id: 'league-1', name: 'League', description: 'Description' }],
teams: [{ id: 'team-1', name: 'Team', tag: 'T' }],
};
const originalDto = { ...homeDataDto };
HomeViewDataBuilder.build(homeDataDto);
expect(homeDataDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle false isAlpha value', () => {
const homeDataDto: HomeDataDTO = {
isAlpha: false,
upcomingRaces: [],
topLeagues: [],
teams: [],
};
const result = HomeViewDataBuilder.build(homeDataDto);
expect(result.isAlpha).toBe(false);
});
it('should handle null/undefined values in arrays', () => {
const homeDataDto: HomeDataDTO = {
isAlpha: true,
upcomingRaces: [{ id: 'race-1', name: 'Race', scheduledAt: '2024-01-01T10:00:00Z', track: 'Track' }],
topLeagues: [{ id: 'league-1', name: 'League', description: 'Description' }],
teams: [{ id: 'team-1', name: 'Team', tag: 'T' }],
};
const result = HomeViewDataBuilder.build(homeDataDto);
expect(result.upcomingRaces[0].id).toBe('race-1');
expect(result.topLeagues[0].id).toBe('league-1');
expect(result.teams[0].id).toBe('team-1');
});
});
});

View File

@@ -0,0 +1,600 @@
import { describe, it, expect } from 'vitest';
import { LeaderboardsViewDataBuilder } from './LeaderboardsViewDataBuilder';
describe('LeaderboardsViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform Leaderboards DTO to LeaderboardsViewData correctly', () => {
const leaderboardsDTO = {
drivers: {
drivers: [
{
id: 'driver-1',
name: 'John Doe',
rating: 1234.56,
skillLevel: 'pro',
nationality: 'USA',
racesCompleted: 150,
wins: 25,
podiums: 60,
isActive: true,
rank: 1,
avatarUrl: 'https://example.com/avatar1.jpg',
},
{
id: 'driver-2',
name: 'Jane Smith',
rating: 1100.0,
skillLevel: 'advanced',
nationality: 'Canada',
racesCompleted: 100,
wins: 15,
podiums: 40,
isActive: true,
rank: 2,
avatarUrl: 'https://example.com/avatar2.jpg',
},
],
totalRaces: 250,
totalWins: 40,
activeCount: 2,
},
teams: {
teams: [
{
id: 'team-1',
name: 'Racing Team Alpha',
tag: 'RTA',
logoUrl: 'https://example.com/logo1.jpg',
memberCount: 15,
rating: 1500,
totalWins: 50,
totalRaces: 200,
performanceLevel: 'elite',
isRecruiting: false,
createdAt: '2023-01-01',
},
{
id: 'team-2',
name: 'Speed Demons',
tag: 'SD',
logoUrl: 'https://example.com/logo2.jpg',
memberCount: 8,
rating: 1200,
totalWins: 20,
totalRaces: 150,
performanceLevel: 'advanced',
isRecruiting: true,
createdAt: '2023-06-01',
},
],
recruitingCount: 5,
groupsBySkillLevel: 'pro,advanced,intermediate',
topTeams: [
{
id: 'team-1',
name: 'Racing Team Alpha',
tag: 'RTA',
logoUrl: 'https://example.com/logo1.jpg',
memberCount: 15,
rating: 1500,
totalWins: 50,
totalRaces: 200,
performanceLevel: 'elite',
isRecruiting: false,
createdAt: '2023-01-01',
},
{
id: 'team-2',
name: 'Speed Demons',
tag: 'SD',
logoUrl: 'https://example.com/logo2.jpg',
memberCount: 8,
rating: 1200,
totalWins: 20,
totalRaces: 150,
performanceLevel: 'advanced',
isRecruiting: true,
createdAt: '2023-06-01',
},
],
},
};
const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO);
// Verify drivers
expect(result.drivers).toHaveLength(2);
expect(result.drivers[0].id).toBe('driver-1');
expect(result.drivers[0].name).toBe('John Doe');
expect(result.drivers[0].rating).toBe(1234.56);
expect(result.drivers[0].skillLevel).toBe('pro');
expect(result.drivers[0].nationality).toBe('USA');
expect(result.drivers[0].wins).toBe(25);
expect(result.drivers[0].podiums).toBe(60);
expect(result.drivers[0].racesCompleted).toBe(150);
expect(result.drivers[0].rank).toBe(1);
expect(result.drivers[0].avatarUrl).toBe('https://example.com/avatar1.jpg');
expect(result.drivers[0].position).toBe(1);
// Verify teams
expect(result.teams).toHaveLength(2);
expect(result.teams[0].id).toBe('team-1');
expect(result.teams[0].name).toBe('Racing Team Alpha');
expect(result.teams[0].tag).toBe('RTA');
expect(result.teams[0].memberCount).toBe(15);
expect(result.teams[0].totalWins).toBe(50);
expect(result.teams[0].totalRaces).toBe(200);
expect(result.teams[0].logoUrl).toBe('https://example.com/logo1.jpg');
expect(result.teams[0].position).toBe(1);
expect(result.teams[0].isRecruiting).toBe(false);
expect(result.teams[0].performanceLevel).toBe('elite');
expect(result.teams[0].rating).toBe(1500);
expect(result.teams[0].category).toBeUndefined();
});
it('should handle empty driver and team arrays', () => {
const leaderboardsDTO = {
drivers: {
drivers: [],
totalRaces: 0,
totalWins: 0,
activeCount: 0,
},
teams: {
teams: [],
recruitingCount: 0,
groupsBySkillLevel: '',
topTeams: [],
},
};
const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO);
expect(result.drivers).toEqual([]);
expect(result.teams).toEqual([]);
});
it('should handle missing avatar URLs with empty string fallback', () => {
const leaderboardsDTO = {
drivers: {
drivers: [
{
id: 'driver-1',
name: 'John Doe',
rating: 1234.56,
skillLevel: 'pro',
nationality: 'USA',
racesCompleted: 150,
wins: 25,
podiums: 60,
isActive: true,
rank: 1,
},
],
totalRaces: 150,
totalWins: 25,
activeCount: 1,
},
teams: {
teams: [],
recruitingCount: 0,
groupsBySkillLevel: '',
topTeams: [
{
id: 'team-1',
name: 'Racing Team Alpha',
tag: 'RTA',
memberCount: 15,
totalWins: 50,
totalRaces: 200,
performanceLevel: 'elite',
isRecruiting: false,
createdAt: '2023-01-01',
},
],
},
};
const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO);
expect(result.drivers[0].avatarUrl).toBe('');
expect(result.teams[0].logoUrl).toBe('');
});
it('should handle missing optional team fields with defaults', () => {
const leaderboardsDTO = {
drivers: {
drivers: [],
totalRaces: 0,
totalWins: 0,
activeCount: 0,
},
teams: {
teams: [],
recruitingCount: 0,
groupsBySkillLevel: '',
topTeams: [
{
id: 'team-1',
name: 'Racing Team Alpha',
tag: 'RTA',
memberCount: 15,
totalWins: 50,
totalRaces: 200,
performanceLevel: 'elite',
isRecruiting: false,
createdAt: '2023-01-01',
},
],
},
};
const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO);
expect(result.teams[0].rating).toBe(0);
expect(result.teams[0].logoUrl).toBe('');
});
it('should calculate position based on index', () => {
const leaderboardsDTO = {
drivers: {
drivers: [
{ id: 'driver-1', name: 'Driver 1', rating: 1000, skillLevel: 'pro', nationality: 'USA', racesCompleted: 100, wins: 10, podiums: 30, isActive: true, rank: 1 },
{ id: 'driver-2', name: 'Driver 2', rating: 900, skillLevel: 'advanced', nationality: 'Canada', racesCompleted: 80, wins: 8, podiums: 25, isActive: true, rank: 2 },
{ id: 'driver-3', name: 'Driver 3', rating: 800, skillLevel: 'intermediate', nationality: 'UK', racesCompleted: 60, wins: 5, podiums: 15, isActive: true, rank: 3 },
],
totalRaces: 240,
totalWins: 23,
activeCount: 3,
},
teams: {
teams: [],
recruitingCount: 1,
groupsBySkillLevel: 'elite,advanced,intermediate',
topTeams: [
{ id: 'team-1', name: 'Team 1', tag: 'T1', memberCount: 10, totalWins: 30, totalRaces: 150, performanceLevel: 'elite', isRecruiting: false, createdAt: '2023-01-01' },
{ id: 'team-2', name: 'Team 2', tag: 'T2', memberCount: 8, totalWins: 20, totalRaces: 120, performanceLevel: 'advanced', isRecruiting: true, createdAt: '2023-02-01' },
{ id: 'team-3', name: 'Team 3', tag: 'T3', memberCount: 6, totalWins: 10, totalRaces: 80, performanceLevel: 'intermediate', isRecruiting: false, createdAt: '2023-03-01' },
],
},
};
const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO);
expect(result.drivers[0].position).toBe(1);
expect(result.drivers[1].position).toBe(2);
expect(result.drivers[2].position).toBe(3);
expect(result.teams[0].position).toBe(1);
expect(result.teams[1].position).toBe(2);
expect(result.teams[2].position).toBe(3);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const leaderboardsDTO = {
drivers: {
drivers: [
{
id: 'driver-123',
name: 'John Doe',
rating: 1234.56,
skillLevel: 'pro',
nationality: 'USA',
racesCompleted: 150,
wins: 25,
podiums: 60,
isActive: true,
rank: 1,
avatarUrl: 'https://example.com/avatar.jpg',
},
],
totalRaces: 150,
totalWins: 25,
activeCount: 1,
},
teams: {
teams: [],
recruitingCount: 5,
groupsBySkillLevel: 'pro,advanced',
topTeams: [
{
id: 'team-123',
name: 'Racing Team Alpha',
tag: 'RTA',
logoUrl: 'https://example.com/logo.jpg',
memberCount: 15,
rating: 1500,
totalWins: 50,
totalRaces: 200,
performanceLevel: 'elite',
isRecruiting: false,
createdAt: '2023-01-01',
},
],
},
};
const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO);
expect(result.drivers[0].name).toBe(leaderboardsDTO.drivers.drivers[0].name);
expect(result.drivers[0].nationality).toBe(leaderboardsDTO.drivers.drivers[0].nationality);
expect(result.drivers[0].avatarUrl).toBe(leaderboardsDTO.drivers.drivers[0].avatarUrl);
expect(result.teams[0].name).toBe(leaderboardsDTO.teams.topTeams[0].name);
expect(result.teams[0].tag).toBe(leaderboardsDTO.teams.topTeams[0].tag);
expect(result.teams[0].logoUrl).toBe(leaderboardsDTO.teams.topTeams[0].logoUrl);
});
it('should not modify the input DTO', () => {
const leaderboardsDTO = {
drivers: {
drivers: [
{
id: 'driver-123',
name: 'John Doe',
rating: 1234.56,
skillLevel: 'pro',
nationality: 'USA',
racesCompleted: 150,
wins: 25,
podiums: 60,
isActive: true,
rank: 1,
avatarUrl: 'https://example.com/avatar.jpg',
},
],
totalRaces: 150,
totalWins: 25,
activeCount: 1,
},
teams: {
teams: [],
recruitingCount: 5,
groupsBySkillLevel: 'pro,advanced',
topTeams: [
{
id: 'team-123',
name: 'Racing Team Alpha',
tag: 'RTA',
logoUrl: 'https://example.com/logo.jpg',
memberCount: 15,
rating: 1500,
totalWins: 50,
totalRaces: 200,
performanceLevel: 'elite',
isRecruiting: false,
createdAt: '2023-01-01',
},
],
},
};
const originalDTO = JSON.parse(JSON.stringify(leaderboardsDTO));
LeaderboardsViewDataBuilder.build(leaderboardsDTO);
expect(leaderboardsDTO).toEqual(originalDTO);
});
it('should handle large numbers correctly', () => {
const leaderboardsDTO = {
drivers: {
drivers: [
{
id: 'driver-1',
name: 'John Doe',
rating: 999999.99,
skillLevel: 'pro',
nationality: 'USA',
racesCompleted: 10000,
wins: 2500,
podiums: 5000,
isActive: true,
rank: 1,
avatarUrl: 'https://example.com/avatar.jpg',
},
],
totalRaces: 10000,
totalWins: 2500,
activeCount: 1,
},
teams: {
teams: [],
recruitingCount: 0,
groupsBySkillLevel: '',
topTeams: [
{
id: 'team-1',
name: 'Racing Team Alpha',
tag: 'RTA',
logoUrl: 'https://example.com/logo.jpg',
memberCount: 100,
rating: 999999,
totalWins: 5000,
totalRaces: 10000,
performanceLevel: 'elite',
isRecruiting: false,
createdAt: '2023-01-01',
},
],
},
};
const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO);
expect(result.drivers[0].rating).toBe(999999.99);
expect(result.drivers[0].wins).toBe(2500);
expect(result.drivers[0].podiums).toBe(5000);
expect(result.drivers[0].racesCompleted).toBe(10000);
expect(result.teams[0].rating).toBe(999999);
expect(result.teams[0].totalWins).toBe(5000);
expect(result.teams[0].totalRaces).toBe(10000);
});
});
describe('edge cases', () => {
it('should handle null/undefined avatar URLs', () => {
const leaderboardsDTO = {
drivers: {
drivers: [
{
id: 'driver-1',
name: 'John Doe',
rating: 1234.56,
skillLevel: 'pro',
nationality: 'USA',
racesCompleted: 150,
wins: 25,
podiums: 60,
isActive: true,
rank: 1,
avatarUrl: null as any,
},
],
totalRaces: 150,
totalWins: 25,
activeCount: 1,
},
teams: {
teams: [],
recruitingCount: 0,
groupsBySkillLevel: '',
topTeams: [
{
id: 'team-1',
name: 'Racing Team Alpha',
tag: 'RTA',
logoUrl: undefined as any,
memberCount: 15,
totalWins: 50,
totalRaces: 200,
performanceLevel: 'elite',
isRecruiting: false,
createdAt: '2023-01-01',
},
],
},
};
const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO);
expect(result.drivers[0].avatarUrl).toBe('');
expect(result.teams[0].logoUrl).toBe('');
});
it('should handle null/undefined rating', () => {
const leaderboardsDTO = {
drivers: {
drivers: [
{
id: 'driver-1',
name: 'John Doe',
rating: null as any,
skillLevel: 'pro',
nationality: 'USA',
racesCompleted: 150,
wins: 25,
podiums: 60,
isActive: true,
rank: 1,
},
],
totalRaces: 150,
totalWins: 25,
activeCount: 1,
},
teams: {
teams: [],
recruitingCount: 0,
groupsBySkillLevel: '',
topTeams: [
{
id: 'team-1',
name: 'Racing Team Alpha',
tag: 'RTA',
memberCount: 15,
rating: null as any,
totalWins: 50,
totalRaces: 200,
performanceLevel: 'elite',
isRecruiting: false,
createdAt: '2023-01-01',
},
],
},
};
const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO);
expect(result.drivers[0].rating).toBeNull();
expect(result.teams[0].rating).toBe(0);
});
it('should handle null/undefined totalWins and totalRaces', () => {
const leaderboardsDTO = {
drivers: {
drivers: [],
totalRaces: 0,
totalWins: 0,
activeCount: 0,
},
teams: {
teams: [],
recruitingCount: 0,
groupsBySkillLevel: '',
topTeams: [
{
id: 'team-1',
name: 'Racing Team Alpha',
tag: 'RTA',
memberCount: 15,
totalWins: null as any,
totalRaces: null as any,
performanceLevel: 'elite',
isRecruiting: false,
createdAt: '2023-01-01',
},
],
},
};
const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO);
expect(result.teams[0].totalWins).toBe(0);
expect(result.teams[0].totalRaces).toBe(0);
});
it('should handle empty performance level', () => {
const leaderboardsDTO = {
drivers: {
drivers: [],
totalRaces: 0,
totalWins: 0,
activeCount: 0,
},
teams: {
teams: [],
recruitingCount: 0,
groupsBySkillLevel: '',
topTeams: [
{
id: 'team-1',
name: 'Racing Team Alpha',
tag: 'RTA',
memberCount: 15,
totalWins: 50,
totalRaces: 200,
performanceLevel: '',
isRecruiting: false,
createdAt: '2023-01-01',
},
],
},
};
const result = LeaderboardsViewDataBuilder.build(leaderboardsDTO);
expect(result.teams[0].performanceLevel).toBe('N/A');
});
});
});

View File

@@ -0,0 +1,141 @@
import { describe, it, expect } from 'vitest';
import { LeagueCoverViewDataBuilder } from './LeagueCoverViewDataBuilder';
import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
describe('LeagueCoverViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform MediaBinaryDTO to LeagueCoverViewData correctly', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = LeagueCoverViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
it('should handle JPEG cover images', () => {
const buffer = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/jpeg',
};
const result = LeagueCoverViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/jpeg');
});
it('should handle WebP cover images', () => {
const buffer = new Uint8Array([0x52, 0x49, 0x46, 0x46]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/webp',
};
const result = LeagueCoverViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/webp');
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = LeagueCoverViewDataBuilder.build(mediaDto);
expect(result.buffer).toBeDefined();
expect(result.contentType).toBe(mediaDto.contentType);
});
it('should not modify the input DTO', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const originalDto = { ...mediaDto };
LeagueCoverViewDataBuilder.build(mediaDto);
expect(mediaDto).toEqual(originalDto);
});
it('should convert buffer to base64 string', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = LeagueCoverViewDataBuilder.build(mediaDto);
expect(typeof result.buffer).toBe('string');
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
});
});
describe('edge cases', () => {
it('should handle empty buffer', () => {
const buffer = new Uint8Array([]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = LeagueCoverViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe('');
expect(result.contentType).toBe('image/png');
});
it('should handle large cover images', () => {
const buffer = new Uint8Array(2 * 1024 * 1024); // 2MB
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/jpeg',
};
const result = LeagueCoverViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/jpeg');
});
it('should handle buffer with all zeros', () => {
const buffer = new Uint8Array([0x00, 0x00, 0x00, 0x00]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = LeagueCoverViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
it('should handle buffer with all ones', () => {
const buffer = new Uint8Array([0xff, 0xff, 0xff, 0xff]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = LeagueCoverViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
});
});

View File

@@ -0,0 +1,577 @@
import { describe, it, expect } from 'vitest';
import { LeagueDetailViewDataBuilder } from './LeagueDetailViewDataBuilder';
import type { LeagueWithCapacityAndScoringDTO } from '@/lib/types/generated/LeagueWithCapacityAndScoringDTO';
import type { LeagueMembershipsDTO } from '@/lib/types/generated/LeagueMembershipsDTO';
import type { GetDriverOutputDTO } from '@/lib/types/generated/GetDriverOutputDTO';
import type { LeagueScoringConfigDTO } from '@/lib/types/generated/LeagueScoringConfigDTO';
import type { RaceDTO } from '@/lib/types/generated/RaceDTO';
describe('LeagueDetailViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform league DTOs to LeagueDetailViewData correctly', () => {
const league: LeagueWithCapacityAndScoringDTO = {
id: 'league-1',
name: 'Pro League',
description: 'A competitive league for experienced drivers',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00.000Z',
settings: {
maxDrivers: 32,
qualifyingFormat: 'Solo • 32 max',
},
usedSlots: 25,
category: 'competitive',
scoring: {
gameId: 'game-1',
gameName: 'iRacing',
primaryChampionshipType: 'Single Championship',
scoringPresetId: 'preset-1',
scoringPresetName: 'Standard',
dropPolicySummary: 'Drop 2 worst races',
scoringPatternSummary: 'Points based on finish position',
},
timingSummary: 'Weekly races on Sundays',
logoUrl: 'https://example.com/logo.png',
pendingJoinRequestsCount: 3,
pendingProtestsCount: 1,
walletBalance: 1000,
};
const owner: GetDriverOutputDTO = {
id: 'owner-1',
name: 'John Doe',
iracingId: '12345',
country: 'USA',
bio: 'Experienced driver',
joinedAt: '2023-01-01T00:00:00.000Z',
avatarUrl: 'https://example.com/avatar.jpg',
};
const scoringConfig: LeagueScoringConfigDTO = {
id: 'config-1',
leagueId: 'league-1',
gameId: 'game-1',
gameName: 'iRacing',
primaryChampionshipType: 'Single Championship',
scoringPresetId: 'preset-1',
scoringPresetName: 'Standard',
dropPolicySummary: 'Drop 2 worst races',
scoringPatternSummary: 'Points based on finish position',
dropRaces: 2,
pointsPerRace: 100,
pointsForWin: 25,
pointsForPodium: [20, 15, 10],
};
const memberships: LeagueMembershipsDTO = {
members: [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
name: 'Alice',
iracingId: '11111',
country: 'UK',
joinedAt: '2023-06-01T00:00:00.000Z',
},
role: 'admin',
joinedAt: '2023-06-01T00:00:00.000Z',
},
{
driverId: 'driver-2',
driver: {
id: 'driver-2',
name: 'Bob',
iracingId: '22222',
country: 'Germany',
joinedAt: '2023-07-01T00:00:00.000Z',
},
role: 'steward',
joinedAt: '2023-07-01T00:00:00.000Z',
},
{
driverId: 'driver-3',
driver: {
id: 'driver-3',
name: 'Charlie',
iracingId: '33333',
country: 'France',
joinedAt: '2023-08-01T00:00:00.000Z',
},
role: 'member',
joinedAt: '2023-08-01T00:00:00.000Z',
},
],
};
const races: RaceDTO[] = [
{
id: 'race-1',
name: 'Race 1',
date: '2024-01-15T14:00:00.000Z',
track: 'Spa',
car: 'Porsche 911 GT3',
sessionType: 'race',
strengthOfField: 1500,
},
{
id: 'race-2',
name: 'Race 2',
date: '2024-01-22T14:00:00.000Z',
track: 'Monza',
car: 'Ferrari 488 GT3',
sessionType: 'race',
strengthOfField: 1600,
},
];
const sponsors: any[] = [
{
id: 'sponsor-1',
name: 'Sponsor A',
tier: 'main',
logoUrl: 'https://example.com/sponsor-a.png',
websiteUrl: 'https://sponsor-a.com',
tagline: 'Premium racing gear',
},
];
const result = LeagueDetailViewDataBuilder.build({
league,
owner,
scoringConfig,
memberships,
races,
sponsors,
});
expect(result.leagueId).toBe('league-1');
expect(result.name).toBe('Pro League');
expect(result.description).toBe('A competitive league for experienced drivers');
expect(result.logoUrl).toBe('https://example.com/logo.png');
expect(result.info.name).toBe('Pro League');
expect(result.info.description).toBe('A competitive league for experienced drivers');
expect(result.info.membersCount).toBe(3);
expect(result.info.racesCount).toBe(2);
expect(result.info.avgSOF).toBe(1550);
expect(result.info.structure).toBe('Solo • 32 max');
expect(result.info.scoring).toBe('preset-1');
expect(result.info.createdAt).toBe('2024-01-01T00:00:00.000Z');
expect(result.info.discordUrl).toBeUndefined();
expect(result.info.youtubeUrl).toBeUndefined();
expect(result.info.websiteUrl).toBeUndefined();
expect(result.ownerSummary).not.toBeNull();
expect(result.ownerSummary?.driverId).toBe('owner-1');
expect(result.ownerSummary?.driverName).toBe('John Doe');
expect(result.ownerSummary?.avatarUrl).toBe('https://example.com/avatar.jpg');
expect(result.ownerSummary?.roleBadgeText).toBe('Owner');
expect(result.adminSummaries).toHaveLength(1);
expect(result.adminSummaries[0].driverId).toBe('driver-1');
expect(result.adminSummaries[0].driverName).toBe('Alice');
expect(result.adminSummaries[0].roleBadgeText).toBe('Admin');
expect(result.stewardSummaries).toHaveLength(1);
expect(result.stewardSummaries[0].driverId).toBe('driver-2');
expect(result.stewardSummaries[0].driverName).toBe('Bob');
expect(result.stewardSummaries[0].roleBadgeText).toBe('Steward');
expect(result.memberSummaries).toHaveLength(1);
expect(result.memberSummaries[0].driverId).toBe('driver-3');
expect(result.memberSummaries[0].driverName).toBe('Charlie');
expect(result.memberSummaries[0].roleBadgeText).toBe('Member');
expect(result.sponsors).toHaveLength(1);
expect(result.sponsors[0].id).toBe('sponsor-1');
expect(result.sponsors[0].name).toBe('Sponsor A');
expect(result.sponsors[0].tier).toBe('main');
expect(result.walletBalance).toBe(1000);
expect(result.pendingProtestsCount).toBe(1);
expect(result.pendingJoinRequestsCount).toBe(3);
});
it('should handle league with no owner', () => {
const league: LeagueWithCapacityAndScoringDTO = {
id: 'league-1',
name: 'Test League',
description: 'Test description',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00.000Z',
settings: {
maxDrivers: 32,
},
usedSlots: 10,
};
const result = LeagueDetailViewDataBuilder.build({
league,
owner: null,
scoringConfig: null,
memberships: { members: [] },
races: [],
sponsors: [],
});
expect(result.ownerSummary).toBeNull();
});
it('should handle league with no scoring config', () => {
const league: LeagueWithCapacityAndScoringDTO = {
id: 'league-1',
name: 'Test League',
description: 'Test description',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00.000Z',
settings: {
maxDrivers: 32,
},
usedSlots: 10,
};
const result = LeagueDetailViewDataBuilder.build({
league,
owner: null,
scoringConfig: null,
memberships: { members: [] },
races: [],
sponsors: [],
});
expect(result.info.scoring).toBe('Standard');
});
it('should handle league with no races', () => {
const league: LeagueWithCapacityAndScoringDTO = {
id: 'league-1',
name: 'Test League',
description: 'Test description',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00.000Z',
settings: {
maxDrivers: 32,
},
usedSlots: 10,
};
const result = LeagueDetailViewDataBuilder.build({
league,
owner: null,
scoringConfig: null,
memberships: { members: [] },
races: [],
sponsors: [],
});
expect(result.info.racesCount).toBe(0);
expect(result.info.avgSOF).toBeNull();
expect(result.runningRaces).toEqual([]);
expect(result.nextRace).toBeUndefined();
expect(result.seasonProgress).toEqual({
completedRaces: 0,
totalRaces: 0,
percentage: 0,
});
expect(result.recentResults).toEqual([]);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const league: LeagueWithCapacityAndScoringDTO = {
id: 'league-1',
name: 'Test League',
description: 'Test description',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00.000Z',
settings: {
maxDrivers: 32,
qualifyingFormat: 'Solo • 32 max',
},
usedSlots: 20,
category: 'test',
scoring: {
gameId: 'game-1',
gameName: 'Test Game',
primaryChampionshipType: 'Test Type',
scoringPresetId: 'preset-1',
scoringPresetName: 'Test Preset',
dropPolicySummary: 'Test drop policy',
scoringPatternSummary: 'Test pattern',
},
timingSummary: 'Test timing',
logoUrl: 'https://example.com/test.png',
pendingJoinRequestsCount: 5,
pendingProtestsCount: 2,
walletBalance: 500,
};
const result = LeagueDetailViewDataBuilder.build({
league,
owner: null,
scoringConfig: null,
memberships: { members: [] },
races: [],
sponsors: [],
});
expect(result.leagueId).toBe(league.id);
expect(result.name).toBe(league.name);
expect(result.description).toBe(league.description);
expect(result.logoUrl).toBe(league.logoUrl);
expect(result.walletBalance).toBe(league.walletBalance);
expect(result.pendingProtestsCount).toBe(league.pendingProtestsCount);
expect(result.pendingJoinRequestsCount).toBe(league.pendingJoinRequestsCount);
});
it('should not modify the input DTOs', () => {
const league: LeagueWithCapacityAndScoringDTO = {
id: 'league-1',
name: 'Test League',
description: 'Test description',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00.000Z',
settings: {
maxDrivers: 32,
},
usedSlots: 20,
};
const originalLeague = JSON.parse(JSON.stringify(league));
LeagueDetailViewDataBuilder.build({
league,
owner: null,
scoringConfig: null,
memberships: { members: [] },
races: [],
sponsors: [],
});
expect(league).toEqual(originalLeague);
});
});
describe('edge cases', () => {
it('should handle league with missing optional fields', () => {
const league: LeagueWithCapacityAndScoringDTO = {
id: 'league-1',
name: 'Minimal League',
description: '',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00.000Z',
settings: {
maxDrivers: 32,
},
usedSlots: 10,
};
const result = LeagueDetailViewDataBuilder.build({
league,
owner: null,
scoringConfig: null,
memberships: { members: [] },
races: [],
sponsors: [],
});
expect(result.description).toBe('');
expect(result.logoUrl).toBeUndefined();
expect(result.info.description).toBe('');
expect(result.info.discordUrl).toBeUndefined();
expect(result.info.youtubeUrl).toBeUndefined();
expect(result.info.websiteUrl).toBeUndefined();
});
it('should handle races with missing strengthOfField', () => {
const league: LeagueWithCapacityAndScoringDTO = {
id: 'league-1',
name: 'Test League',
description: 'Test description',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00.000Z',
settings: {
maxDrivers: 32,
},
usedSlots: 10,
};
const races: RaceDTO[] = [
{
id: 'race-1',
name: 'Race 1',
date: '2024-01-15T14:00:00.000Z',
track: 'Spa',
car: 'Porsche 911 GT3',
sessionType: 'race',
},
];
const result = LeagueDetailViewDataBuilder.build({
league,
owner: null,
scoringConfig: null,
memberships: { members: [] },
races,
sponsors: [],
});
expect(result.info.avgSOF).toBeNull();
});
it('should handle races with zero strengthOfField', () => {
const league: LeagueWithCapacityAndScoringDTO = {
id: 'league-1',
name: 'Test League',
description: 'Test description',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00.000Z',
settings: {
maxDrivers: 32,
},
usedSlots: 10,
};
const races: RaceDTO[] = [
{
id: 'race-1',
name: 'Race 1',
date: '2024-01-15T14:00:00.000Z',
track: 'Spa',
car: 'Porsche 911 GT3',
sessionType: 'race',
strengthOfField: 0,
},
];
const result = LeagueDetailViewDataBuilder.build({
league,
owner: null,
scoringConfig: null,
memberships: { members: [] },
races,
sponsors: [],
});
expect(result.info.avgSOF).toBeNull();
});
it('should handle races with different dates for next race calculation', () => {
const now = new Date();
const pastDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 1 day ago
const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 1 day from now
const league: LeagueWithCapacityAndScoringDTO = {
id: 'league-1',
name: 'Test League',
description: 'Test description',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00.000Z',
settings: {
maxDrivers: 32,
},
usedSlots: 10,
};
const races: RaceDTO[] = [
{
id: 'race-1',
name: 'Past Race',
date: pastDate.toISOString(),
track: 'Spa',
car: 'Porsche 911 GT3',
sessionType: 'race',
},
{
id: 'race-2',
name: 'Future Race',
date: futureDate.toISOString(),
track: 'Monza',
car: 'Ferrari 488 GT3',
sessionType: 'race',
},
];
const result = LeagueDetailViewDataBuilder.build({
league,
owner: null,
scoringConfig: null,
memberships: { members: [] },
races,
sponsors: [],
});
expect(result.nextRace).toBeDefined();
expect(result.nextRace?.id).toBe('race-2');
expect(result.nextRace?.name).toBe('Future Race');
expect(result.seasonProgress.completedRaces).toBe(1);
expect(result.seasonProgress.totalRaces).toBe(2);
expect(result.seasonProgress.percentage).toBe(50);
expect(result.recentResults).toHaveLength(1);
expect(result.recentResults[0].raceId).toBe('race-1');
});
it('should handle members with different roles', () => {
const league: LeagueWithCapacityAndScoringDTO = {
id: 'league-1',
name: 'Test League',
description: 'Test description',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00.000Z',
settings: {
maxDrivers: 32,
},
usedSlots: 10,
};
const memberships: LeagueMembershipsDTO = {
members: [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
name: 'Admin',
iracingId: '11111',
country: 'UK',
joinedAt: '2023-06-01T00:00:00.000Z',
},
role: 'admin',
joinedAt: '2023-06-01T00:00:00.000Z',
},
{
driverId: 'driver-2',
driver: {
id: 'driver-2',
name: 'Steward',
iracingId: '22222',
country: 'Germany',
joinedAt: '2023-07-01T00:00:00.000Z',
},
role: 'steward',
joinedAt: '2023-07-01T00:00:00.000Z',
},
{
driverId: 'driver-3',
driver: {
id: 'driver-3',
name: 'Member',
iracingId: '33333',
country: 'France',
joinedAt: '2023-08-01T00:00:00.000Z',
},
role: 'member',
joinedAt: '2023-08-01T00:00:00.000Z',
},
],
};
const result = LeagueDetailViewDataBuilder.build({
league,
owner: null,
scoringConfig: null,
memberships,
races: [],
sponsors: [],
});
expect(result.adminSummaries).toHaveLength(1);
expect(result.stewardSummaries).toHaveLength(1);
expect(result.memberSummaries).toHaveLength(1);
expect(result.info.membersCount).toBe(3);
});
});
});

View File

@@ -0,0 +1,128 @@
import { describe, it, expect } from 'vitest';
import { LeagueLogoViewDataBuilder } from './LeagueLogoViewDataBuilder';
import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
describe('LeagueLogoViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform MediaBinaryDTO to LeagueLogoViewData correctly', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = LeagueLogoViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
it('should handle SVG league logos', () => {
const buffer = new TextEncoder().encode('<svg xmlns="http://www.w3.org/2000/svg"><rect width="100" height="100"/></svg>');
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/svg+xml',
};
const result = LeagueLogoViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/svg+xml');
});
it('should handle transparent PNG logos', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = LeagueLogoViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = LeagueLogoViewDataBuilder.build(mediaDto);
expect(result.buffer).toBeDefined();
expect(result.contentType).toBe(mediaDto.contentType);
});
it('should not modify the input DTO', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const originalDto = { ...mediaDto };
LeagueLogoViewDataBuilder.build(mediaDto);
expect(mediaDto).toEqual(originalDto);
});
it('should convert buffer to base64 string', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = LeagueLogoViewDataBuilder.build(mediaDto);
expect(typeof result.buffer).toBe('string');
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
});
});
describe('edge cases', () => {
it('should handle empty buffer', () => {
const buffer = new Uint8Array([]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = LeagueLogoViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe('');
expect(result.contentType).toBe('image/png');
});
it('should handle small logo files', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = LeagueLogoViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
it('should handle buffer with special characters', () => {
const buffer = new Uint8Array([0x00, 0x01, 0x02, 0x03, 0xff, 0xfe, 0xfd, 0xfc]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = LeagueLogoViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
});
});

View File

@@ -0,0 +1,255 @@
import { describe, it, expect } from 'vitest';
import { LeagueRosterAdminViewDataBuilder } from './LeagueRosterAdminViewDataBuilder';
import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMemberDTO';
import type { LeagueRosterJoinRequestDTO } from '@/lib/types/generated/LeagueRosterJoinRequestDTO';
describe('LeagueRosterAdminViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform roster DTOs to LeagueRosterAdminViewData correctly', () => {
const members: LeagueRosterMemberDTO[] = [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
name: 'Alice',
iracingId: '11111',
country: 'UK',
joinedAt: '2023-06-01T00:00:00.000Z',
},
role: 'admin',
joinedAt: '2023-06-01T00:00:00.000Z',
},
{
driverId: 'driver-2',
driver: {
id: 'driver-2',
name: 'Bob',
iracingId: '22222',
country: 'Germany',
joinedAt: '2023-07-01T00:00:00.000Z',
},
role: 'member',
joinedAt: '2023-07-01T00:00:00.000Z',
},
];
const joinRequests: LeagueRosterJoinRequestDTO[] = [
{
id: 'request-1',
leagueId: 'league-1',
driverId: 'driver-3',
requestedAt: '2024-01-15T10:00:00.000Z',
message: 'I would like to join this league',
driver: {},
},
];
const result = LeagueRosterAdminViewDataBuilder.build({
leagueId: 'league-1',
members,
joinRequests,
});
expect(result.leagueId).toBe('league-1');
expect(result.members).toHaveLength(2);
expect(result.members[0].driverId).toBe('driver-1');
expect(result.members[0].driver.id).toBe('driver-1');
expect(result.members[0].driver.name).toBe('Alice');
expect(result.members[0].role).toBe('admin');
expect(result.members[0].joinedAt).toBe('2023-06-01T00:00:00.000Z');
expect(result.members[0].formattedJoinedAt).toBeDefined();
expect(result.members[1].driverId).toBe('driver-2');
expect(result.members[1].driver.id).toBe('driver-2');
expect(result.members[1].driver.name).toBe('Bob');
expect(result.members[1].role).toBe('member');
expect(result.members[1].joinedAt).toBe('2023-07-01T00:00:00.000Z');
expect(result.members[1].formattedJoinedAt).toBeDefined();
expect(result.joinRequests).toHaveLength(1);
expect(result.joinRequests[0].id).toBe('request-1');
expect(result.joinRequests[0].driver.id).toBe('driver-3');
expect(result.joinRequests[0].driver.name).toBe('Unknown Driver');
expect(result.joinRequests[0].requestedAt).toBe('2024-01-15T10:00:00.000Z');
expect(result.joinRequests[0].formattedRequestedAt).toBeDefined();
expect(result.joinRequests[0].message).toBe('I would like to join this league');
});
it('should handle empty members and join requests', () => {
const result = LeagueRosterAdminViewDataBuilder.build({
leagueId: 'league-1',
members: [],
joinRequests: [],
});
expect(result.leagueId).toBe('league-1');
expect(result.members).toHaveLength(0);
expect(result.joinRequests).toHaveLength(0);
});
it('should handle members without driver details', () => {
const members: LeagueRosterMemberDTO[] = [
{
driverId: 'driver-1',
driver: undefined as any,
role: 'member',
joinedAt: '2023-06-01T00:00:00.000Z',
},
];
const result = LeagueRosterAdminViewDataBuilder.build({
leagueId: 'league-1',
members,
joinRequests: [],
});
expect(result.members[0].driver.name).toBe('Unknown Driver');
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const members: LeagueRosterMemberDTO[] = [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
name: 'Alice',
iracingId: '11111',
country: 'UK',
joinedAt: '2023-06-01T00:00:00.000Z',
},
role: 'admin',
joinedAt: '2023-06-01T00:00:00.000Z',
},
];
const joinRequests: LeagueRosterJoinRequestDTO[] = [
{
id: 'request-1',
leagueId: 'league-1',
driverId: 'driver-3',
requestedAt: '2024-01-15T10:00:00.000Z',
message: 'I would like to join this league',
driver: {},
},
];
const result = LeagueRosterAdminViewDataBuilder.build({
leagueId: 'league-1',
members,
joinRequests,
});
expect(result.leagueId).toBe('league-1');
expect(result.members[0].driverId).toBe(members[0].driverId);
expect(result.members[0].driver.id).toBe(members[0].driver.id);
expect(result.members[0].driver.name).toBe(members[0].driver.name);
expect(result.members[0].role).toBe(members[0].role);
expect(result.members[0].joinedAt).toBe(members[0].joinedAt);
expect(result.joinRequests[0].id).toBe(joinRequests[0].id);
expect(result.joinRequests[0].requestedAt).toBe(joinRequests[0].requestedAt);
expect(result.joinRequests[0].message).toBe(joinRequests[0].message);
});
it('should not modify the input DTOs', () => {
const members: LeagueRosterMemberDTO[] = [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
name: 'Alice',
iracingId: '11111',
country: 'UK',
joinedAt: '2023-06-01T00:00:00.000Z',
},
role: 'admin',
joinedAt: '2023-06-01T00:00:00.000Z',
},
];
const joinRequests: LeagueRosterJoinRequestDTO[] = [
{
id: 'request-1',
leagueId: 'league-1',
driverId: 'driver-3',
requestedAt: '2024-01-15T10:00:00.000Z',
message: 'I would like to join this league',
driver: {},
},
];
const originalMembers = JSON.parse(JSON.stringify(members));
const originalRequests = JSON.parse(JSON.stringify(joinRequests));
LeagueRosterAdminViewDataBuilder.build({
leagueId: 'league-1',
members,
joinRequests,
});
expect(members).toEqual(originalMembers);
expect(joinRequests).toEqual(originalRequests);
});
});
describe('edge cases', () => {
it('should handle members with missing driver field', () => {
const members: LeagueRosterMemberDTO[] = [
{
driverId: 'driver-1',
driver: undefined as any,
role: 'member',
joinedAt: '2023-06-01T00:00:00.000Z',
},
];
const result = LeagueRosterAdminViewDataBuilder.build({
leagueId: 'league-1',
members,
joinRequests: [],
});
expect(result.members[0].driver.name).toBe('Unknown Driver');
});
it('should handle join requests with missing driver field', () => {
const joinRequests: LeagueRosterJoinRequestDTO[] = [
{
id: 'request-1',
leagueId: 'league-1',
driverId: 'driver-3',
requestedAt: '2024-01-15T10:00:00.000Z',
message: 'I would like to join this league',
driver: undefined,
},
];
const result = LeagueRosterAdminViewDataBuilder.build({
leagueId: 'league-1',
members: [],
joinRequests,
});
expect(result.joinRequests[0].driver.name).toBe('Unknown Driver');
});
it('should handle join requests without message', () => {
const joinRequests: LeagueRosterJoinRequestDTO[] = [
{
id: 'request-1',
leagueId: 'league-1',
driverId: 'driver-3',
requestedAt: '2024-01-15T10:00:00.000Z',
driver: {},
},
];
const result = LeagueRosterAdminViewDataBuilder.build({
leagueId: 'league-1',
members: [],
joinRequests,
});
expect(result.joinRequests[0].message).toBeUndefined();
});
});
});

View File

@@ -0,0 +1,211 @@
import { describe, it, expect } from 'vitest';
import { LeagueScheduleViewDataBuilder } from './LeagueScheduleViewDataBuilder';
describe('LeagueScheduleViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform schedule DTO to LeagueScheduleViewData correctly', () => {
const now = new Date();
const pastDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 1 day ago
const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 1 day from now
const apiDto = {
leagueId: 'league-1',
races: [
{
id: 'race-1',
name: 'Past Race',
date: pastDate.toISOString(),
track: 'Spa',
car: 'Porsche 911 GT3',
sessionType: 'race',
},
{
id: 'race-2',
name: 'Future Race',
date: futureDate.toISOString(),
track: 'Monza',
car: 'Ferrari 488 GT3',
sessionType: 'race',
},
],
};
const result = LeagueScheduleViewDataBuilder.build(apiDto, 'driver-1', true);
expect(result.leagueId).toBe('league-1');
expect(result.races).toHaveLength(2);
expect(result.races[0].id).toBe('race-1');
expect(result.races[0].name).toBe('Past Race');
expect(result.races[0].scheduledAt).toBe(pastDate.toISOString());
expect(result.races[0].track).toBe('Spa');
expect(result.races[0].car).toBe('Porsche 911 GT3');
expect(result.races[0].sessionType).toBe('race');
expect(result.races[0].isPast).toBe(true);
expect(result.races[0].isUpcoming).toBe(false);
expect(result.races[0].status).toBe('completed');
expect(result.races[0].isUserRegistered).toBe(false);
expect(result.races[0].canRegister).toBe(false);
expect(result.races[0].canEdit).toBe(true);
expect(result.races[0].canReschedule).toBe(true);
expect(result.races[1].id).toBe('race-2');
expect(result.races[1].name).toBe('Future Race');
expect(result.races[1].scheduledAt).toBe(futureDate.toISOString());
expect(result.races[1].track).toBe('Monza');
expect(result.races[1].car).toBe('Ferrari 488 GT3');
expect(result.races[1].sessionType).toBe('race');
expect(result.races[1].isPast).toBe(false);
expect(result.races[1].isUpcoming).toBe(true);
expect(result.races[1].status).toBe('scheduled');
expect(result.races[1].isUserRegistered).toBe(false);
expect(result.races[1].canRegister).toBe(true);
expect(result.races[1].canEdit).toBe(true);
expect(result.races[1].canReschedule).toBe(true);
expect(result.currentDriverId).toBe('driver-1');
expect(result.isAdmin).toBe(true);
});
it('should handle empty races list', () => {
const apiDto = {
leagueId: 'league-1',
races: [],
};
const result = LeagueScheduleViewDataBuilder.build(apiDto);
expect(result.leagueId).toBe('league-1');
expect(result.races).toHaveLength(0);
});
it('should handle non-admin user', () => {
const now = new Date();
const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000);
const apiDto = {
leagueId: 'league-1',
races: [
{
id: 'race-1',
name: 'Future Race',
date: futureDate.toISOString(),
track: 'Spa',
car: 'Porsche 911 GT3',
sessionType: 'race',
},
],
};
const result = LeagueScheduleViewDataBuilder.build(apiDto, 'driver-1', false);
expect(result.races[0].canEdit).toBe(false);
expect(result.races[0].canReschedule).toBe(false);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const now = new Date();
const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000);
const apiDto = {
leagueId: 'league-1',
races: [
{
id: 'race-1',
name: 'Test Race',
date: futureDate.toISOString(),
track: 'Spa',
car: 'Porsche 911 GT3',
sessionType: 'race',
},
],
};
const result = LeagueScheduleViewDataBuilder.build(apiDto);
expect(result.leagueId).toBe(apiDto.leagueId);
expect(result.races[0].id).toBe(apiDto.races[0].id);
expect(result.races[0].name).toBe(apiDto.races[0].name);
expect(result.races[0].scheduledAt).toBe(apiDto.races[0].date);
expect(result.races[0].track).toBe(apiDto.races[0].track);
expect(result.races[0].car).toBe(apiDto.races[0].car);
expect(result.races[0].sessionType).toBe(apiDto.races[0].sessionType);
});
it('should not modify the input DTO', () => {
const now = new Date();
const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000);
const apiDto = {
leagueId: 'league-1',
races: [
{
id: 'race-1',
name: 'Test Race',
date: futureDate.toISOString(),
track: 'Spa',
car: 'Porsche 911 GT3',
sessionType: 'race',
},
],
};
const originalDto = JSON.parse(JSON.stringify(apiDto));
LeagueScheduleViewDataBuilder.build(apiDto);
expect(apiDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle races with missing optional fields', () => {
const now = new Date();
const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000);
const apiDto = {
leagueId: 'league-1',
races: [
{
id: 'race-1',
name: 'Test Race',
date: futureDate.toISOString(),
track: 'Spa',
car: 'Porsche 911 GT3',
sessionType: 'race',
},
],
};
const result = LeagueScheduleViewDataBuilder.build(apiDto);
expect(result.races[0].track).toBe('Spa');
expect(result.races[0].car).toBe('Porsche 911 GT3');
expect(result.races[0].sessionType).toBe('race');
});
it('should handle races at exactly the current time', () => {
const now = new Date();
const currentRaceDate = new Date(now.getTime());
const apiDto = {
leagueId: 'league-1',
races: [
{
id: 'race-1',
name: 'Current Race',
date: currentRaceDate.toISOString(),
track: 'Spa',
car: 'Porsche 911 GT3',
sessionType: 'race',
},
],
};
const result = LeagueScheduleViewDataBuilder.build(apiDto);
// Race at current time should be considered past
expect(result.races[0].isPast).toBe(true);
expect(result.races[0].isUpcoming).toBe(false);
expect(result.races[0].status).toBe('completed');
});
});
});

View File

@@ -9,7 +9,7 @@ export class LeagueScheduleViewDataBuilder {
leagueId: apiDto.leagueId,
races: apiDto.races.map((race) => {
const scheduledAt = new Date(race.date);
const isPast = scheduledAt.getTime() < now.getTime();
const isPast = scheduledAt.getTime() <= now.getTime();
const isUpcoming = !isPast;
return {

View File

@@ -0,0 +1,148 @@
import { describe, it, expect } from 'vitest';
import { LeagueSettingsViewDataBuilder } from './LeagueSettingsViewDataBuilder';
import type { LeagueSettingsApiDto } from '@/lib/types/tbd/LeagueSettingsApiDto';
describe('LeagueSettingsViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform LeagueSettingsApiDto to LeagueSettingsViewData correctly', () => {
const leagueSettingsApiDto: LeagueSettingsApiDto = {
leagueId: 'league-123',
league: {
id: 'league-123',
name: 'Test League',
description: 'Test Description',
},
config: {
maxDrivers: 32,
qualifyingFormat: 'Open',
raceLength: 30,
},
};
const result = LeagueSettingsViewDataBuilder.build(leagueSettingsApiDto);
expect(result).toEqual({
leagueId: 'league-123',
league: {
id: 'league-123',
name: 'Test League',
description: 'Test Description',
},
config: {
maxDrivers: 32,
qualifyingFormat: 'Open',
raceLength: 30,
},
});
});
it('should handle minimal configuration', () => {
const leagueSettingsApiDto: LeagueSettingsApiDto = {
leagueId: 'league-456',
league: {
id: 'league-456',
name: 'Minimal League',
description: '',
},
config: {
maxDrivers: 16,
qualifyingFormat: 'Open',
raceLength: 20,
},
};
const result = LeagueSettingsViewDataBuilder.build(leagueSettingsApiDto);
expect(result.leagueId).toBe('league-456');
expect(result.league.name).toBe('Minimal League');
expect(result.config.maxDrivers).toBe(16);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const leagueSettingsApiDto: LeagueSettingsApiDto = {
leagueId: 'league-789',
league: {
id: 'league-789',
name: 'Full League',
description: 'Full Description',
},
config: {
maxDrivers: 24,
qualifyingFormat: 'Open',
raceLength: 45,
},
};
const result = LeagueSettingsViewDataBuilder.build(leagueSettingsApiDto);
expect(result.leagueId).toBe(leagueSettingsApiDto.leagueId);
expect(result.league).toEqual(leagueSettingsApiDto.league);
expect(result.config).toEqual(leagueSettingsApiDto.config);
});
it('should not modify the input DTO', () => {
const leagueSettingsApiDto: LeagueSettingsApiDto = {
leagueId: 'league-101',
league: {
id: 'league-101',
name: 'Test League',
description: 'Test',
},
config: {
maxDrivers: 20,
qualifyingFormat: 'Open',
raceLength: 25,
},
};
const originalDto = { ...leagueSettingsApiDto };
LeagueSettingsViewDataBuilder.build(leagueSettingsApiDto);
expect(leagueSettingsApiDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle different qualifying formats', () => {
const leagueSettingsApiDto: LeagueSettingsApiDto = {
leagueId: 'league-102',
league: {
id: 'league-102',
name: 'Test League',
description: 'Test',
},
config: {
maxDrivers: 20,
qualifyingFormat: 'Closed',
raceLength: 30,
},
};
const result = LeagueSettingsViewDataBuilder.build(leagueSettingsApiDto);
expect(result.config.qualifyingFormat).toBe('Closed');
});
it('should handle large driver counts', () => {
const leagueSettingsApiDto: LeagueSettingsApiDto = {
leagueId: 'league-103',
league: {
id: 'league-103',
name: 'Test League',
description: 'Test',
},
config: {
maxDrivers: 100,
qualifyingFormat: 'Open',
raceLength: 60,
},
};
const result = LeagueSettingsViewDataBuilder.build(leagueSettingsApiDto);
expect(result.config.maxDrivers).toBe(100);
});
});
});

View File

@@ -0,0 +1,235 @@
import { describe, it, expect } from 'vitest';
import { LeagueSponsorshipsViewDataBuilder } from './LeagueSponsorshipsViewDataBuilder';
import type { LeagueSponsorshipsApiDto } from '@/lib/types/tbd/LeagueSponsorshipsApiDto';
describe('LeagueSponsorshipsViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform LeagueSponsorshipsApiDto to LeagueSponsorshipsViewData correctly', () => {
const leagueSponsorshipsApiDto: LeagueSponsorshipsApiDto = {
leagueId: 'league-123',
league: {
id: 'league-123',
name: 'Test League',
},
sponsorshipSlots: [
{
id: 'slot-1',
name: 'Primary Sponsor',
price: 1000,
status: 'available',
},
],
sponsorshipRequests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorLogo: 'logo-url',
message: 'Test message',
requestedAt: '2024-01-01T10:00:00Z',
status: 'pending',
},
],
};
const result = LeagueSponsorshipsViewDataBuilder.build(leagueSponsorshipsApiDto);
expect(result).toEqual({
leagueId: 'league-123',
activeTab: 'overview',
onTabChange: expect.any(Function),
league: {
id: 'league-123',
name: 'Test League',
},
sponsorshipSlots: [
{
id: 'slot-1',
name: 'Primary Sponsor',
price: 1000,
status: 'available',
},
],
sponsorshipRequests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorLogo: 'logo-url',
message: 'Test message',
requestedAt: '2024-01-01T10:00:00Z',
status: 'pending',
formattedRequestedAt: expect.any(String),
statusLabel: expect.any(String),
},
],
});
});
it('should handle empty sponsorship requests', () => {
const leagueSponsorshipsApiDto: LeagueSponsorshipsApiDto = {
leagueId: 'league-456',
league: {
id: 'league-456',
name: 'Test League',
},
sponsorshipSlots: [
{
id: 'slot-1',
name: 'Primary Sponsor',
price: 1000,
status: 'available',
},
],
sponsorshipRequests: [],
};
const result = LeagueSponsorshipsViewDataBuilder.build(leagueSponsorshipsApiDto);
expect(result.sponsorshipRequests).toHaveLength(0);
});
it('should handle multiple sponsorship requests', () => {
const leagueSponsorshipsApiDto: LeagueSponsorshipsApiDto = {
leagueId: 'league-789',
league: {
id: 'league-789',
name: 'Test League',
},
sponsorshipSlots: [],
sponsorshipRequests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Sponsor 1',
sponsorLogo: 'logo-1',
message: 'Message 1',
requestedAt: '2024-01-01T10:00:00Z',
status: 'pending',
},
{
id: 'request-2',
sponsorId: 'sponsor-2',
sponsorName: 'Sponsor 2',
sponsorLogo: 'logo-2',
message: 'Message 2',
requestedAt: '2024-01-02T10:00:00Z',
status: 'approved',
},
],
};
const result = LeagueSponsorshipsViewDataBuilder.build(leagueSponsorshipsApiDto);
expect(result.sponsorshipRequests).toHaveLength(2);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const leagueSponsorshipsApiDto: LeagueSponsorshipsApiDto = {
leagueId: 'league-101',
league: {
id: 'league-101',
name: 'Test League',
},
sponsorshipSlots: [
{
id: 'slot-1',
name: 'Primary Sponsor',
price: 1000,
status: 'available',
},
],
sponsorshipRequests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorLogo: 'logo-url',
message: 'Test message',
requestedAt: '2024-01-01T10:00:00Z',
status: 'pending',
},
],
};
const result = LeagueSponsorshipsViewDataBuilder.build(leagueSponsorshipsApiDto);
expect(result.leagueId).toBe(leagueSponsorshipsApiDto.leagueId);
expect(result.league).toEqual(leagueSponsorshipsApiDto.league);
expect(result.sponsorshipSlots).toEqual(leagueSponsorshipsApiDto.sponsorshipSlots);
});
it('should not modify the input DTO', () => {
const leagueSponsorshipsApiDto: LeagueSponsorshipsApiDto = {
leagueId: 'league-102',
league: {
id: 'league-102',
name: 'Test League',
},
sponsorshipSlots: [],
sponsorshipRequests: [],
};
const originalDto = { ...leagueSponsorshipsApiDto };
LeagueSponsorshipsViewDataBuilder.build(leagueSponsorshipsApiDto);
expect(leagueSponsorshipsApiDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle requests without sponsor logo', () => {
const leagueSponsorshipsApiDto: LeagueSponsorshipsApiDto = {
leagueId: 'league-103',
league: {
id: 'league-103',
name: 'Test League',
},
sponsorshipSlots: [],
sponsorshipRequests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorLogo: null,
message: 'Test message',
requestedAt: '2024-01-01T10:00:00Z',
status: 'pending',
},
],
};
const result = LeagueSponsorshipsViewDataBuilder.build(leagueSponsorshipsApiDto);
expect(result.sponsorshipRequests[0].sponsorLogoUrl).toBeNull();
});
it('should handle requests without message', () => {
const leagueSponsorshipsApiDto: LeagueSponsorshipsApiDto = {
leagueId: 'league-104',
league: {
id: 'league-104',
name: 'Test League',
},
sponsorshipSlots: [],
sponsorshipRequests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorLogo: 'logo-url',
message: null,
requestedAt: '2024-01-01T10:00:00Z',
status: 'pending',
},
],
};
const result = LeagueSponsorshipsViewDataBuilder.build(leagueSponsorshipsApiDto);
expect(result.sponsorshipRequests[0].message).toBeNull();
});
});
});

View File

@@ -0,0 +1,464 @@
import { describe, it, expect } from 'vitest';
import { LeagueStandingsViewDataBuilder } from './LeagueStandingsViewDataBuilder';
describe('LeagueStandingsViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform standings DTOs to LeagueStandingsViewData correctly', () => {
const standingsDto = {
standings: [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
name: 'Alice',
iracingId: '11111',
country: 'UK',
},
points: 1250,
position: 1,
wins: 5,
podiums: 10,
races: 15,
positionChange: 2,
lastRacePoints: 25,
droppedRaceIds: ['race-1', 'race-2'],
},
{
driverId: 'driver-2',
driver: {
id: 'driver-2',
name: 'Bob',
iracingId: '22222',
country: 'Germany',
},
points: 1100,
position: 2,
wins: 3,
podiums: 8,
races: 15,
positionChange: -1,
lastRacePoints: 15,
droppedRaceIds: [],
},
],
};
const membershipsDto = {
members: [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
name: 'Alice',
iracingId: '11111',
country: 'UK',
joinedAt: '2023-06-01T00:00:00.000Z',
},
role: 'member',
joinedAt: '2023-06-01T00:00:00.000Z',
},
{
driverId: 'driver-2',
driver: {
id: 'driver-2',
name: 'Bob',
iracingId: '22222',
country: 'Germany',
joinedAt: '2023-07-01T00:00:00.000Z',
},
role: 'member',
joinedAt: '2023-07-01T00:00:00.000Z',
},
],
};
const result = LeagueStandingsViewDataBuilder.build(
standingsDto,
membershipsDto,
'league-1',
false
);
expect(result.leagueId).toBe('league-1');
expect(result.isTeamChampionship).toBe(false);
expect(result.currentDriverId).toBeNull();
expect(result.isAdmin).toBe(false);
expect(result.standings).toHaveLength(2);
expect(result.standings[0].driverId).toBe('driver-1');
expect(result.standings[0].position).toBe(1);
expect(result.standings[0].totalPoints).toBe(1250);
expect(result.standings[0].racesFinished).toBe(15);
expect(result.standings[0].racesStarted).toBe(15);
expect(result.standings[0].avgFinish).toBeNull();
expect(result.standings[0].penaltyPoints).toBe(0);
expect(result.standings[0].bonusPoints).toBe(0);
expect(result.standings[0].positionChange).toBe(2);
expect(result.standings[0].lastRacePoints).toBe(25);
expect(result.standings[0].droppedRaceIds).toEqual(['race-1', 'race-2']);
expect(result.standings[0].wins).toBe(5);
expect(result.standings[0].podiums).toBe(10);
expect(result.standings[1].driverId).toBe('driver-2');
expect(result.standings[1].position).toBe(2);
expect(result.standings[1].totalPoints).toBe(1100);
expect(result.standings[1].racesFinished).toBe(15);
expect(result.standings[1].racesStarted).toBe(15);
expect(result.standings[1].avgFinish).toBeNull();
expect(result.standings[1].penaltyPoints).toBe(0);
expect(result.standings[1].bonusPoints).toBe(0);
expect(result.standings[1].positionChange).toBe(-1);
expect(result.standings[1].lastRacePoints).toBe(15);
expect(result.standings[1].droppedRaceIds).toEqual([]);
expect(result.standings[1].wins).toBe(3);
expect(result.standings[1].podiums).toBe(8);
expect(result.drivers).toHaveLength(2);
expect(result.drivers[0].id).toBe('driver-1');
expect(result.drivers[0].name).toBe('Alice');
expect(result.drivers[0].iracingId).toBe('11111');
expect(result.drivers[0].country).toBe('UK');
expect(result.drivers[0].avatarUrl).toBeNull();
expect(result.drivers[1].id).toBe('driver-2');
expect(result.drivers[1].name).toBe('Bob');
expect(result.drivers[1].iracingId).toBe('22222');
expect(result.drivers[1].country).toBe('Germany');
expect(result.drivers[1].avatarUrl).toBeNull();
expect(result.memberships).toHaveLength(2);
expect(result.memberships[0].driverId).toBe('driver-1');
expect(result.memberships[0].leagueId).toBe('league-1');
expect(result.memberships[0].role).toBe('member');
expect(result.memberships[0].joinedAt).toBe('2023-06-01T00:00:00.000Z');
expect(result.memberships[0].status).toBe('active');
expect(result.memberships[1].driverId).toBe('driver-2');
expect(result.memberships[1].leagueId).toBe('league-1');
expect(result.memberships[1].role).toBe('member');
expect(result.memberships[1].joinedAt).toBe('2023-07-01T00:00:00.000Z');
expect(result.memberships[1].status).toBe('active');
});
it('should handle empty standings and memberships', () => {
const standingsDto = {
standings: [],
};
const membershipsDto = {
members: [],
};
const result = LeagueStandingsViewDataBuilder.build(
standingsDto,
membershipsDto,
'league-1',
false
);
expect(result.standings).toHaveLength(0);
expect(result.drivers).toHaveLength(0);
expect(result.memberships).toHaveLength(0);
});
it('should handle team championship mode', () => {
const standingsDto = {
standings: [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
name: 'Alice',
iracingId: '11111',
country: 'UK',
},
points: 1250,
position: 1,
wins: 5,
podiums: 10,
races: 15,
positionChange: 2,
lastRacePoints: 25,
droppedRaceIds: [],
},
],
};
const membershipsDto = {
members: [],
};
const result = LeagueStandingsViewDataBuilder.build(
standingsDto,
membershipsDto,
'league-1',
true
);
expect(result.isTeamChampionship).toBe(true);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const standingsDto = {
standings: [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
name: 'Alice',
iracingId: '11111',
country: 'UK',
},
points: 1250,
position: 1,
wins: 5,
podiums: 10,
races: 15,
positionChange: 2,
lastRacePoints: 25,
droppedRaceIds: ['race-1'],
},
],
};
const membershipsDto = {
members: [],
};
const result = LeagueStandingsViewDataBuilder.build(
standingsDto,
membershipsDto,
'league-1',
false
);
expect(result.standings[0].driverId).toBe(standingsDto.standings[0].driverId);
expect(result.standings[0].position).toBe(standingsDto.standings[0].position);
expect(result.standings[0].totalPoints).toBe(standingsDto.standings[0].points);
expect(result.standings[0].racesFinished).toBe(standingsDto.standings[0].races);
expect(result.standings[0].racesStarted).toBe(standingsDto.standings[0].races);
expect(result.standings[0].positionChange).toBe(standingsDto.standings[0].positionChange);
expect(result.standings[0].lastRacePoints).toBe(standingsDto.standings[0].lastRacePoints);
expect(result.standings[0].droppedRaceIds).toEqual(standingsDto.standings[0].droppedRaceIds);
expect(result.standings[0].wins).toBe(standingsDto.standings[0].wins);
expect(result.standings[0].podiums).toBe(standingsDto.standings[0].podiums);
expect(result.drivers[0].id).toBe(standingsDto.standings[0].driver.id);
expect(result.drivers[0].name).toBe(standingsDto.standings[0].driver.name);
expect(result.drivers[0].iracingId).toBe(standingsDto.standings[0].driver.iracingId);
expect(result.drivers[0].country).toBe(standingsDto.standings[0].driver.country);
});
it('should not modify the input DTOs', () => {
const standingsDto = {
standings: [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
name: 'Alice',
iracingId: '11111',
country: 'UK',
},
points: 1250,
position: 1,
wins: 5,
podiums: 10,
races: 15,
positionChange: 2,
lastRacePoints: 25,
droppedRaceIds: ['race-1'],
},
],
};
const membershipsDto = {
members: [],
};
const originalStandings = JSON.parse(JSON.stringify(standingsDto));
const originalMemberships = JSON.parse(JSON.stringify(membershipsDto));
LeagueStandingsViewDataBuilder.build(
standingsDto,
membershipsDto,
'league-1',
false
);
expect(standingsDto).toEqual(originalStandings);
expect(membershipsDto).toEqual(originalMemberships);
});
});
describe('edge cases', () => {
it('should handle standings with missing optional fields', () => {
const standingsDto = {
standings: [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
name: 'Alice',
iracingId: '11111',
country: 'UK',
},
points: 1250,
position: 1,
wins: 5,
podiums: 10,
races: 15,
},
],
};
const membershipsDto = {
members: [],
};
const result = LeagueStandingsViewDataBuilder.build(
standingsDto,
membershipsDto,
'league-1',
false
);
expect(result.standings[0].positionChange).toBe(0);
expect(result.standings[0].lastRacePoints).toBe(0);
expect(result.standings[0].droppedRaceIds).toEqual([]);
});
it('should handle standings with missing driver field', () => {
const standingsDto = {
standings: [
{
driverId: 'driver-1',
driver: undefined as any,
points: 1250,
position: 1,
wins: 5,
podiums: 10,
races: 15,
positionChange: 2,
lastRacePoints: 25,
droppedRaceIds: [],
},
],
};
const membershipsDto = {
members: [],
};
const result = LeagueStandingsViewDataBuilder.build(
standingsDto,
membershipsDto,
'league-1',
false
);
expect(result.drivers).toHaveLength(0);
});
it('should handle duplicate drivers in standings', () => {
const standingsDto = {
standings: [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
name: 'Alice',
iracingId: '11111',
country: 'UK',
},
points: 1250,
position: 1,
wins: 5,
podiums: 10,
races: 15,
positionChange: 2,
lastRacePoints: 25,
droppedRaceIds: [],
},
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
name: 'Alice',
iracingId: '11111',
country: 'UK',
},
points: 1100,
position: 2,
wins: 3,
podiums: 8,
races: 15,
positionChange: -1,
lastRacePoints: 15,
droppedRaceIds: [],
},
],
};
const membershipsDto = {
members: [],
};
const result = LeagueStandingsViewDataBuilder.build(
standingsDto,
membershipsDto,
'league-1',
false
);
// Should only have one driver entry
expect(result.drivers).toHaveLength(1);
expect(result.drivers[0].id).toBe('driver-1');
});
it('should handle members with different roles', () => {
const standingsDto = {
standings: [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
name: 'Alice',
iracingId: '11111',
country: 'UK',
},
points: 1250,
position: 1,
wins: 5,
podiums: 10,
races: 15,
positionChange: 2,
lastRacePoints: 25,
droppedRaceIds: [],
},
],
};
const membershipsDto = {
members: [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
name: 'Alice',
iracingId: '11111',
country: 'UK',
joinedAt: '2023-06-01T00:00:00.000Z',
},
role: 'admin',
joinedAt: '2023-06-01T00:00:00.000Z',
},
],
};
const result = LeagueStandingsViewDataBuilder.build(
standingsDto,
membershipsDto,
'league-1',
false
);
expect(result.memberships[0].role).toBe('admin');
});
});
});

View File

@@ -0,0 +1,213 @@
import { describe, it, expect } from 'vitest';
import { LeagueWalletViewDataBuilder } from './LeagueWalletViewDataBuilder';
import type { LeagueWalletApiDto } from '@/lib/types/tbd/LeagueWalletApiDto';
describe('LeagueWalletViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform LeagueWalletApiDto to LeagueWalletViewData correctly', () => {
const leagueWalletApiDto: LeagueWalletApiDto = {
leagueId: 'league-123',
balance: 5000,
currency: 'USD',
transactions: [
{
id: 'txn-1',
amount: 1000,
status: 'completed',
createdAt: '2024-01-01T10:00:00Z',
description: 'Sponsorship payment',
},
],
};
const result = LeagueWalletViewDataBuilder.build(leagueWalletApiDto);
expect(result).toEqual({
leagueId: 'league-123',
balance: 5000,
formattedBalance: expect.any(String),
totalRevenue: 5000,
formattedTotalRevenue: expect.any(String),
totalFees: 0,
formattedTotalFees: expect.any(String),
pendingPayouts: 0,
formattedPendingPayouts: expect.any(String),
currency: 'USD',
transactions: [
{
id: 'txn-1',
amount: 1000,
status: 'completed',
createdAt: '2024-01-01T10:00:00Z',
description: 'Sponsorship payment',
formattedAmount: expect.any(String),
amountColor: 'green',
formattedDate: expect.any(String),
statusColor: 'green',
typeColor: 'blue',
},
],
});
});
it('should handle empty transactions', () => {
const leagueWalletApiDto: LeagueWalletApiDto = {
leagueId: 'league-456',
balance: 0,
currency: 'USD',
transactions: [],
};
const result = LeagueWalletViewDataBuilder.build(leagueWalletApiDto);
expect(result.transactions).toHaveLength(0);
expect(result.balance).toBe(0);
});
it('should handle multiple transactions', () => {
const leagueWalletApiDto: LeagueWalletApiDto = {
leagueId: 'league-789',
balance: 10000,
currency: 'USD',
transactions: [
{
id: 'txn-1',
amount: 5000,
status: 'completed',
createdAt: '2024-01-01T10:00:00Z',
description: 'Sponsorship payment',
},
{
id: 'txn-2',
amount: -1000,
status: 'completed',
createdAt: '2024-01-02T10:00:00Z',
description: 'Payout',
},
],
};
const result = LeagueWalletViewDataBuilder.build(leagueWalletApiDto);
expect(result.transactions).toHaveLength(2);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const leagueWalletApiDto: LeagueWalletApiDto = {
leagueId: 'league-101',
balance: 7500,
currency: 'EUR',
transactions: [
{
id: 'txn-1',
amount: 2500,
status: 'completed',
createdAt: '2024-01-01T10:00:00Z',
description: 'Test transaction',
},
],
};
const result = LeagueWalletViewDataBuilder.build(leagueWalletApiDto);
expect(result.leagueId).toBe(leagueWalletApiDto.leagueId);
expect(result.balance).toBe(leagueWalletApiDto.balance);
expect(result.currency).toBe(leagueWalletApiDto.currency);
});
it('should not modify the input DTO', () => {
const leagueWalletApiDto: LeagueWalletApiDto = {
leagueId: 'league-102',
balance: 5000,
currency: 'USD',
transactions: [],
};
const originalDto = { ...leagueWalletApiDto };
LeagueWalletViewDataBuilder.build(leagueWalletApiDto);
expect(leagueWalletApiDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle negative balance', () => {
const leagueWalletApiDto: LeagueWalletApiDto = {
leagueId: 'league-103',
balance: -500,
currency: 'USD',
transactions: [
{
id: 'txn-1',
amount: -500,
status: 'completed',
createdAt: '2024-01-01T10:00:00Z',
description: 'Overdraft',
},
],
};
const result = LeagueWalletViewDataBuilder.build(leagueWalletApiDto);
expect(result.balance).toBe(-500);
expect(result.transactions[0].amountColor).toBe('red');
});
it('should handle pending transactions', () => {
const leagueWalletApiDto: LeagueWalletApiDto = {
leagueId: 'league-104',
balance: 1000,
currency: 'USD',
transactions: [
{
id: 'txn-1',
amount: 500,
status: 'pending',
createdAt: '2024-01-01T10:00:00Z',
description: 'Pending payment',
},
],
};
const result = LeagueWalletViewDataBuilder.build(leagueWalletApiDto);
expect(result.transactions[0].statusColor).toBe('yellow');
});
it('should handle failed transactions', () => {
const leagueWalletApiDto: LeagueWalletApiDto = {
leagueId: 'league-105',
balance: 1000,
currency: 'USD',
transactions: [
{
id: 'txn-1',
amount: 500,
status: 'failed',
createdAt: '2024-01-01T10:00:00Z',
description: 'Failed payment',
},
],
};
const result = LeagueWalletViewDataBuilder.build(leagueWalletApiDto);
expect(result.transactions[0].statusColor).toBe('red');
});
it('should handle different currencies', () => {
const leagueWalletApiDto: LeagueWalletApiDto = {
leagueId: 'league-106',
balance: 1000,
currency: 'EUR',
transactions: [],
};
const result = LeagueWalletViewDataBuilder.build(leagueWalletApiDto);
expect(result.currency).toBe('EUR');
});
});
});

View File

@@ -0,0 +1,351 @@
import { describe, it, expect } from 'vitest';
import { LeaguesViewDataBuilder } from './LeaguesViewDataBuilder';
import type { AllLeaguesWithCapacityAndScoringDTO } from '@/lib/types/generated/AllLeaguesWithCapacityAndScoringDTO';
describe('LeaguesViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform AllLeaguesWithCapacityAndScoringDTO to LeaguesViewData correctly', () => {
const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = {
leagues: [
{
id: 'league-1',
name: 'Pro League',
description: 'A competitive league for experienced drivers',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00.000Z',
settings: {
maxDrivers: 32,
qualifyingFormat: 'Solo • 32 max',
},
usedSlots: 25,
category: 'competitive',
scoring: {
gameId: 'game-1',
gameName: 'iRacing',
primaryChampionshipType: 'Single Championship',
scoringPresetId: 'preset-1',
scoringPresetName: 'Standard',
dropPolicySummary: 'Drop 2 worst races',
scoringPatternSummary: 'Points based on finish position',
},
timingSummary: 'Weekly races on Sundays',
logoUrl: 'https://example.com/logo.png',
pendingJoinRequestsCount: 3,
pendingProtestsCount: 1,
walletBalance: 1000,
},
{
id: 'league-2',
name: 'Rookie League',
description: null,
ownerId: 'owner-2',
createdAt: '2024-02-01T00:00:00.000Z',
settings: {
maxDrivers: 16,
qualifyingFormat: 'Solo • 16 max',
},
usedSlots: 10,
category: 'rookie',
scoring: {
gameId: 'game-1',
gameName: 'iRacing',
primaryChampionshipType: 'Single Championship',
scoringPresetId: 'preset-2',
scoringPresetName: 'Rookie',
dropPolicySummary: 'No drops',
scoringPatternSummary: 'Points based on finish position',
},
timingSummary: 'Bi-weekly races',
logoUrl: null,
pendingJoinRequestsCount: 0,
pendingProtestsCount: 0,
walletBalance: 0,
},
],
totalCount: 2,
};
const result = LeaguesViewDataBuilder.build(leaguesDTO);
expect(result.leagues).toHaveLength(2);
expect(result.leagues[0]).toEqual({
id: 'league-1',
name: 'Pro League',
description: 'A competitive league for experienced drivers',
logoUrl: 'https://example.com/logo.png',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00.000Z',
maxDrivers: 32,
usedDriverSlots: 25,
activeDriversCount: undefined,
nextRaceAt: undefined,
maxTeams: undefined,
usedTeamSlots: undefined,
structureSummary: 'Solo • 32 max',
timingSummary: 'Weekly races on Sundays',
category: 'competitive',
scoring: {
gameId: 'game-1',
gameName: 'iRacing',
primaryChampionshipType: 'Single Championship',
scoringPresetId: 'preset-1',
scoringPresetName: 'Standard',
dropPolicySummary: 'Drop 2 worst races',
scoringPatternSummary: 'Points based on finish position',
},
});
expect(result.leagues[1]).toEqual({
id: 'league-2',
name: 'Rookie League',
description: null,
logoUrl: null,
ownerId: 'owner-2',
createdAt: '2024-02-01T00:00:00.000Z',
maxDrivers: 16,
usedDriverSlots: 10,
activeDriversCount: undefined,
nextRaceAt: undefined,
maxTeams: undefined,
usedTeamSlots: undefined,
structureSummary: 'Solo • 16 max',
timingSummary: 'Bi-weekly races',
category: 'rookie',
scoring: {
gameId: 'game-1',
gameName: 'iRacing',
primaryChampionshipType: 'Single Championship',
scoringPresetId: 'preset-2',
scoringPresetName: 'Rookie',
dropPolicySummary: 'No drops',
scoringPatternSummary: 'Points based on finish position',
},
});
});
it('should handle empty leagues list', () => {
const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = {
leagues: [],
totalCount: 0,
};
const result = LeaguesViewDataBuilder.build(leaguesDTO);
expect(result.leagues).toHaveLength(0);
});
it('should handle leagues with missing optional fields', () => {
const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = {
leagues: [
{
id: 'league-1',
name: 'Minimal League',
description: '',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00.000Z',
settings: {
maxDrivers: 20,
},
usedSlots: 5,
},
],
totalCount: 1,
};
const result = LeaguesViewDataBuilder.build(leaguesDTO);
expect(result.leagues[0].description).toBe(null);
expect(result.leagues[0].logoUrl).toBe(null);
expect(result.leagues[0].category).toBe(null);
expect(result.leagues[0].scoring).toBeUndefined();
expect(result.leagues[0].timingSummary).toBe('');
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = {
leagues: [
{
id: 'league-1',
name: 'Test League',
description: 'Test description',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00.000Z',
settings: {
maxDrivers: 32,
qualifyingFormat: 'Solo • 32 max',
},
usedSlots: 20,
category: 'test',
scoring: {
gameId: 'game-1',
gameName: 'Test Game',
primaryChampionshipType: 'Test Type',
scoringPresetId: 'preset-1',
scoringPresetName: 'Test Preset',
dropPolicySummary: 'Test drop policy',
scoringPatternSummary: 'Test pattern',
},
timingSummary: 'Test timing',
logoUrl: 'https://example.com/test.png',
pendingJoinRequestsCount: 5,
pendingProtestsCount: 2,
walletBalance: 500,
},
],
totalCount: 1,
};
const result = LeaguesViewDataBuilder.build(leaguesDTO);
expect(result.leagues[0].id).toBe(leaguesDTO.leagues[0].id);
expect(result.leagues[0].name).toBe(leaguesDTO.leagues[0].name);
expect(result.leagues[0].description).toBe(leaguesDTO.leagues[0].description);
expect(result.leagues[0].logoUrl).toBe(leaguesDTO.leagues[0].logoUrl);
expect(result.leagues[0].ownerId).toBe(leaguesDTO.leagues[0].ownerId);
expect(result.leagues[0].createdAt).toBe(leaguesDTO.leagues[0].createdAt);
expect(result.leagues[0].maxDrivers).toBe(leaguesDTO.leagues[0].settings.maxDrivers);
expect(result.leagues[0].usedDriverSlots).toBe(leaguesDTO.leagues[0].usedSlots);
expect(result.leagues[0].structureSummary).toBe(leaguesDTO.leagues[0].settings.qualifyingFormat);
expect(result.leagues[0].timingSummary).toBe(leaguesDTO.leagues[0].timingSummary);
expect(result.leagues[0].category).toBe(leaguesDTO.leagues[0].category);
expect(result.leagues[0].scoring).toEqual(leaguesDTO.leagues[0].scoring);
});
it('should not modify the input DTO', () => {
const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = {
leagues: [
{
id: 'league-1',
name: 'Test League',
description: 'Test description',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00.000Z',
settings: {
maxDrivers: 32,
qualifyingFormat: 'Solo • 32 max',
},
usedSlots: 20,
category: 'test',
scoring: {
gameId: 'game-1',
gameName: 'Test Game',
primaryChampionshipType: 'Test Type',
scoringPresetId: 'preset-1',
scoringPresetName: 'Test Preset',
dropPolicySummary: 'Test drop policy',
scoringPatternSummary: 'Test pattern',
},
timingSummary: 'Test timing',
logoUrl: 'https://example.com/test.png',
pendingJoinRequestsCount: 5,
pendingProtestsCount: 2,
walletBalance: 500,
},
],
totalCount: 1,
};
const originalDTO = JSON.parse(JSON.stringify(leaguesDTO));
LeaguesViewDataBuilder.build(leaguesDTO);
expect(leaguesDTO).toEqual(originalDTO);
});
});
describe('edge cases', () => {
it('should handle leagues with very long descriptions', () => {
const longDescription = 'A'.repeat(1000);
const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = {
leagues: [
{
id: 'league-1',
name: 'Test League',
description: longDescription,
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00.000Z',
settings: {
maxDrivers: 32,
},
usedSlots: 20,
},
],
totalCount: 1,
};
const result = LeaguesViewDataBuilder.build(leaguesDTO);
expect(result.leagues[0].description).toBe(longDescription);
});
it('should handle leagues with special characters in name', () => {
const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = {
leagues: [
{
id: 'league-1',
name: 'League & Co. (2024)',
description: 'Test league',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00.000Z',
settings: {
maxDrivers: 32,
},
usedSlots: 20,
},
],
totalCount: 1,
};
const result = LeaguesViewDataBuilder.build(leaguesDTO);
expect(result.leagues[0].name).toBe('League & Co. (2024)');
});
it('should handle leagues with zero used slots', () => {
const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = {
leagues: [
{
id: 'league-1',
name: 'Empty League',
description: 'No members yet',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00.000Z',
settings: {
maxDrivers: 32,
},
usedSlots: 0,
},
],
totalCount: 1,
};
const result = LeaguesViewDataBuilder.build(leaguesDTO);
expect(result.leagues[0].usedDriverSlots).toBe(0);
});
it('should handle leagues with maximum capacity', () => {
const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = {
leagues: [
{
id: 'league-1',
name: 'Full League',
description: 'At maximum capacity',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00.000Z',
settings: {
maxDrivers: 32,
},
usedSlots: 32,
},
],
totalCount: 1,
};
const result = LeaguesViewDataBuilder.build(leaguesDTO);
expect(result.leagues[0].usedDriverSlots).toBe(32);
expect(result.leagues[0].maxDrivers).toBe(32);
});
});
});

View File

@@ -0,0 +1,205 @@
import { describe, it, expect } from 'vitest';
import { LoginViewDataBuilder } from './LoginViewDataBuilder';
import type { LoginPageDTO } from '@/lib/services/auth/types/LoginPageDTO';
describe('LoginViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform LoginPageDTO to LoginViewData correctly', () => {
const loginPageDTO: LoginPageDTO = {
returnTo: '/dashboard',
hasInsufficientPermissions: false,
};
const result = LoginViewDataBuilder.build(loginPageDTO);
expect(result).toEqual({
returnTo: '/dashboard',
hasInsufficientPermissions: false,
showPassword: false,
showErrorDetails: false,
formState: {
fields: {
email: { value: '', error: undefined, touched: false, validating: false },
password: { value: '', error: undefined, touched: false, validating: false },
rememberMe: { value: false, error: undefined, touched: false, validating: false },
},
isValid: true,
isSubmitting: false,
submitError: undefined,
submitCount: 0,
},
isSubmitting: false,
submitError: undefined,
});
});
it('should handle insufficient permissions flag correctly', () => {
const loginPageDTO: LoginPageDTO = {
returnTo: '/admin',
hasInsufficientPermissions: true,
};
const result = LoginViewDataBuilder.build(loginPageDTO);
expect(result.hasInsufficientPermissions).toBe(true);
expect(result.returnTo).toBe('/admin');
});
it('should handle empty returnTo path', () => {
const loginPageDTO: LoginPageDTO = {
returnTo: '',
hasInsufficientPermissions: false,
};
const result = LoginViewDataBuilder.build(loginPageDTO);
expect(result.returnTo).toBe('');
expect(result.hasInsufficientPermissions).toBe(false);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const loginPageDTO: LoginPageDTO = {
returnTo: '/dashboard',
hasInsufficientPermissions: false,
};
const result = LoginViewDataBuilder.build(loginPageDTO);
expect(result.returnTo).toBe(loginPageDTO.returnTo);
expect(result.hasInsufficientPermissions).toBe(loginPageDTO.hasInsufficientPermissions);
});
it('should not modify the input DTO', () => {
const loginPageDTO: LoginPageDTO = {
returnTo: '/dashboard',
hasInsufficientPermissions: false,
};
const originalDTO = { ...loginPageDTO };
LoginViewDataBuilder.build(loginPageDTO);
expect(loginPageDTO).toEqual(originalDTO);
});
it('should initialize form fields with default values', () => {
const loginPageDTO: LoginPageDTO = {
returnTo: '/dashboard',
hasInsufficientPermissions: false,
};
const result = LoginViewDataBuilder.build(loginPageDTO);
expect(result.formState.fields.email.value).toBe('');
expect(result.formState.fields.email.error).toBeUndefined();
expect(result.formState.fields.email.touched).toBe(false);
expect(result.formState.fields.email.validating).toBe(false);
expect(result.formState.fields.password.value).toBe('');
expect(result.formState.fields.password.error).toBeUndefined();
expect(result.formState.fields.password.touched).toBe(false);
expect(result.formState.fields.password.validating).toBe(false);
expect(result.formState.fields.rememberMe.value).toBe(false);
expect(result.formState.fields.rememberMe.error).toBeUndefined();
expect(result.formState.fields.rememberMe.touched).toBe(false);
expect(result.formState.fields.rememberMe.validating).toBe(false);
});
it('should initialize form state with default values', () => {
const loginPageDTO: LoginPageDTO = {
returnTo: '/dashboard',
hasInsufficientPermissions: false,
};
const result = LoginViewDataBuilder.build(loginPageDTO);
expect(result.formState.isValid).toBe(true);
expect(result.formState.isSubmitting).toBe(false);
expect(result.formState.submitError).toBeUndefined();
expect(result.formState.submitCount).toBe(0);
});
it('should initialize UI state flags correctly', () => {
const loginPageDTO: LoginPageDTO = {
returnTo: '/dashboard',
hasInsufficientPermissions: false,
};
const result = LoginViewDataBuilder.build(loginPageDTO);
expect(result.showPassword).toBe(false);
expect(result.showErrorDetails).toBe(false);
expect(result.isSubmitting).toBe(false);
expect(result.submitError).toBeUndefined();
});
});
describe('edge cases', () => {
it('should handle special characters in returnTo path', () => {
const loginPageDTO: LoginPageDTO = {
returnTo: '/dashboard?param=value&other=test',
hasInsufficientPermissions: false,
};
const result = LoginViewDataBuilder.build(loginPageDTO);
expect(result.returnTo).toBe('/dashboard?param=value&other=test');
});
it('should handle returnTo with hash fragment', () => {
const loginPageDTO: LoginPageDTO = {
returnTo: '/dashboard#section',
hasInsufficientPermissions: false,
};
const result = LoginViewDataBuilder.build(loginPageDTO);
expect(result.returnTo).toBe('/dashboard#section');
});
it('should handle returnTo with encoded characters', () => {
const loginPageDTO: LoginPageDTO = {
returnTo: '/dashboard?redirect=%2Fadmin',
hasInsufficientPermissions: false,
};
const result = LoginViewDataBuilder.build(loginPageDTO);
expect(result.returnTo).toBe('/dashboard?redirect=%2Fadmin');
});
});
describe('form state structure', () => {
it('should have all required form fields', () => {
const loginPageDTO: LoginPageDTO = {
returnTo: '/dashboard',
hasInsufficientPermissions: false,
};
const result = LoginViewDataBuilder.build(loginPageDTO);
expect(result.formState.fields).toHaveProperty('email');
expect(result.formState.fields).toHaveProperty('password');
expect(result.formState.fields).toHaveProperty('rememberMe');
});
it('should have consistent field state structure', () => {
const loginPageDTO: LoginPageDTO = {
returnTo: '/dashboard',
hasInsufficientPermissions: false,
};
const result = LoginViewDataBuilder.build(loginPageDTO);
const fields = result.formState.fields;
Object.values(fields).forEach((field) => {
expect(field).toHaveProperty('value');
expect(field).toHaveProperty('error');
expect(field).toHaveProperty('touched');
expect(field).toHaveProperty('validating');
});
});
});
});

View File

@@ -0,0 +1,122 @@
import { describe, it, expect } from 'vitest';
import { OnboardingPageViewDataBuilder } from './OnboardingPageViewDataBuilder';
describe('OnboardingPageViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform driver data to ViewData correctly when driver exists', () => {
const apiDto = { id: 'driver-123', name: 'Test Driver' };
const result = OnboardingPageViewDataBuilder.build(apiDto);
expect(result).toEqual({
isAlreadyOnboarded: true,
});
});
it('should handle empty object as driver data', () => {
const apiDto = {};
const result = OnboardingPageViewDataBuilder.build(apiDto);
expect(result).toEqual({
isAlreadyOnboarded: true,
});
});
it('should handle null driver data', () => {
const apiDto = null;
const result = OnboardingPageViewDataBuilder.build(apiDto);
expect(result).toEqual({
isAlreadyOnboarded: false,
});
});
it('should handle undefined driver data', () => {
const apiDto = undefined;
const result = OnboardingPageViewDataBuilder.build(apiDto);
expect(result).toEqual({
isAlreadyOnboarded: false,
});
});
});
describe('data transformation', () => {
it('should preserve all driver data fields in the output', () => {
const apiDto = {
id: 'driver-123',
name: 'Test Driver',
email: 'test@example.com',
createdAt: '2024-01-01T00:00:00.000Z',
};
const result = OnboardingPageViewDataBuilder.build(apiDto);
expect(result.isAlreadyOnboarded).toBe(true);
});
it('should not modify the input driver data', () => {
const apiDto = { id: 'driver-123', name: 'Test Driver' };
const originalDto = { ...apiDto };
OnboardingPageViewDataBuilder.build(apiDto);
expect(apiDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle empty string as driver data', () => {
const apiDto = '';
const result = OnboardingPageViewDataBuilder.build(apiDto);
expect(result).toEqual({
isAlreadyOnboarded: false,
});
});
it('should handle zero as driver data', () => {
const apiDto = 0;
const result = OnboardingPageViewDataBuilder.build(apiDto);
expect(result).toEqual({
isAlreadyOnboarded: false,
});
});
it('should handle false as driver data', () => {
const apiDto = false;
const result = OnboardingPageViewDataBuilder.build(apiDto);
expect(result).toEqual({
isAlreadyOnboarded: false,
});
});
it('should handle array as driver data', () => {
const apiDto = ['driver-123'];
const result = OnboardingPageViewDataBuilder.build(apiDto);
expect(result).toEqual({
isAlreadyOnboarded: true,
});
});
it('should handle function as driver data', () => {
const apiDto = () => {};
const result = OnboardingPageViewDataBuilder.build(apiDto);
expect(result).toEqual({
isAlreadyOnboarded: true,
});
});
});
});

View File

@@ -0,0 +1,151 @@
import { describe, it, expect } from 'vitest';
import { OnboardingViewDataBuilder } from './OnboardingViewDataBuilder';
import { Result } from '@/lib/contracts/Result';
describe('OnboardingViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform successful onboarding check to ViewData correctly', () => {
const apiDto: Result<{ isAlreadyOnboarded: boolean }, any> = Result.ok({
isAlreadyOnboarded: false,
});
const result = OnboardingViewDataBuilder.build(apiDto);
expect(result.isOk()).toBe(true);
expect(result.unwrap()).toEqual({
isAlreadyOnboarded: false,
});
});
it('should handle already onboarded user correctly', () => {
const apiDto: Result<{ isAlreadyOnboarded: boolean }, any> = Result.ok({
isAlreadyOnboarded: true,
});
const result = OnboardingViewDataBuilder.build(apiDto);
expect(result.isOk()).toBe(true);
expect(result.unwrap()).toEqual({
isAlreadyOnboarded: true,
});
});
it('should handle missing isAlreadyOnboarded field with default false', () => {
const apiDto: Result<{ isAlreadyOnboarded?: boolean }, any> = Result.ok({});
const result = OnboardingViewDataBuilder.build(apiDto);
expect(result.isOk()).toBe(true);
expect(result.unwrap()).toEqual({
isAlreadyOnboarded: false,
});
});
});
describe('error handling', () => {
it('should propagate unauthorized error', () => {
const apiDto: Result<{ isAlreadyOnboarded: boolean }, any> = Result.err('unauthorized');
const result = OnboardingViewDataBuilder.build(apiDto);
expect(result.isErr()).toBe(true);
expect(result.getError()).toBe('unauthorized');
});
it('should propagate notFound error', () => {
const apiDto: Result<{ isAlreadyOnboarded: boolean }, any> = Result.err('notFound');
const result = OnboardingViewDataBuilder.build(apiDto);
expect(result.isErr()).toBe(true);
expect(result.getError()).toBe('notFound');
});
it('should propagate serverError', () => {
const apiDto: Result<{ isAlreadyOnboarded: boolean }, any> = Result.err('serverError');
const result = OnboardingViewDataBuilder.build(apiDto);
expect(result.isErr()).toBe(true);
expect(result.getError()).toBe('serverError');
});
it('should propagate networkError', () => {
const apiDto: Result<{ isAlreadyOnboarded: boolean }, any> = Result.err('networkError');
const result = OnboardingViewDataBuilder.build(apiDto);
expect(result.isErr()).toBe(true);
expect(result.getError()).toBe('networkError');
});
it('should propagate validationError', () => {
const apiDto: Result<{ isAlreadyOnboarded: boolean }, any> = Result.err('validationError');
const result = OnboardingViewDataBuilder.build(apiDto);
expect(result.isErr()).toBe(true);
expect(result.getError()).toBe('validationError');
});
it('should propagate unknown error', () => {
const apiDto: Result<{ isAlreadyOnboarded: boolean }, any> = Result.err('unknown');
const result = OnboardingViewDataBuilder.build(apiDto);
expect(result.isErr()).toBe(true);
expect(result.getError()).toBe('unknown');
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const apiDto: Result<{ isAlreadyOnboarded: boolean }, any> = Result.ok({
isAlreadyOnboarded: false,
});
const result = OnboardingViewDataBuilder.build(apiDto);
expect(result.unwrap().isAlreadyOnboarded).toBe(false);
});
it('should not modify the input DTO', () => {
const apiDto: Result<{ isAlreadyOnboarded: boolean }, any> = Result.ok({
isAlreadyOnboarded: false,
});
const originalDto = { ...apiDto.unwrap() };
OnboardingViewDataBuilder.build(apiDto);
expect(apiDto.unwrap()).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle null isAlreadyOnboarded as false', () => {
const apiDto: Result<{ isAlreadyOnboarded: boolean | null }, any> = Result.ok({
isAlreadyOnboarded: null,
});
const result = OnboardingViewDataBuilder.build(apiDto);
expect(result.isOk()).toBe(true);
expect(result.unwrap()).toEqual({
isAlreadyOnboarded: false,
});
});
it('should handle undefined isAlreadyOnboarded as false', () => {
const apiDto: Result<{ isAlreadyOnboarded: boolean | undefined }, any> = Result.ok({
isAlreadyOnboarded: undefined,
});
const result = OnboardingViewDataBuilder.build(apiDto);
expect(result.isOk()).toBe(true);
expect(result.unwrap()).toEqual({
isAlreadyOnboarded: false,
});
});
});
});

View File

@@ -0,0 +1,243 @@
import { describe, it, expect } from 'vitest';
import { ProfileLeaguesViewDataBuilder } from './ProfileLeaguesViewDataBuilder';
describe('ProfileLeaguesViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform ProfileLeaguesPageDto to ProfileLeaguesViewData correctly', () => {
const profileLeaguesPageDto = {
ownedLeagues: [
{
leagueId: 'league-1',
name: 'Owned League',
description: 'Test Description',
membershipRole: 'owner' as const,
},
],
memberLeagues: [
{
leagueId: 'league-2',
name: 'Member League',
description: 'Test Description',
membershipRole: 'member' as const,
},
],
};
const result = ProfileLeaguesViewDataBuilder.build(profileLeaguesPageDto);
expect(result).toEqual({
ownedLeagues: [
{
leagueId: 'league-1',
name: 'Owned League',
description: 'Test Description',
membershipRole: 'owner',
},
],
memberLeagues: [
{
leagueId: 'league-2',
name: 'Member League',
description: 'Test Description',
membershipRole: 'member',
},
],
});
});
it('should handle empty owned leagues', () => {
const profileLeaguesPageDto = {
ownedLeagues: [],
memberLeagues: [
{
leagueId: 'league-1',
name: 'Member League',
description: 'Test Description',
membershipRole: 'member' as const,
},
],
};
const result = ProfileLeaguesViewDataBuilder.build(profileLeaguesPageDto);
expect(result.ownedLeagues).toHaveLength(0);
expect(result.memberLeagues).toHaveLength(1);
});
it('should handle empty member leagues', () => {
const profileLeaguesPageDto = {
ownedLeagues: [
{
leagueId: 'league-1',
name: 'Owned League',
description: 'Test Description',
membershipRole: 'owner' as const,
},
],
memberLeagues: [],
};
const result = ProfileLeaguesViewDataBuilder.build(profileLeaguesPageDto);
expect(result.ownedLeagues).toHaveLength(1);
expect(result.memberLeagues).toHaveLength(0);
});
it('should handle multiple leagues in both arrays', () => {
const profileLeaguesPageDto = {
ownedLeagues: [
{
leagueId: 'league-1',
name: 'Owned League 1',
description: 'Description 1',
membershipRole: 'owner' as const,
},
{
leagueId: 'league-2',
name: 'Owned League 2',
description: 'Description 2',
membershipRole: 'admin' as const,
},
],
memberLeagues: [
{
leagueId: 'league-3',
name: 'Member League 1',
description: 'Description 3',
membershipRole: 'member' as const,
},
{
leagueId: 'league-4',
name: 'Member League 2',
description: 'Description 4',
membershipRole: 'steward' as const,
},
],
};
const result = ProfileLeaguesViewDataBuilder.build(profileLeaguesPageDto);
expect(result.ownedLeagues).toHaveLength(2);
expect(result.memberLeagues).toHaveLength(2);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const profileLeaguesPageDto = {
ownedLeagues: [
{
leagueId: 'league-1',
name: 'Test League',
description: 'Test Description',
membershipRole: 'owner' as const,
},
],
memberLeagues: [
{
leagueId: 'league-2',
name: 'Test League 2',
description: 'Test Description 2',
membershipRole: 'member' as const,
},
],
};
const result = ProfileLeaguesViewDataBuilder.build(profileLeaguesPageDto);
expect(result.ownedLeagues[0].leagueId).toBe(profileLeaguesPageDto.ownedLeagues[0].leagueId);
expect(result.ownedLeagues[0].name).toBe(profileLeaguesPageDto.ownedLeagues[0].name);
expect(result.ownedLeagues[0].description).toBe(profileLeaguesPageDto.ownedLeagues[0].description);
expect(result.ownedLeagues[0].membershipRole).toBe(profileLeaguesPageDto.ownedLeagues[0].membershipRole);
expect(result.memberLeagues[0].leagueId).toBe(profileLeaguesPageDto.memberLeagues[0].leagueId);
expect(result.memberLeagues[0].name).toBe(profileLeaguesPageDto.memberLeagues[0].name);
expect(result.memberLeagues[0].description).toBe(profileLeaguesPageDto.memberLeagues[0].description);
expect(result.memberLeagues[0].membershipRole).toBe(profileLeaguesPageDto.memberLeagues[0].membershipRole);
});
it('should not modify the input DTO', () => {
const profileLeaguesPageDto = {
ownedLeagues: [
{
leagueId: 'league-1',
name: 'Test League',
description: 'Test Description',
membershipRole: 'owner' as const,
},
],
memberLeagues: [
{
leagueId: 'league-2',
name: 'Test League 2',
description: 'Test Description 2',
membershipRole: 'member' as const,
},
],
};
const originalDto = { ...profileLeaguesPageDto };
ProfileLeaguesViewDataBuilder.build(profileLeaguesPageDto);
expect(profileLeaguesPageDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle different membership roles', () => {
const profileLeaguesPageDto = {
ownedLeagues: [
{
leagueId: 'league-1',
name: 'Test League',
description: 'Test Description',
membershipRole: 'owner' as const,
},
{
leagueId: 'league-2',
name: 'Test League 2',
description: 'Test Description 2',
membershipRole: 'admin' as const,
},
{
leagueId: 'league-3',
name: 'Test League 3',
description: 'Test Description 3',
membershipRole: 'steward' as const,
},
{
leagueId: 'league-4',
name: 'Test League 4',
description: 'Test Description 4',
membershipRole: 'member' as const,
},
],
memberLeagues: [],
};
const result = ProfileLeaguesViewDataBuilder.build(profileLeaguesPageDto);
expect(result.ownedLeagues[0].membershipRole).toBe('owner');
expect(result.ownedLeagues[1].membershipRole).toBe('admin');
expect(result.ownedLeagues[2].membershipRole).toBe('steward');
expect(result.ownedLeagues[3].membershipRole).toBe('member');
});
it('should handle empty description', () => {
const profileLeaguesPageDto = {
ownedLeagues: [
{
leagueId: 'league-1',
name: 'Test League',
description: '',
membershipRole: 'owner' as const,
},
],
memberLeagues: [],
};
const result = ProfileLeaguesViewDataBuilder.build(profileLeaguesPageDto);
expect(result.ownedLeagues[0].description).toBe('');
});
});
});

View File

@@ -0,0 +1,499 @@
import { describe, it, expect } from 'vitest';
import { ProfileViewDataBuilder } from './ProfileViewDataBuilder';
import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO';
describe('ProfileViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform GetDriverProfileOutputDTO to ProfileViewData correctly', () => {
const profileDto: GetDriverProfileOutputDTO = {
currentDriver: {
id: 'driver-123',
name: 'Test Driver',
country: 'US',
avatarUrl: 'avatar-url',
bio: 'Test bio',
iracingId: 12345,
joinedAt: '2024-01-01',
globalRank: 100,
},
stats: {
totalRaces: 50,
wins: 10,
podiums: 20,
dnfs: 5,
avgFinish: 5.5,
bestFinish: 1,
worstFinish: 20,
finishRate: 90,
winRate: 20,
podiumRate: 40,
percentile: 95,
rating: 1500,
consistency: 85,
overallRank: 100,
},
finishDistribution: {
totalRaces: 50,
wins: 10,
podiums: 20,
topTen: 30,
dnfs: 5,
other: 15,
},
teamMemberships: [
{
teamId: 'team-1',
teamName: 'Test Team',
teamTag: 'TT',
role: 'driver',
joinedAt: '2024-01-01',
isCurrent: true,
},
],
socialSummary: {
friendsCount: 10,
friends: [
{
id: 'friend-1',
name: 'Friend 1',
country: 'US',
avatarUrl: 'avatar-url',
},
],
},
extendedProfile: {
socialHandles: [
{
platform: 'twitter',
handle: '@test',
url: 'https://twitter.com/test',
},
],
achievements: [
{
id: 'ach-1',
title: 'Achievement',
description: 'Test achievement',
icon: 'trophy',
rarity: 'rare',
earnedAt: '2024-01-01',
},
],
racingStyle: 'Aggressive',
favoriteTrack: 'Test Track',
favoriteCar: 'Test Car',
timezone: 'UTC',
availableHours: 10,
lookingForTeam: true,
openToRequests: true,
},
};
const result = ProfileViewDataBuilder.build(profileDto);
expect(result.driver.id).toBe('driver-123');
expect(result.driver.name).toBe('Test Driver');
expect(result.driver.countryCode).toBe('US');
expect(result.driver.bio).toBe('Test bio');
expect(result.driver.iracingId).toBe('12345');
expect(result.stats).not.toBeNull();
expect(result.stats?.ratingLabel).toBe('1500');
expect(result.teamMemberships).toHaveLength(1);
expect(result.extendedProfile).not.toBeNull();
expect(result.extendedProfile?.socialHandles).toHaveLength(1);
expect(result.extendedProfile?.achievements).toHaveLength(1);
});
it('should handle null driver (no profile)', () => {
const profileDto: GetDriverProfileOutputDTO = {
currentDriver: null,
stats: null,
finishDistribution: null,
teamMemberships: [],
socialSummary: {
friendsCount: 0,
friends: [],
},
extendedProfile: null,
};
const result = ProfileViewDataBuilder.build(profileDto);
expect(result.driver.id).toBe('');
expect(result.driver.name).toBe('');
expect(result.driver.countryCode).toBe('');
expect(result.driver.bio).toBeNull();
expect(result.driver.iracingId).toBeNull();
expect(result.stats).toBeNull();
expect(result.teamMemberships).toHaveLength(0);
expect(result.extendedProfile).toBeNull();
});
it('should handle null stats', () => {
const profileDto: GetDriverProfileOutputDTO = {
currentDriver: {
id: 'driver-123',
name: 'Test Driver',
country: 'US',
avatarUrl: 'avatar-url',
bio: null,
iracingId: null,
joinedAt: '2024-01-01',
globalRank: null,
},
stats: null,
finishDistribution: null,
teamMemberships: [],
socialSummary: {
friendsCount: 0,
friends: [],
},
extendedProfile: null,
};
const result = ProfileViewDataBuilder.build(profileDto);
expect(result.stats).toBeNull();
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const profileDto: GetDriverProfileOutputDTO = {
currentDriver: {
id: 'driver-123',
name: 'Test Driver',
country: 'US',
avatarUrl: 'avatar-url',
bio: 'Test bio',
iracingId: 12345,
joinedAt: '2024-01-01',
globalRank: 100,
},
stats: {
totalRaces: 50,
wins: 10,
podiums: 20,
dnfs: 5,
avgFinish: 5.5,
bestFinish: 1,
worstFinish: 20,
finishRate: 90,
winRate: 20,
podiumRate: 40,
percentile: 95,
rating: 1500,
consistency: 85,
overallRank: 100,
},
finishDistribution: {
totalRaces: 50,
wins: 10,
podiums: 20,
topTen: 30,
dnfs: 5,
other: 15,
},
teamMemberships: [
{
teamId: 'team-1',
teamName: 'Test Team',
teamTag: 'TT',
role: 'driver',
joinedAt: '2024-01-01',
isCurrent: true,
},
],
socialSummary: {
friendsCount: 10,
friends: [
{
id: 'friend-1',
name: 'Friend 1',
country: 'US',
avatarUrl: 'avatar-url',
},
],
},
extendedProfile: {
socialHandles: [
{
platform: 'twitter',
handle: '@test',
url: 'https://twitter.com/test',
},
],
achievements: [
{
id: 'ach-1',
title: 'Achievement',
description: 'Test achievement',
icon: 'trophy',
rarity: 'rare',
earnedAt: '2024-01-01',
},
],
racingStyle: 'Aggressive',
favoriteTrack: 'Test Track',
favoriteCar: 'Test Car',
timezone: 'UTC',
availableHours: 10,
lookingForTeam: true,
openToRequests: true,
},
};
const result = ProfileViewDataBuilder.build(profileDto);
expect(result.driver.id).toBe(profileDto.currentDriver?.id);
expect(result.driver.name).toBe(profileDto.currentDriver?.name);
expect(result.driver.countryCode).toBe(profileDto.currentDriver?.country);
expect(result.driver.bio).toBe(profileDto.currentDriver?.bio);
expect(result.driver.iracingId).toBe(String(profileDto.currentDriver?.iracingId));
expect(result.stats?.totalRacesLabel).toBe('50');
expect(result.stats?.winsLabel).toBe('10');
expect(result.teamMemberships).toHaveLength(1);
expect(result.extendedProfile?.socialHandles).toHaveLength(1);
expect(result.extendedProfile?.achievements).toHaveLength(1);
});
it('should not modify the input DTO', () => {
const profileDto: GetDriverProfileOutputDTO = {
currentDriver: {
id: 'driver-123',
name: 'Test Driver',
country: 'US',
avatarUrl: 'avatar-url',
bio: 'Test bio',
iracingId: 12345,
joinedAt: '2024-01-01',
globalRank: 100,
},
stats: {
totalRaces: 50,
wins: 10,
podiums: 20,
dnfs: 5,
avgFinish: 5.5,
bestFinish: 1,
worstFinish: 20,
finishRate: 90,
winRate: 20,
podiumRate: 40,
percentile: 95,
rating: 1500,
consistency: 85,
overallRank: 100,
},
finishDistribution: {
totalRaces: 50,
wins: 10,
podiums: 20,
topTen: 30,
dnfs: 5,
other: 15,
},
teamMemberships: [
{
teamId: 'team-1',
teamName: 'Test Team',
teamTag: 'TT',
role: 'driver',
joinedAt: '2024-01-01',
isCurrent: true,
},
],
socialSummary: {
friendsCount: 10,
friends: [
{
id: 'friend-1',
name: 'Friend 1',
country: 'US',
avatarUrl: 'avatar-url',
},
],
},
extendedProfile: {
socialHandles: [
{
platform: 'twitter',
handle: '@test',
url: 'https://twitter.com/test',
},
],
achievements: [
{
id: 'ach-1',
title: 'Achievement',
description: 'Test achievement',
icon: 'trophy',
rarity: 'rare',
earnedAt: '2024-01-01',
},
],
racingStyle: 'Aggressive',
favoriteTrack: 'Test Track',
favoriteCar: 'Test Car',
timezone: 'UTC',
availableHours: 10,
lookingForTeam: true,
openToRequests: true,
},
};
const originalDto = { ...profileDto };
ProfileViewDataBuilder.build(profileDto);
expect(profileDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle driver without avatar', () => {
const profileDto: GetDriverProfileOutputDTO = {
currentDriver: {
id: 'driver-123',
name: 'Test Driver',
country: 'US',
avatarUrl: null,
bio: null,
iracingId: null,
joinedAt: '2024-01-01',
globalRank: null,
},
stats: null,
finishDistribution: null,
teamMemberships: [],
socialSummary: {
friendsCount: 0,
friends: [],
},
extendedProfile: null,
};
const result = ProfileViewDataBuilder.build(profileDto);
expect(result.driver.avatarUrl).toContain('default');
});
it('should handle driver without iracingId', () => {
const profileDto: GetDriverProfileOutputDTO = {
currentDriver: {
id: 'driver-123',
name: 'Test Driver',
country: 'US',
avatarUrl: 'avatar-url',
bio: null,
iracingId: null,
joinedAt: '2024-01-01',
globalRank: null,
},
stats: null,
finishDistribution: null,
teamMemberships: [],
socialSummary: {
friendsCount: 0,
friends: [],
},
extendedProfile: null,
};
const result = ProfileViewDataBuilder.build(profileDto);
expect(result.driver.iracingId).toBeNull();
});
it('should handle driver without global rank', () => {
const profileDto: GetDriverProfileOutputDTO = {
currentDriver: {
id: 'driver-123',
name: 'Test Driver',
country: 'US',
avatarUrl: 'avatar-url',
bio: null,
iracingId: null,
joinedAt: '2024-01-01',
globalRank: null,
},
stats: null,
finishDistribution: null,
teamMemberships: [],
socialSummary: {
friendsCount: 0,
friends: [],
},
extendedProfile: null,
};
const result = ProfileViewDataBuilder.build(profileDto);
expect(result.driver.globalRankLabel).toBe('—');
});
it('should handle empty team memberships', () => {
const profileDto: GetDriverProfileOutputDTO = {
currentDriver: {
id: 'driver-123',
name: 'Test Driver',
country: 'US',
avatarUrl: 'avatar-url',
bio: null,
iracingId: null,
joinedAt: '2024-01-01',
globalRank: null,
},
stats: null,
finishDistribution: null,
teamMemberships: [],
socialSummary: {
friendsCount: 0,
friends: [],
},
extendedProfile: null,
};
const result = ProfileViewDataBuilder.build(profileDto);
expect(result.teamMemberships).toHaveLength(0);
});
it('should handle empty friends list', () => {
const profileDto: GetDriverProfileOutputDTO = {
currentDriver: {
id: 'driver-123',
name: 'Test Driver',
country: 'US',
avatarUrl: 'avatar-url',
bio: null,
iracingId: null,
joinedAt: '2024-01-01',
globalRank: null,
},
stats: null,
finishDistribution: null,
teamMemberships: [],
socialSummary: {
friendsCount: 0,
friends: [],
},
extendedProfile: {
socialHandles: [],
achievements: [],
racingStyle: null,
favoriteTrack: null,
favoriteCar: null,
timezone: null,
availableHours: null,
lookingForTeam: false,
openToRequests: false,
},
};
const result = ProfileViewDataBuilder.build(profileDto);
expect(result.extendedProfile?.friends).toHaveLength(0);
expect(result.extendedProfile?.friendsCountLabel).toBe('0');
});
});
});

View File

@@ -0,0 +1,319 @@
import { describe, it, expect } from 'vitest';
import { ProtestDetailViewDataBuilder } from './ProtestDetailViewDataBuilder';
describe('ProtestDetailViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform ProtestDetailApiDto to ProtestDetailViewData correctly', () => {
const protestDetailApiDto = {
id: 'protest-123',
leagueId: 'league-456',
status: 'pending',
submittedAt: '2024-01-01T10:00:00Z',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
protestingDriver: {
id: 'driver-1',
name: 'Driver 1',
},
accusedDriver: {
id: 'driver-2',
name: 'Driver 2',
},
race: {
id: 'race-1',
name: 'Test Race',
scheduledAt: '2024-01-01T10:00:00Z',
},
penaltyTypes: [
{
type: 'time_penalty',
label: 'Time Penalty',
description: 'Add time to race result',
},
],
};
const result = ProtestDetailViewDataBuilder.build(protestDetailApiDto);
expect(result).toEqual({
protestId: 'protest-123',
leagueId: 'league-456',
status: 'pending',
submittedAt: '2024-01-01T10:00:00Z',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
protestingDriver: {
id: 'driver-1',
name: 'Driver 1',
},
accusedDriver: {
id: 'driver-2',
name: 'Driver 2',
},
race: {
id: 'race-1',
name: 'Test Race',
scheduledAt: '2024-01-01T10:00:00Z',
},
penaltyTypes: [
{
type: 'time_penalty',
label: 'Time Penalty',
description: 'Add time to race result',
},
],
});
});
it('should handle resolved status', () => {
const protestDetailApiDto = {
id: 'protest-456',
leagueId: 'league-789',
status: 'resolved',
submittedAt: '2024-01-01T10:00:00Z',
incident: {
lap: 10,
description: 'Contact at turn 5',
},
protestingDriver: {
id: 'driver-3',
name: 'Driver 3',
},
accusedDriver: {
id: 'driver-4',
name: 'Driver 4',
},
race: {
id: 'race-2',
name: 'Test Race 2',
scheduledAt: '2024-01-02T10:00:00Z',
},
penaltyTypes: [],
};
const result = ProtestDetailViewDataBuilder.build(protestDetailApiDto);
expect(result.status).toBe('resolved');
});
it('should handle multiple penalty types', () => {
const protestDetailApiDto = {
id: 'protest-789',
leagueId: 'league-101',
status: 'pending',
submittedAt: '2024-01-01T10:00:00Z',
incident: {
lap: 15,
description: 'Contact at turn 7',
},
protestingDriver: {
id: 'driver-5',
name: 'Driver 5',
},
accusedDriver: {
id: 'driver-6',
name: 'Driver 6',
},
race: {
id: 'race-3',
name: 'Test Race 3',
scheduledAt: '2024-01-03T10:00:00Z',
},
penaltyTypes: [
{
type: 'time_penalty',
label: 'Time Penalty',
description: 'Add time to race result',
},
{
type: 'grid_penalty',
label: 'Grid Penalty',
description: 'Drop grid positions',
},
],
};
const result = ProtestDetailViewDataBuilder.build(protestDetailApiDto);
expect(result.penaltyTypes).toHaveLength(2);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const protestDetailApiDto = {
id: 'protest-101',
leagueId: 'league-102',
status: 'pending',
submittedAt: '2024-01-01T10:00:00Z',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
protestingDriver: {
id: 'driver-1',
name: 'Driver 1',
},
accusedDriver: {
id: 'driver-2',
name: 'Driver 2',
},
race: {
id: 'race-1',
name: 'Test Race',
scheduledAt: '2024-01-01T10:00:00Z',
},
penaltyTypes: [
{
type: 'time_penalty',
label: 'Time Penalty',
description: 'Add time to race result',
},
],
};
const result = ProtestDetailViewDataBuilder.build(protestDetailApiDto);
expect(result.protestId).toBe(protestDetailApiDto.id);
expect(result.leagueId).toBe(protestDetailApiDto.leagueId);
expect(result.status).toBe(protestDetailApiDto.status);
expect(result.submittedAt).toBe(protestDetailApiDto.submittedAt);
expect(result.incident).toEqual(protestDetailApiDto.incident);
expect(result.protestingDriver).toEqual(protestDetailApiDto.protestingDriver);
expect(result.accusedDriver).toEqual(protestDetailApiDto.accusedDriver);
expect(result.race).toEqual(protestDetailApiDto.race);
expect(result.penaltyTypes).toEqual(protestDetailApiDto.penaltyTypes);
});
it('should not modify the input DTO', () => {
const protestDetailApiDto = {
id: 'protest-102',
leagueId: 'league-103',
status: 'pending',
submittedAt: '2024-01-01T10:00:00Z',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
protestingDriver: {
id: 'driver-1',
name: 'Driver 1',
},
accusedDriver: {
id: 'driver-2',
name: 'Driver 2',
},
race: {
id: 'race-1',
name: 'Test Race',
scheduledAt: '2024-01-01T10:00:00Z',
},
penaltyTypes: [],
};
const originalDto = { ...protestDetailApiDto };
ProtestDetailViewDataBuilder.build(protestDetailApiDto);
expect(protestDetailApiDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle different status values', () => {
const protestDetailApiDto = {
id: 'protest-103',
leagueId: 'league-104',
status: 'rejected',
submittedAt: '2024-01-01T10:00:00Z',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
protestingDriver: {
id: 'driver-1',
name: 'Driver 1',
},
accusedDriver: {
id: 'driver-2',
name: 'Driver 2',
},
race: {
id: 'race-1',
name: 'Test Race',
scheduledAt: '2024-01-01T10:00:00Z',
},
penaltyTypes: [],
};
const result = ProtestDetailViewDataBuilder.build(protestDetailApiDto);
expect(result.status).toBe('rejected');
});
it('should handle lap 0', () => {
const protestDetailApiDto = {
id: 'protest-104',
leagueId: 'league-105',
status: 'pending',
submittedAt: '2024-01-01T10:00:00Z',
incident: {
lap: 0,
description: 'Contact at start',
},
protestingDriver: {
id: 'driver-1',
name: 'Driver 1',
},
accusedDriver: {
id: 'driver-2',
name: 'Driver 2',
},
race: {
id: 'race-1',
name: 'Test Race',
scheduledAt: '2024-01-01T10:00:00Z',
},
penaltyTypes: [],
};
const result = ProtestDetailViewDataBuilder.build(protestDetailApiDto);
expect(result.incident.lap).toBe(0);
});
it('should handle empty description', () => {
const protestDetailApiDto = {
id: 'protest-105',
leagueId: 'league-106',
status: 'pending',
submittedAt: '2024-01-01T10:00:00Z',
incident: {
lap: 5,
description: '',
},
protestingDriver: {
id: 'driver-1',
name: 'Driver 1',
},
accusedDriver: {
id: 'driver-2',
name: 'Driver 2',
},
race: {
id: 'race-1',
name: 'Test Race',
scheduledAt: '2024-01-01T10:00:00Z',
},
penaltyTypes: [],
};
const result = ProtestDetailViewDataBuilder.build(protestDetailApiDto);
expect(result.incident.description).toBe('');
});
});
});

View File

@@ -0,0 +1,393 @@
import { describe, it, expect } from 'vitest';
import { RaceDetailViewDataBuilder } from './RaceDetailViewDataBuilder';
describe('RaceDetailViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform API DTO to RaceDetailViewData correctly', () => {
const apiDto = {
race: {
id: 'race-123',
track: 'Test Track',
car: 'Test Car',
scheduledAt: '2024-01-01T10:00:00Z',
status: 'scheduled',
sessionType: 'race',
},
league: {
id: 'league-456',
name: 'Test League',
description: 'Test Description',
settings: {
maxDrivers: 32,
qualifyingFormat: 'Open',
},
},
entryList: [
{
id: 'driver-1',
name: 'Driver 1',
avatarUrl: 'avatar-url',
country: 'US',
rating: 1500,
isCurrentUser: false,
},
],
registration: {
isUserRegistered: false,
canRegister: true,
},
userResult: {
position: 5,
startPosition: 10,
positionChange: 5,
incidents: 2,
isClean: false,
isPodium: false,
ratingChange: 10,
},
canReopenRace: false,
};
const result = RaceDetailViewDataBuilder.build(apiDto);
expect(result).toEqual({
race: {
id: 'race-123',
track: 'Test Track',
car: 'Test Car',
scheduledAt: '2024-01-01T10:00:00Z',
status: 'scheduled',
sessionType: 'race',
},
league: {
id: 'league-456',
name: 'Test League',
description: 'Test Description',
settings: {
maxDrivers: 32,
qualifyingFormat: 'Open',
},
},
entryList: [
{
id: 'driver-1',
name: 'Driver 1',
avatarUrl: 'avatar-url',
country: 'US',
rating: 1500,
isCurrentUser: false,
},
],
registration: {
isUserRegistered: false,
canRegister: true,
},
userResult: {
position: 5,
startPosition: 10,
positionChange: 5,
incidents: 2,
isClean: false,
isPodium: false,
ratingChange: 10,
},
canReopenRace: false,
});
});
it('should handle race without league', () => {
const apiDto = {
race: {
id: 'race-456',
track: 'Test Track',
car: 'Test Car',
scheduledAt: '2024-01-01T10:00:00Z',
status: 'scheduled',
sessionType: 'race',
},
entryList: [],
registration: {
isUserRegistered: false,
canRegister: false,
},
canReopenRace: false,
};
const result = RaceDetailViewDataBuilder.build(apiDto);
expect(result.league).toBeUndefined();
});
it('should handle race without user result', () => {
const apiDto = {
race: {
id: 'race-789',
track: 'Test Track',
car: 'Test Car',
scheduledAt: '2024-01-01T10:00:00Z',
status: 'scheduled',
sessionType: 'race',
},
entryList: [],
registration: {
isUserRegistered: false,
canRegister: false,
},
canReopenRace: false,
};
const result = RaceDetailViewDataBuilder.build(apiDto);
expect(result.userResult).toBeUndefined();
});
it('should handle multiple entries in entry list', () => {
const apiDto = {
race: {
id: 'race-101',
track: 'Test Track',
car: 'Test Car',
scheduledAt: '2024-01-01T10:00:00Z',
status: 'scheduled',
sessionType: 'race',
},
entryList: [
{
id: 'driver-1',
name: 'Driver 1',
avatarUrl: 'avatar-url',
country: 'US',
rating: 1500,
isCurrentUser: false,
},
{
id: 'driver-2',
name: 'Driver 2',
avatarUrl: 'avatar-url',
country: 'UK',
rating: 1600,
isCurrentUser: true,
},
],
registration: {
isUserRegistered: true,
canRegister: false,
},
canReopenRace: false,
};
const result = RaceDetailViewDataBuilder.build(apiDto);
expect(result.entryList).toHaveLength(2);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const apiDto = {
race: {
id: 'race-102',
track: 'Test Track',
car: 'Test Car',
scheduledAt: '2024-01-01T10:00:00Z',
status: 'scheduled',
sessionType: 'race',
},
league: {
id: 'league-103',
name: 'Test League',
description: 'Test Description',
settings: {
maxDrivers: 32,
qualifyingFormat: 'Open',
},
},
entryList: [
{
id: 'driver-1',
name: 'Driver 1',
avatarUrl: 'avatar-url',
country: 'US',
rating: 1500,
isCurrentUser: false,
},
],
registration: {
isUserRegistered: false,
canRegister: true,
},
userResult: {
position: 5,
startPosition: 10,
positionChange: 5,
incidents: 2,
isClean: false,
isPodium: false,
ratingChange: 10,
},
canReopenRace: false,
};
const result = RaceDetailViewDataBuilder.build(apiDto);
expect(result.race.id).toBe(apiDto.race.id);
expect(result.race.track).toBe(apiDto.race.track);
expect(result.race.car).toBe(apiDto.race.car);
expect(result.race.scheduledAt).toBe(apiDto.race.scheduledAt);
expect(result.race.status).toBe(apiDto.race.status);
expect(result.race.sessionType).toBe(apiDto.race.sessionType);
expect(result.league?.id).toBe(apiDto.league.id);
expect(result.league?.name).toBe(apiDto.league.name);
expect(result.registration.isUserRegistered).toBe(apiDto.registration.isUserRegistered);
expect(result.registration.canRegister).toBe(apiDto.registration.canRegister);
expect(result.canReopenRace).toBe(apiDto.canReopenRace);
});
it('should not modify the input DTO', () => {
const apiDto = {
race: {
id: 'race-104',
track: 'Test Track',
car: 'Test Car',
scheduledAt: '2024-01-01T10:00:00Z',
status: 'scheduled',
sessionType: 'race',
},
entryList: [],
registration: {
isUserRegistered: false,
canRegister: false,
},
canReopenRace: false,
};
const originalDto = { ...apiDto };
RaceDetailViewDataBuilder.build(apiDto);
expect(apiDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle null API DTO', () => {
const result = RaceDetailViewDataBuilder.build(null);
expect(result.race.id).toBe('');
expect(result.race.track).toBe('');
expect(result.race.car).toBe('');
expect(result.race.scheduledAt).toBe('');
expect(result.race.status).toBe('scheduled');
expect(result.race.sessionType).toBe('race');
expect(result.entryList).toHaveLength(0);
expect(result.registration.isUserRegistered).toBe(false);
expect(result.registration.canRegister).toBe(false);
expect(result.canReopenRace).toBe(false);
});
it('should handle undefined API DTO', () => {
const result = RaceDetailViewDataBuilder.build(undefined);
expect(result.race.id).toBe('');
expect(result.race.track).toBe('');
expect(result.race.car).toBe('');
expect(result.race.scheduledAt).toBe('');
expect(result.race.status).toBe('scheduled');
expect(result.race.sessionType).toBe('race');
expect(result.entryList).toHaveLength(0);
expect(result.registration.isUserRegistered).toBe(false);
expect(result.registration.canRegister).toBe(false);
expect(result.canReopenRace).toBe(false);
});
it('should handle race without entry list', () => {
const apiDto = {
race: {
id: 'race-105',
track: 'Test Track',
car: 'Test Car',
scheduledAt: '2024-01-01T10:00:00Z',
status: 'scheduled',
sessionType: 'race',
},
registration: {
isUserRegistered: false,
canRegister: false,
},
canReopenRace: false,
};
const result = RaceDetailViewDataBuilder.build(apiDto);
expect(result.entryList).toHaveLength(0);
});
it('should handle different race statuses', () => {
const apiDto = {
race: {
id: 'race-106',
track: 'Test Track',
car: 'Test Car',
scheduledAt: '2024-01-01T10:00:00Z',
status: 'running',
sessionType: 'race',
},
entryList: [],
registration: {
isUserRegistered: false,
canRegister: false,
},
canReopenRace: false,
};
const result = RaceDetailViewDataBuilder.build(apiDto);
expect(result.race.status).toBe('running');
});
it('should handle different session types', () => {
const apiDto = {
race: {
id: 'race-107',
track: 'Test Track',
car: 'Test Car',
scheduledAt: '2024-01-01T10:00:00Z',
status: 'scheduled',
sessionType: 'qualifying',
},
entryList: [],
registration: {
isUserRegistered: false,
canRegister: false,
},
canReopenRace: false,
};
const result = RaceDetailViewDataBuilder.build(apiDto);
expect(result.race.sessionType).toBe('qualifying');
});
it('should handle canReopenRace true', () => {
const apiDto = {
race: {
id: 'race-108',
track: 'Test Track',
car: 'Test Car',
scheduledAt: '2024-01-01T10:00:00Z',
status: 'completed',
sessionType: 'race',
},
entryList: [],
registration: {
isUserRegistered: false,
canRegister: false,
},
canReopenRace: true,
};
const result = RaceDetailViewDataBuilder.build(apiDto);
expect(result.canReopenRace).toBe(true);
});
});
});

View File

@@ -0,0 +1,775 @@
import { describe, it, expect } from 'vitest';
import { RaceResultsViewDataBuilder } from './RaceResultsViewDataBuilder';
describe('RaceResultsViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform API DTO to RaceResultsViewData correctly', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [
{
position: 1,
driverId: 'driver-1',
driverName: 'Driver 1',
avatarUrl: 'avatar-url',
country: 'US',
car: 'Test Car',
laps: 30,
time: '1:23.456',
fastestLap: '1:20.000',
points: 25,
incidents: 0,
isCurrentUser: false,
},
],
penalties: [
{
driverId: 'driver-2',
driverName: 'Driver 2',
type: 'time_penalty',
value: 5,
reason: 'Track limits',
notes: 'Warning issued',
},
],
pointsSystem: {
1: 25,
2: 18,
3: 15,
},
fastestLapTime: 120000,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result).toEqual({
raceTrack: 'Test Track',
raceScheduledAt: '2024-01-01T10:00:00Z',
totalDrivers: 20,
leagueName: 'Test League',
raceSOF: 1500,
results: [
{
position: 1,
driverId: 'driver-1',
driverName: 'Driver 1',
driverAvatar: 'avatar-url',
country: 'US',
car: 'Test Car',
laps: 30,
time: '1:23.456',
fastestLap: '1:20.000',
points: 25,
incidents: 0,
isCurrentUser: false,
},
],
penalties: [
{
driverId: 'driver-2',
driverName: 'Driver 2',
type: 'time_penalty',
value: 5,
reason: 'Track limits',
notes: 'Warning issued',
},
],
pointsSystem: {
1: 25,
2: 18,
3: 15,
},
fastestLapTime: 120000,
});
});
it('should handle empty results and penalties', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 0,
},
league: {
name: 'Test League',
},
strengthOfField: null,
results: [],
penalties: [],
pointsSystem: {},
fastestLapTime: 0,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result.results).toHaveLength(0);
expect(result.penalties).toHaveLength(0);
expect(result.raceSOF).toBeNull();
});
it('should handle multiple results and penalties', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [
{
position: 1,
driverId: 'driver-1',
driverName: 'Driver 1',
avatarUrl: 'avatar-url',
country: 'US',
car: 'Test Car',
laps: 30,
time: '1:23.456',
fastestLap: '1:20.000',
points: 25,
incidents: 0,
isCurrentUser: false,
},
{
position: 2,
driverId: 'driver-2',
driverName: 'Driver 2',
avatarUrl: 'avatar-url',
country: 'UK',
car: 'Test Car',
laps: 30,
time: '1:24.000',
fastestLap: '1:21.000',
points: 18,
incidents: 1,
isCurrentUser: true,
},
],
penalties: [
{
driverId: 'driver-3',
driverName: 'Driver 3',
type: 'time_penalty',
value: 5,
reason: 'Track limits',
notes: 'Warning issued',
},
{
driverId: 'driver-4',
driverName: 'Driver 4',
type: 'grid_penalty',
value: 3,
reason: 'Qualifying infringement',
notes: null,
},
],
pointsSystem: {
1: 25,
2: 18,
3: 15,
},
fastestLapTime: 120000,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result.results).toHaveLength(2);
expect(result.penalties).toHaveLength(2);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [
{
position: 1,
driverId: 'driver-1',
driverName: 'Driver 1',
avatarUrl: 'avatar-url',
country: 'US',
car: 'Test Car',
laps: 30,
time: '1:23.456',
fastestLap: '1:20.000',
points: 25,
incidents: 0,
isCurrentUser: false,
},
],
penalties: [],
pointsSystem: {
1: 25,
2: 18,
3: 15,
},
fastestLapTime: 120000,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result.raceTrack).toBe(apiDto.race.track);
expect(result.raceScheduledAt).toBe(apiDto.race.scheduledAt);
expect(result.totalDrivers).toBe(apiDto.stats.totalDrivers);
expect(result.leagueName).toBe(apiDto.league.name);
expect(result.raceSOF).toBe(apiDto.strengthOfField);
expect(result.pointsSystem).toEqual(apiDto.pointsSystem);
expect(result.fastestLapTime).toBe(apiDto.fastestLapTime);
});
it('should not modify the input DTO', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [],
penalties: [],
pointsSystem: {},
fastestLapTime: 0,
};
const originalDto = { ...apiDto };
RaceResultsViewDataBuilder.build(apiDto);
expect(apiDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle null API DTO', () => {
const result = RaceResultsViewDataBuilder.build(null);
expect(result.raceSOF).toBeNull();
expect(result.results).toHaveLength(0);
expect(result.penalties).toHaveLength(0);
expect(result.pointsSystem).toEqual({});
expect(result.fastestLapTime).toBe(0);
});
it('should handle undefined API DTO', () => {
const result = RaceResultsViewDataBuilder.build(undefined);
expect(result.raceSOF).toBeNull();
expect(result.results).toHaveLength(0);
expect(result.penalties).toHaveLength(0);
expect(result.pointsSystem).toEqual({});
expect(result.fastestLapTime).toBe(0);
});
it('should handle results without country', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [
{
position: 1,
driverId: 'driver-1',
driverName: 'Driver 1',
avatarUrl: 'avatar-url',
country: null,
car: 'Test Car',
laps: 30,
time: '1:23.456',
fastestLap: '1:20.000',
points: 25,
incidents: 0,
isCurrentUser: false,
},
],
penalties: [],
pointsSystem: {},
fastestLapTime: 0,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result.results[0].country).toBe('US');
});
it('should handle results without car', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [
{
position: 1,
driverId: 'driver-1',
driverName: 'Driver 1',
avatarUrl: 'avatar-url',
country: 'US',
car: null,
laps: 30,
time: '1:23.456',
fastestLap: '1:20.000',
points: 25,
incidents: 0,
isCurrentUser: false,
},
],
penalties: [],
pointsSystem: {},
fastestLapTime: 0,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result.results[0].car).toBe('Unknown');
});
it('should handle results without laps', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [
{
position: 1,
driverId: 'driver-1',
driverName: 'Driver 1',
avatarUrl: 'avatar-url',
country: 'US',
car: 'Test Car',
laps: null,
time: '1:23.456',
fastestLap: '1:20.000',
points: 25,
incidents: 0,
isCurrentUser: false,
},
],
penalties: [],
pointsSystem: {},
fastestLapTime: 0,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result.results[0].laps).toBe(0);
});
it('should handle results without time', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [
{
position: 1,
driverId: 'driver-1',
driverName: 'Driver 1',
avatarUrl: 'avatar-url',
country: 'US',
car: 'Test Car',
laps: 30,
time: null,
fastestLap: '1:20.000',
points: 25,
incidents: 0,
isCurrentUser: false,
},
],
penalties: [],
pointsSystem: {},
fastestLapTime: 0,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result.results[0].time).toBe('0:00.00');
});
it('should handle results without fastest lap', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [
{
position: 1,
driverId: 'driver-1',
driverName: 'Driver 1',
avatarUrl: 'avatar-url',
country: 'US',
car: 'Test Car',
laps: 30,
time: '1:23.456',
fastestLap: null,
points: 25,
incidents: 0,
isCurrentUser: false,
},
],
penalties: [],
pointsSystem: {},
fastestLapTime: 0,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result.results[0].fastestLap).toBe('0.00');
});
it('should handle results without points', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [
{
position: 1,
driverId: 'driver-1',
driverName: 'Driver 1',
avatarUrl: 'avatar-url',
country: 'US',
car: 'Test Car',
laps: 30,
time: '1:23.456',
fastestLap: '1:20.000',
points: null,
incidents: 0,
isCurrentUser: false,
},
],
penalties: [],
pointsSystem: {},
fastestLapTime: 0,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result.results[0].points).toBe(0);
});
it('should handle results without incidents', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [
{
position: 1,
driverId: 'driver-1',
driverName: 'Driver 1',
avatarUrl: 'avatar-url',
country: 'US',
car: 'Test Car',
laps: 30,
time: '1:23.456',
fastestLap: '1:20.000',
points: 25,
incidents: null,
isCurrentUser: false,
},
],
penalties: [],
pointsSystem: {},
fastestLapTime: 0,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result.results[0].incidents).toBe(0);
});
it('should handle results without isCurrentUser', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [
{
position: 1,
driverId: 'driver-1',
driverName: 'Driver 1',
avatarUrl: 'avatar-url',
country: 'US',
car: 'Test Car',
laps: 30,
time: '1:23.456',
fastestLap: '1:20.000',
points: 25,
incidents: 0,
isCurrentUser: null,
},
],
penalties: [],
pointsSystem: {},
fastestLapTime: 0,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result.results[0].isCurrentUser).toBe(false);
});
it('should handle penalties without driver name', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [],
penalties: [
{
driverId: 'driver-1',
driverName: null,
type: 'time_penalty',
value: 5,
reason: 'Track limits',
notes: 'Warning issued',
},
],
pointsSystem: {},
fastestLapTime: 0,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result.penalties[0].driverName).toBe('Unknown');
});
it('should handle penalties without value', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [],
penalties: [
{
driverId: 'driver-1',
driverName: 'Driver 1',
type: 'time_penalty',
value: null,
reason: 'Track limits',
notes: 'Warning issued',
},
],
pointsSystem: {},
fastestLapTime: 0,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result.penalties[0].value).toBe(0);
});
it('should handle penalties without reason', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [],
penalties: [
{
driverId: 'driver-1',
driverName: 'Driver 1',
type: 'time_penalty',
value: 5,
reason: null,
notes: 'Warning issued',
},
],
pointsSystem: {},
fastestLapTime: 0,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result.penalties[0].reason).toBe('Penalty applied');
});
it('should handle different penalty types', () => {
const apiDto = {
race: {
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
stats: {
totalDrivers: 20,
},
league: {
name: 'Test League',
},
strengthOfField: 1500,
results: [],
penalties: [
{
driverId: 'driver-1',
driverName: 'Driver 1',
type: 'grid_penalty',
value: 3,
reason: 'Qualifying infringement',
notes: null,
},
{
driverId: 'driver-2',
driverName: 'Driver 2',
type: 'points_deduction',
value: 10,
reason: 'Dangerous driving',
notes: null,
},
{
driverId: 'driver-3',
driverName: 'Driver 3',
type: 'disqualification',
value: 0,
reason: 'Technical infringement',
notes: null,
},
{
driverId: 'driver-4',
driverName: 'Driver 4',
type: 'warning',
value: 0,
reason: 'Minor infraction',
notes: null,
},
{
driverId: 'driver-5',
driverName: 'Driver 5',
type: 'license_points',
value: 2,
reason: 'Multiple incidents',
notes: null,
},
],
pointsSystem: {},
fastestLapTime: 0,
};
const result = RaceResultsViewDataBuilder.build(apiDto);
expect(result.penalties[0].type).toBe('grid_penalty');
expect(result.penalties[1].type).toBe('points_deduction');
expect(result.penalties[2].type).toBe('disqualification');
expect(result.penalties[3].type).toBe('warning');
expect(result.penalties[4].type).toBe('license_points');
});
});
});

View File

@@ -0,0 +1,841 @@
import { describe, it, expect } from 'vitest';
import { RaceStewardingViewDataBuilder } from './RaceStewardingViewDataBuilder';
describe('RaceStewardingViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform API DTO to RaceStewardingViewData correctly', () => {
const apiDto = {
race: {
id: 'race-123',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-456',
},
pendingProtests: [
{
id: 'protest-1',
protestingDriverId: 'driver-1',
accusedDriverId: 'driver-2',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'pending',
proofVideoUrl: 'video-url',
decisionNotes: null,
},
],
resolvedProtests: [
{
id: 'protest-2',
protestingDriverId: 'driver-3',
accusedDriverId: 'driver-4',
incident: {
lap: 10,
description: 'Contact at turn 5',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'resolved',
proofVideoUrl: 'video-url',
decisionNotes: 'Penalty applied',
},
],
penalties: [
{
id: 'penalty-1',
driverId: 'driver-5',
type: 'time_penalty',
value: 5,
reason: 'Track limits',
notes: 'Warning issued',
},
],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
'driver-2': { id: 'driver-2', name: 'Driver 2' },
'driver-3': { id: 'driver-3', name: 'Driver 3' },
'driver-4': { id: 'driver-4', name: 'Driver 4' },
'driver-5': { id: 'driver-5', name: 'Driver 5' },
},
pendingCount: 1,
resolvedCount: 1,
penaltiesCount: 1,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result).toEqual({
race: {
id: 'race-123',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-456',
},
pendingProtests: [
{
id: 'protest-1',
protestingDriverId: 'driver-1',
accusedDriverId: 'driver-2',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'pending',
proofVideoUrl: 'video-url',
decisionNotes: null,
},
],
resolvedProtests: [
{
id: 'protest-2',
protestingDriverId: 'driver-3',
accusedDriverId: 'driver-4',
incident: {
lap: 10,
description: 'Contact at turn 5',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'resolved',
proofVideoUrl: 'video-url',
decisionNotes: 'Penalty applied',
},
],
penalties: [
{
id: 'penalty-1',
driverId: 'driver-5',
type: 'time_penalty',
value: 5,
reason: 'Track limits',
notes: 'Warning issued',
},
],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
'driver-2': { id: 'driver-2', name: 'Driver 2' },
'driver-3': { id: 'driver-3', name: 'Driver 3' },
'driver-4': { id: 'driver-4', name: 'Driver 4' },
'driver-5': { id: 'driver-5', name: 'Driver 5' },
},
pendingCount: 1,
resolvedCount: 1,
penaltiesCount: 1,
});
});
it('should handle empty protests and penalties', () => {
const apiDto = {
race: {
id: 'race-456',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-789',
},
pendingProtests: [],
resolvedProtests: [],
penalties: [],
driverMap: {},
pendingCount: 0,
resolvedCount: 0,
penaltiesCount: 0,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.pendingProtests).toHaveLength(0);
expect(result.resolvedProtests).toHaveLength(0);
expect(result.penalties).toHaveLength(0);
expect(result.pendingCount).toBe(0);
expect(result.resolvedCount).toBe(0);
expect(result.penaltiesCount).toBe(0);
});
it('should handle multiple protests and penalties', () => {
const apiDto = {
race: {
id: 'race-789',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-101',
},
pendingProtests: [
{
id: 'protest-1',
protestingDriverId: 'driver-1',
accusedDriverId: 'driver-2',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'pending',
proofVideoUrl: 'video-url',
decisionNotes: null,
},
{
id: 'protest-2',
protestingDriverId: 'driver-3',
accusedDriverId: 'driver-4',
incident: {
lap: 10,
description: 'Contact at turn 5',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'pending',
proofVideoUrl: 'video-url',
decisionNotes: null,
},
],
resolvedProtests: [
{
id: 'protest-3',
protestingDriverId: 'driver-5',
accusedDriverId: 'driver-6',
incident: {
lap: 15,
description: 'Contact at turn 7',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'resolved',
proofVideoUrl: 'video-url',
decisionNotes: 'Penalty applied',
},
],
penalties: [
{
id: 'penalty-1',
driverId: 'driver-7',
type: 'time_penalty',
value: 5,
reason: 'Track limits',
notes: 'Warning issued',
},
{
id: 'penalty-2',
driverId: 'driver-8',
type: 'grid_penalty',
value: 3,
reason: 'Qualifying infringement',
notes: null,
},
],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
'driver-2': { id: 'driver-2', name: 'Driver 2' },
'driver-3': { id: 'driver-3', name: 'Driver 3' },
'driver-4': { id: 'driver-4', name: 'Driver 4' },
'driver-5': { id: 'driver-5', name: 'Driver 5' },
'driver-6': { id: 'driver-6', name: 'Driver 6' },
'driver-7': { id: 'driver-7', name: 'Driver 7' },
'driver-8': { id: 'driver-8', name: 'Driver 8' },
},
pendingCount: 2,
resolvedCount: 1,
penaltiesCount: 2,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.pendingProtests).toHaveLength(2);
expect(result.resolvedProtests).toHaveLength(1);
expect(result.penalties).toHaveLength(2);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const apiDto = {
race: {
id: 'race-102',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-103',
},
pendingProtests: [
{
id: 'protest-1',
protestingDriverId: 'driver-1',
accusedDriverId: 'driver-2',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'pending',
proofVideoUrl: 'video-url',
decisionNotes: null,
},
],
resolvedProtests: [],
penalties: [],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
'driver-2': { id: 'driver-2', name: 'Driver 2' },
},
pendingCount: 1,
resolvedCount: 0,
penaltiesCount: 0,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.race?.id).toBe(apiDto.race.id);
expect(result.race?.track).toBe(apiDto.race.track);
expect(result.race?.scheduledAt).toBe(apiDto.race.scheduledAt);
expect(result.league?.id).toBe(apiDto.league.id);
expect(result.pendingCount).toBe(apiDto.pendingCount);
expect(result.resolvedCount).toBe(apiDto.resolvedCount);
expect(result.penaltiesCount).toBe(apiDto.penaltiesCount);
});
it('should not modify the input DTO', () => {
const apiDto = {
race: {
id: 'race-104',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-105',
},
pendingProtests: [],
resolvedProtests: [],
penalties: [],
driverMap: {},
pendingCount: 0,
resolvedCount: 0,
penaltiesCount: 0,
};
const originalDto = { ...apiDto };
RaceStewardingViewDataBuilder.build(apiDto);
expect(apiDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle null API DTO', () => {
const result = RaceStewardingViewDataBuilder.build(null);
expect(result.race).toBeNull();
expect(result.league).toBeNull();
expect(result.pendingProtests).toHaveLength(0);
expect(result.resolvedProtests).toHaveLength(0);
expect(result.penalties).toHaveLength(0);
expect(result.driverMap).toEqual({});
expect(result.pendingCount).toBe(0);
expect(result.resolvedCount).toBe(0);
expect(result.penaltiesCount).toBe(0);
});
it('should handle undefined API DTO', () => {
const result = RaceStewardingViewDataBuilder.build(undefined);
expect(result.race).toBeNull();
expect(result.league).toBeNull();
expect(result.pendingProtests).toHaveLength(0);
expect(result.resolvedProtests).toHaveLength(0);
expect(result.penalties).toHaveLength(0);
expect(result.driverMap).toEqual({});
expect(result.pendingCount).toBe(0);
expect(result.resolvedCount).toBe(0);
expect(result.penaltiesCount).toBe(0);
});
it('should handle race without league', () => {
const apiDto = {
race: {
id: 'race-106',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
pendingProtests: [],
resolvedProtests: [],
penalties: [],
driverMap: {},
pendingCount: 0,
resolvedCount: 0,
penaltiesCount: 0,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.league).toBeNull();
});
it('should handle protests without proof video', () => {
const apiDto = {
race: {
id: 'race-107',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-108',
},
pendingProtests: [
{
id: 'protest-1',
protestingDriverId: 'driver-1',
accusedDriverId: 'driver-2',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'pending',
proofVideoUrl: null,
decisionNotes: null,
},
],
resolvedProtests: [],
penalties: [],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
'driver-2': { id: 'driver-2', name: 'Driver 2' },
},
pendingCount: 1,
resolvedCount: 0,
penaltiesCount: 0,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.pendingProtests[0].proofVideoUrl).toBeNull();
});
it('should handle protests without decision notes', () => {
const apiDto = {
race: {
id: 'race-109',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-110',
},
pendingProtests: [],
resolvedProtests: [
{
id: 'protest-1',
protestingDriverId: 'driver-1',
accusedDriverId: 'driver-2',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'resolved',
proofVideoUrl: 'video-url',
decisionNotes: null,
},
],
penalties: [],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
'driver-2': { id: 'driver-2', name: 'Driver 2' },
},
pendingCount: 0,
resolvedCount: 1,
penaltiesCount: 0,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.resolvedProtests[0].decisionNotes).toBeNull();
});
it('should handle penalties without notes', () => {
const apiDto = {
race: {
id: 'race-111',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-112',
},
pendingProtests: [],
resolvedProtests: [],
penalties: [
{
id: 'penalty-1',
driverId: 'driver-1',
type: 'time_penalty',
value: 5,
reason: 'Track limits',
notes: null,
},
],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
},
pendingCount: 0,
resolvedCount: 0,
penaltiesCount: 1,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.penalties[0].notes).toBeNull();
});
it('should handle penalties without value', () => {
const apiDto = {
race: {
id: 'race-113',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-114',
},
pendingProtests: [],
resolvedProtests: [],
penalties: [
{
id: 'penalty-1',
driverId: 'driver-1',
type: 'disqualification',
value: null,
reason: 'Technical infringement',
notes: null,
},
],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
},
pendingCount: 0,
resolvedCount: 0,
penaltiesCount: 1,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.penalties[0].value).toBe(0);
});
it('should handle penalties without reason', () => {
const apiDto = {
race: {
id: 'race-115',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-116',
},
pendingProtests: [],
resolvedProtests: [],
penalties: [
{
id: 'penalty-1',
driverId: 'driver-1',
type: 'warning',
value: 0,
reason: null,
notes: null,
},
],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
},
pendingCount: 0,
resolvedCount: 0,
penaltiesCount: 1,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.penalties[0].reason).toBe('');
});
it('should handle different protest statuses', () => {
const apiDto = {
race: {
id: 'race-117',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-118',
},
pendingProtests: [
{
id: 'protest-1',
protestingDriverId: 'driver-1',
accusedDriverId: 'driver-2',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'pending',
proofVideoUrl: 'video-url',
decisionNotes: null,
},
],
resolvedProtests: [
{
id: 'protest-2',
protestingDriverId: 'driver-3',
accusedDriverId: 'driver-4',
incident: {
lap: 10,
description: 'Contact at turn 5',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'resolved',
proofVideoUrl: 'video-url',
decisionNotes: 'Penalty applied',
},
{
id: 'protest-3',
protestingDriverId: 'driver-5',
accusedDriverId: 'driver-6',
incident: {
lap: 15,
description: 'Contact at turn 7',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'rejected',
proofVideoUrl: 'video-url',
decisionNotes: 'Insufficient evidence',
},
],
penalties: [],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
'driver-2': { id: 'driver-2', name: 'Driver 2' },
'driver-3': { id: 'driver-3', name: 'Driver 3' },
'driver-4': { id: 'driver-4', name: 'Driver 4' },
'driver-5': { id: 'driver-5', name: 'Driver 5' },
'driver-6': { id: 'driver-6', name: 'Driver 6' },
},
pendingCount: 1,
resolvedCount: 2,
penaltiesCount: 0,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.pendingProtests[0].status).toBe('pending');
expect(result.resolvedProtests[0].status).toBe('resolved');
expect(result.resolvedProtests[1].status).toBe('rejected');
});
it('should handle different penalty types', () => {
const apiDto = {
race: {
id: 'race-119',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-120',
},
pendingProtests: [],
resolvedProtests: [],
penalties: [
{
id: 'penalty-1',
driverId: 'driver-1',
type: 'time_penalty',
value: 5,
reason: 'Track limits',
notes: 'Warning issued',
},
{
id: 'penalty-2',
driverId: 'driver-2',
type: 'grid_penalty',
value: 3,
reason: 'Qualifying infringement',
notes: null,
},
{
id: 'penalty-3',
driverId: 'driver-3',
type: 'points_deduction',
value: 10,
reason: 'Dangerous driving',
notes: null,
},
{
id: 'penalty-4',
driverId: 'driver-4',
type: 'disqualification',
value: 0,
reason: 'Technical infringement',
notes: null,
},
{
id: 'penalty-5',
driverId: 'driver-5',
type: 'warning',
value: 0,
reason: 'Minor infraction',
notes: null,
},
{
id: 'penalty-6',
driverId: 'driver-6',
type: 'license_points',
value: 2,
reason: 'Multiple incidents',
notes: null,
},
],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
'driver-2': { id: 'driver-2', name: 'Driver 2' },
'driver-3': { id: 'driver-3', name: 'Driver 3' },
'driver-4': { id: 'driver-4', name: 'Driver 4' },
'driver-5': { id: 'driver-5', name: 'Driver 5' },
'driver-6': { id: 'driver-6', name: 'Driver 6' },
},
pendingCount: 0,
resolvedCount: 0,
penaltiesCount: 6,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.penalties[0].type).toBe('time_penalty');
expect(result.penalties[1].type).toBe('grid_penalty');
expect(result.penalties[2].type).toBe('points_deduction');
expect(result.penalties[3].type).toBe('disqualification');
expect(result.penalties[4].type).toBe('warning');
expect(result.penalties[5].type).toBe('license_points');
});
it('should handle empty driver map', () => {
const apiDto = {
race: {
id: 'race-121',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-122',
},
pendingProtests: [],
resolvedProtests: [],
penalties: [],
driverMap: {},
pendingCount: 0,
resolvedCount: 0,
penaltiesCount: 0,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.driverMap).toEqual({});
});
it('should handle count values from DTO', () => {
const apiDto = {
race: {
id: 'race-123',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-124',
},
pendingProtests: [],
resolvedProtests: [],
penalties: [],
driverMap: {},
pendingCount: 5,
resolvedCount: 10,
penaltiesCount: 3,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.pendingCount).toBe(5);
expect(result.resolvedCount).toBe(10);
expect(result.penaltiesCount).toBe(3);
});
it('should calculate counts from arrays when not provided', () => {
const apiDto = {
race: {
id: 'race-125',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-126',
},
pendingProtests: [
{
id: 'protest-1',
protestingDriverId: 'driver-1',
accusedDriverId: 'driver-2',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'pending',
proofVideoUrl: 'video-url',
decisionNotes: null,
},
],
resolvedProtests: [
{
id: 'protest-2',
protestingDriverId: 'driver-3',
accusedDriverId: 'driver-4',
incident: {
lap: 10,
description: 'Contact at turn 5',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'resolved',
proofVideoUrl: 'video-url',
decisionNotes: 'Penalty applied',
},
],
penalties: [
{
id: 'penalty-1',
driverId: 'driver-5',
type: 'time_penalty',
value: 5,
reason: 'Track limits',
notes: 'Warning issued',
},
],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
'driver-2': { id: 'driver-2', name: 'Driver 2' },
'driver-3': { id: 'driver-3', name: 'Driver 3' },
'driver-4': { id: 'driver-4', name: 'Driver 4' },
'driver-5': { id: 'driver-5', name: 'Driver 5' },
},
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.pendingCount).toBe(1);
expect(result.resolvedCount).toBe(1);
expect(result.penaltiesCount).toBe(1);
});
});
});

View File

@@ -0,0 +1,187 @@
import { describe, it, expect } from 'vitest';
import { RacesViewDataBuilder } from './RacesViewDataBuilder';
import type { RacesPageDataDTO } from '@/lib/types/generated/RacesPageDataDTO';
describe('RacesViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform RacesPageDataDTO to RacesViewData correctly', () => {
const now = new Date();
const pastDate = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000);
const apiDto: RacesPageDataDTO = {
races: [
{
id: 'race-1',
track: 'Spa',
car: 'Porsche 911 GT3',
scheduledAt: pastDate.toISOString(),
status: 'completed',
leagueId: 'league-1',
leagueName: 'Pro League',
strengthOfField: 1500,
isUpcoming: false,
isLive: false,
isPast: true,
},
{
id: 'race-2',
track: 'Monza',
car: 'Ferrari 488 GT3',
scheduledAt: futureDate.toISOString(),
status: 'scheduled',
leagueId: 'league-1',
leagueName: 'Pro League',
strengthOfField: 1600,
isUpcoming: true,
isLive: false,
isPast: false,
},
],
};
const result = RacesViewDataBuilder.build(apiDto);
expect(result.races).toHaveLength(2);
expect(result.totalCount).toBe(2);
expect(result.completedCount).toBe(1);
expect(result.scheduledCount).toBe(1);
expect(result.leagues).toHaveLength(1);
expect(result.leagues[0]).toEqual({ id: 'league-1', name: 'Pro League' });
expect(result.upcomingRaces).toHaveLength(1);
expect(result.upcomingRaces[0].id).toBe('race-2');
expect(result.recentResults).toHaveLength(1);
expect(result.recentResults[0].id).toBe('race-1');
expect(result.racesByDate).toHaveLength(2);
});
it('should handle empty races list', () => {
const apiDto: RacesPageDataDTO = {
races: [],
};
const result = RacesViewDataBuilder.build(apiDto);
expect(result.races).toHaveLength(0);
expect(result.totalCount).toBe(0);
expect(result.leagues).toHaveLength(0);
expect(result.racesByDate).toHaveLength(0);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const now = new Date();
const apiDto: RacesPageDataDTO = {
races: [
{
id: 'race-1',
track: 'Spa',
car: 'Porsche 911 GT3',
scheduledAt: now.toISOString(),
status: 'scheduled',
leagueId: 'league-1',
leagueName: 'Pro League',
strengthOfField: 1500,
isUpcoming: true,
isLive: false,
isPast: false,
},
],
};
const result = RacesViewDataBuilder.build(apiDto);
expect(result.races[0].id).toBe(apiDto.races[0].id);
expect(result.races[0].track).toBe(apiDto.races[0].track);
expect(result.races[0].car).toBe(apiDto.races[0].car);
expect(result.races[0].scheduledAt).toBe(apiDto.races[0].scheduledAt);
expect(result.races[0].status).toBe(apiDto.races[0].status);
expect(result.races[0].leagueId).toBe(apiDto.races[0].leagueId);
expect(result.races[0].leagueName).toBe(apiDto.races[0].leagueName);
expect(result.races[0].strengthOfField).toBe(apiDto.races[0].strengthOfField);
});
it('should not modify the input DTO', () => {
const now = new Date();
const apiDto: RacesPageDataDTO = {
races: [
{
id: 'race-1',
track: 'Spa',
car: 'Porsche 911 GT3',
scheduledAt: now.toISOString(),
status: 'scheduled',
isUpcoming: true,
isLive: false,
isPast: false,
},
],
};
const originalDto = JSON.parse(JSON.stringify(apiDto));
RacesViewDataBuilder.build(apiDto);
expect(apiDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle races with missing optional fields', () => {
const now = new Date();
const apiDto: RacesPageDataDTO = {
races: [
{
id: 'race-1',
track: 'Spa',
car: 'Porsche 911 GT3',
scheduledAt: now.toISOString(),
status: 'scheduled',
isUpcoming: true,
isLive: false,
isPast: false,
},
],
};
const result = RacesViewDataBuilder.build(apiDto);
expect(result.races[0].leagueId).toBeUndefined();
expect(result.races[0].leagueName).toBeUndefined();
expect(result.races[0].strengthOfField).toBeNull();
});
it('should handle multiple races on the same date', () => {
const date = '2024-01-15T14:00:00.000Z';
const apiDto: RacesPageDataDTO = {
races: [
{
id: 'race-1',
track: 'Spa',
car: 'Porsche',
scheduledAt: date,
status: 'scheduled',
isUpcoming: true,
isLive: false,
isPast: false,
},
{
id: 'race-2',
track: 'Monza',
car: 'Ferrari',
scheduledAt: date,
status: 'scheduled',
isUpcoming: true,
isLive: false,
isPast: false,
},
],
};
const result = RacesViewDataBuilder.build(apiDto);
expect(result.racesByDate).toHaveLength(1);
expect(result.racesByDate[0].races).toHaveLength(2);
});
});
});

View File

@@ -0,0 +1,205 @@
import { describe, it, expect } from 'vitest';
import { ResetPasswordViewDataBuilder } from './ResetPasswordViewDataBuilder';
import type { ResetPasswordPageDTO } from '@/lib/services/auth/types/ResetPasswordPageDTO';
describe('ResetPasswordViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform ResetPasswordPageDTO to ResetPasswordViewData correctly', () => {
const resetPasswordPageDTO: ResetPasswordPageDTO = {
token: 'abc123def456',
returnTo: '/login',
};
const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO);
expect(result).toEqual({
token: 'abc123def456',
returnTo: '/login',
showSuccess: false,
formState: {
fields: {
newPassword: { value: '', error: undefined, touched: false, validating: false },
confirmPassword: { value: '', error: undefined, touched: false, validating: false },
},
isValid: true,
isSubmitting: false,
submitError: undefined,
submitCount: 0,
},
isSubmitting: false,
submitError: undefined,
});
});
it('should handle empty returnTo path', () => {
const resetPasswordPageDTO: ResetPasswordPageDTO = {
token: 'abc123def456',
returnTo: '',
};
const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO);
expect(result.returnTo).toBe('');
});
it('should handle returnTo with query parameters', () => {
const resetPasswordPageDTO: ResetPasswordPageDTO = {
token: 'abc123def456',
returnTo: '/login?success=true',
};
const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO);
expect(result.returnTo).toBe('/login?success=true');
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const resetPasswordPageDTO: ResetPasswordPageDTO = {
token: 'abc123def456',
returnTo: '/login',
};
const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO);
expect(result.token).toBe(resetPasswordPageDTO.token);
expect(result.returnTo).toBe(resetPasswordPageDTO.returnTo);
});
it('should not modify the input DTO', () => {
const resetPasswordPageDTO: ResetPasswordPageDTO = {
token: 'abc123def456',
returnTo: '/login',
};
const originalDTO = { ...resetPasswordPageDTO };
ResetPasswordViewDataBuilder.build(resetPasswordPageDTO);
expect(resetPasswordPageDTO).toEqual(originalDTO);
});
it('should initialize form fields with default values', () => {
const resetPasswordPageDTO: ResetPasswordPageDTO = {
token: 'abc123def456',
returnTo: '/login',
};
const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO);
expect(result.formState.fields.newPassword.value).toBe('');
expect(result.formState.fields.newPassword.error).toBeUndefined();
expect(result.formState.fields.newPassword.touched).toBe(false);
expect(result.formState.fields.newPassword.validating).toBe(false);
expect(result.formState.fields.confirmPassword.value).toBe('');
expect(result.formState.fields.confirmPassword.error).toBeUndefined();
expect(result.formState.fields.confirmPassword.touched).toBe(false);
expect(result.formState.fields.confirmPassword.validating).toBe(false);
});
it('should initialize form state with default values', () => {
const resetPasswordPageDTO: ResetPasswordPageDTO = {
token: 'abc123def456',
returnTo: '/login',
};
const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO);
expect(result.formState.isValid).toBe(true);
expect(result.formState.isSubmitting).toBe(false);
expect(result.formState.submitError).toBeUndefined();
expect(result.formState.submitCount).toBe(0);
});
it('should initialize UI state flags correctly', () => {
const resetPasswordPageDTO: ResetPasswordPageDTO = {
token: 'abc123def456',
returnTo: '/login',
};
const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO);
expect(result.showSuccess).toBe(false);
expect(result.isSubmitting).toBe(false);
expect(result.submitError).toBeUndefined();
});
});
describe('edge cases', () => {
it('should handle token with special characters', () => {
const resetPasswordPageDTO: ResetPasswordPageDTO = {
token: 'abc-123_def.456',
returnTo: '/login',
};
const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO);
expect(result.token).toBe('abc-123_def.456');
});
it('should handle token with URL-encoded characters', () => {
const resetPasswordPageDTO: ResetPasswordPageDTO = {
token: 'abc%20123%40def',
returnTo: '/login',
};
const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO);
expect(result.token).toBe('abc%20123%40def');
});
it('should handle returnTo with encoded characters', () => {
const resetPasswordPageDTO: ResetPasswordPageDTO = {
token: 'abc123def456',
returnTo: '/login?redirect=%2Fdashboard',
};
const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO);
expect(result.returnTo).toBe('/login?redirect=%2Fdashboard');
});
it('should handle returnTo with hash fragment', () => {
const resetPasswordPageDTO: ResetPasswordPageDTO = {
token: 'abc123def456',
returnTo: '/login#section',
};
const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO);
expect(result.returnTo).toBe('/login#section');
});
});
describe('form state structure', () => {
it('should have all required form fields', () => {
const resetPasswordPageDTO: ResetPasswordPageDTO = {
token: 'abc123def456',
returnTo: '/login',
};
const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO);
expect(result.formState.fields).toHaveProperty('newPassword');
expect(result.formState.fields).toHaveProperty('confirmPassword');
});
it('should have consistent field state structure', () => {
const resetPasswordPageDTO: ResetPasswordPageDTO = {
token: 'abc123def456',
returnTo: '/login',
};
const result = ResetPasswordViewDataBuilder.build(resetPasswordPageDTO);
const fields = result.formState.fields;
Object.values(fields).forEach((field) => {
expect(field).toHaveProperty('value');
expect(field).toHaveProperty('error');
expect(field).toHaveProperty('touched');
expect(field).toHaveProperty('validating');
});
});
});
});

View File

@@ -0,0 +1,407 @@
import { describe, it, expect } from 'vitest';
import { RulebookViewDataBuilder } from './RulebookViewDataBuilder';
import type { RulebookApiDto } from '@/lib/types/tbd/RulebookApiDto';
describe('RulebookViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform RulebookApiDto to RulebookViewData correctly', () => {
const rulebookApiDto: RulebookApiDto = {
leagueId: 'league-123',
scoringConfig: {
gameName: 'iRacing',
scoringPresetName: 'Standard',
championships: [
{
type: 'driver',
sessionTypes: ['race'],
pointsPreview: [
{ sessionType: 'race', position: 1, points: 25 },
{ sessionType: 'race', position: 2, points: 18 },
{ sessionType: 'race', position: 3, points: 15 },
],
bonusSummary: [
{ type: 'fastest_lap', points: 5, description: 'Fastest lap' },
],
},
],
dropPolicySummary: 'Drop 2 worst results',
},
};
const result = RulebookViewDataBuilder.build(rulebookApiDto);
expect(result).toEqual({
leagueId: 'league-123',
gameName: 'iRacing',
scoringPresetName: 'Standard',
championshipsCount: 1,
sessionTypes: 'race',
dropPolicySummary: 'Drop 2 worst results',
hasActiveDropPolicy: true,
positionPoints: [
{ position: 1, points: 25 },
{ position: 2, points: 18 },
{ position: 3, points: 15 },
],
bonusPoints: [
{ type: 'fastest_lap', points: 5, description: 'Fastest lap' },
],
hasBonusPoints: true,
});
});
it('should handle championship without driver type', () => {
const rulebookApiDto: RulebookApiDto = {
leagueId: 'league-456',
scoringConfig: {
gameName: 'iRacing',
scoringPresetName: 'Standard',
championships: [
{
type: 'team',
sessionTypes: ['race'],
pointsPreview: [
{ sessionType: 'race', position: 1, points: 25 },
],
bonusSummary: [],
},
],
dropPolicySummary: 'No drops',
},
};
const result = RulebookViewDataBuilder.build(rulebookApiDto);
expect(result.positionPoints).toEqual([{ position: 1, points: 25 }]);
});
it('should handle multiple championships', () => {
const rulebookApiDto: RulebookApiDto = {
leagueId: 'league-789',
scoringConfig: {
gameName: 'iRacing',
scoringPresetName: 'Standard',
championships: [
{
type: 'driver',
sessionTypes: ['race'],
pointsPreview: [
{ sessionType: 'race', position: 1, points: 25 },
],
bonusSummary: [],
},
{
type: 'team',
sessionTypes: ['race'],
pointsPreview: [
{ sessionType: 'race', position: 1, points: 25 },
],
bonusSummary: [],
},
],
dropPolicySummary: 'No drops',
},
};
const result = RulebookViewDataBuilder.build(rulebookApiDto);
expect(result.championshipsCount).toBe(2);
});
it('should handle empty bonus points', () => {
const rulebookApiDto: RulebookApiDto = {
leagueId: 'league-101',
scoringConfig: {
gameName: 'iRacing',
scoringPresetName: 'Standard',
championships: [
{
type: 'driver',
sessionTypes: ['race'],
pointsPreview: [
{ sessionType: 'race', position: 1, points: 25 },
],
bonusSummary: [],
},
],
dropPolicySummary: 'No drops',
},
};
const result = RulebookViewDataBuilder.build(rulebookApiDto);
expect(result.bonusPoints).toEqual([]);
expect(result.hasBonusPoints).toBe(false);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const rulebookApiDto: RulebookApiDto = {
leagueId: 'league-102',
scoringConfig: {
gameName: 'iRacing',
scoringPresetName: 'Standard',
championships: [
{
type: 'driver',
sessionTypes: ['race'],
pointsPreview: [
{ sessionType: 'race', position: 1, points: 25 },
],
bonusSummary: [
{ type: 'fastest_lap', points: 5, description: 'Fastest lap' },
],
},
],
dropPolicySummary: 'Drop 2 worst results',
},
};
const result = RulebookViewDataBuilder.build(rulebookApiDto);
expect(result.leagueId).toBe(rulebookApiDto.leagueId);
expect(result.gameName).toBe(rulebookApiDto.scoringConfig.gameName);
expect(result.scoringPresetName).toBe(rulebookApiDto.scoringConfig.scoringPresetName);
expect(result.dropPolicySummary).toBe(rulebookApiDto.scoringConfig.dropPolicySummary);
});
it('should not modify the input DTO', () => {
const rulebookApiDto: RulebookApiDto = {
leagueId: 'league-103',
scoringConfig: {
gameName: 'iRacing',
scoringPresetName: 'Standard',
championships: [
{
type: 'driver',
sessionTypes: ['race'],
pointsPreview: [
{ sessionType: 'race', position: 1, points: 25 },
],
bonusSummary: [],
},
],
dropPolicySummary: 'No drops',
},
};
const originalDto = { ...rulebookApiDto };
RulebookViewDataBuilder.build(rulebookApiDto);
expect(rulebookApiDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle empty drop policy', () => {
const rulebookApiDto: RulebookApiDto = {
leagueId: 'league-104',
scoringConfig: {
gameName: 'iRacing',
scoringPresetName: 'Standard',
championships: [
{
type: 'driver',
sessionTypes: ['race'],
pointsPreview: [
{ sessionType: 'race', position: 1, points: 25 },
],
bonusSummary: [],
},
],
dropPolicySummary: '',
},
};
const result = RulebookViewDataBuilder.build(rulebookApiDto);
expect(result.hasActiveDropPolicy).toBe(false);
});
it('should handle drop policy with "All" keyword', () => {
const rulebookApiDto: RulebookApiDto = {
leagueId: 'league-105',
scoringConfig: {
gameName: 'iRacing',
scoringPresetName: 'Standard',
championships: [
{
type: 'driver',
sessionTypes: ['race'],
pointsPreview: [
{ sessionType: 'race', position: 1, points: 25 },
],
bonusSummary: [],
},
],
dropPolicySummary: 'Drop all results',
},
};
const result = RulebookViewDataBuilder.build(rulebookApiDto);
expect(result.hasActiveDropPolicy).toBe(false);
});
it('should handle multiple session types', () => {
const rulebookApiDto: RulebookApiDto = {
leagueId: 'league-106',
scoringConfig: {
gameName: 'iRacing',
scoringPresetName: 'Standard',
championships: [
{
type: 'driver',
sessionTypes: ['race', 'qualifying', 'practice'],
pointsPreview: [
{ sessionType: 'race', position: 1, points: 25 },
],
bonusSummary: [],
},
],
dropPolicySummary: 'No drops',
},
};
const result = RulebookViewDataBuilder.build(rulebookApiDto);
expect(result.sessionTypes).toBe('race, qualifying, practice');
});
it('should handle single session type', () => {
const rulebookApiDto: RulebookApiDto = {
leagueId: 'league-107',
scoringConfig: {
gameName: 'iRacing',
scoringPresetName: 'Standard',
championships: [
{
type: 'driver',
sessionTypes: ['race'],
pointsPreview: [
{ sessionType: 'race', position: 1, points: 25 },
],
bonusSummary: [],
},
],
dropPolicySummary: 'No drops',
},
};
const result = RulebookViewDataBuilder.build(rulebookApiDto);
expect(result.sessionTypes).toBe('race');
});
it('should handle empty points preview', () => {
const rulebookApiDto: RulebookApiDto = {
leagueId: 'league-108',
scoringConfig: {
gameName: 'iRacing',
scoringPresetName: 'Standard',
championships: [
{
type: 'driver',
sessionTypes: ['race'],
pointsPreview: [],
bonusSummary: [],
},
],
dropPolicySummary: 'No drops',
},
};
const result = RulebookViewDataBuilder.build(rulebookApiDto);
expect(result.positionPoints).toEqual([]);
});
it('should handle points preview with different session types', () => {
const rulebookApiDto: RulebookApiDto = {
leagueId: 'league-109',
scoringConfig: {
gameName: 'iRacing',
scoringPresetName: 'Standard',
championships: [
{
type: 'driver',
sessionTypes: ['race'],
pointsPreview: [
{ sessionType: 'race', position: 1, points: 25 },
{ sessionType: 'qualifying', position: 1, points: 10 },
],
bonusSummary: [],
},
],
dropPolicySummary: 'No drops',
},
};
const result = RulebookViewDataBuilder.build(rulebookApiDto);
expect(result.positionPoints).toEqual([{ position: 1, points: 25 }]);
});
it('should handle points preview with non-sequential positions', () => {
const rulebookApiDto: RulebookApiDto = {
leagueId: 'league-110',
scoringConfig: {
gameName: 'iRacing',
scoringPresetName: 'Standard',
championships: [
{
type: 'driver',
sessionTypes: ['race'],
pointsPreview: [
{ sessionType: 'race', position: 1, points: 25 },
{ sessionType: 'race', position: 3, points: 15 },
{ sessionType: 'race', position: 2, points: 18 },
],
bonusSummary: [],
},
],
dropPolicySummary: 'No drops',
},
};
const result = RulebookViewDataBuilder.build(rulebookApiDto);
expect(result.positionPoints).toEqual([
{ position: 1, points: 25 },
{ position: 2, points: 18 },
{ position: 3, points: 15 },
]);
});
it('should handle multiple bonus points', () => {
const rulebookApiDto: RulebookApiDto = {
leagueId: 'league-111',
scoringConfig: {
gameName: 'iRacing',
scoringPresetName: 'Standard',
championships: [
{
type: 'driver',
sessionTypes: ['race'],
pointsPreview: [
{ sessionType: 'race', position: 1, points: 25 },
],
bonusSummary: [
{ type: 'fastest_lap', points: 5, description: 'Fastest lap' },
{ type: 'pole_position', points: 3, description: 'Pole position' },
{ type: 'clean_race', points: 2, description: 'Clean race' },
],
},
],
dropPolicySummary: 'No drops',
},
};
const result = RulebookViewDataBuilder.build(rulebookApiDto);
expect(result.bonusPoints).toHaveLength(3);
expect(result.hasBonusPoints).toBe(true);
});
});
});

View File

@@ -0,0 +1,188 @@
import { describe, it, expect } from 'vitest';
import { SignupViewDataBuilder } from './SignupViewDataBuilder';
import type { SignupPageDTO } from '@/lib/services/auth/types/SignupPageDTO';
describe('SignupViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform SignupPageDTO to SignupViewData correctly', () => {
const signupPageDTO: SignupPageDTO = {
returnTo: '/dashboard',
};
const result = SignupViewDataBuilder.build(signupPageDTO);
expect(result).toEqual({
returnTo: '/dashboard',
formState: {
fields: {
firstName: { value: '', error: undefined, touched: false, validating: false },
lastName: { value: '', error: undefined, touched: false, validating: false },
email: { value: '', error: undefined, touched: false, validating: false },
password: { value: '', error: undefined, touched: false, validating: false },
confirmPassword: { value: '', error: undefined, touched: false, validating: false },
},
isValid: true,
isSubmitting: false,
submitError: undefined,
submitCount: 0,
},
isSubmitting: false,
submitError: undefined,
});
});
it('should handle empty returnTo path', () => {
const signupPageDTO: SignupPageDTO = {
returnTo: '',
};
const result = SignupViewDataBuilder.build(signupPageDTO);
expect(result.returnTo).toBe('');
});
it('should handle returnTo with query parameters', () => {
const signupPageDTO: SignupPageDTO = {
returnTo: '/dashboard?welcome=true',
};
const result = SignupViewDataBuilder.build(signupPageDTO);
expect(result.returnTo).toBe('/dashboard?welcome=true');
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const signupPageDTO: SignupPageDTO = {
returnTo: '/dashboard',
};
const result = SignupViewDataBuilder.build(signupPageDTO);
expect(result.returnTo).toBe(signupPageDTO.returnTo);
});
it('should not modify the input DTO', () => {
const signupPageDTO: SignupPageDTO = {
returnTo: '/dashboard',
};
const originalDTO = { ...signupPageDTO };
SignupViewDataBuilder.build(signupPageDTO);
expect(signupPageDTO).toEqual(originalDTO);
});
it('should initialize all signup form fields with default values', () => {
const signupPageDTO: SignupPageDTO = {
returnTo: '/dashboard',
};
const result = SignupViewDataBuilder.build(signupPageDTO);
expect(result.formState.fields.firstName.value).toBe('');
expect(result.formState.fields.firstName.error).toBeUndefined();
expect(result.formState.fields.firstName.touched).toBe(false);
expect(result.formState.fields.firstName.validating).toBe(false);
expect(result.formState.fields.lastName.value).toBe('');
expect(result.formState.fields.lastName.error).toBeUndefined();
expect(result.formState.fields.lastName.touched).toBe(false);
expect(result.formState.fields.lastName.validating).toBe(false);
expect(result.formState.fields.email.value).toBe('');
expect(result.formState.fields.email.error).toBeUndefined();
expect(result.formState.fields.email.touched).toBe(false);
expect(result.formState.fields.email.validating).toBe(false);
expect(result.formState.fields.password.value).toBe('');
expect(result.formState.fields.password.error).toBeUndefined();
expect(result.formState.fields.password.touched).toBe(false);
expect(result.formState.fields.password.validating).toBe(false);
expect(result.formState.fields.confirmPassword.value).toBe('');
expect(result.formState.fields.confirmPassword.error).toBeUndefined();
expect(result.formState.fields.confirmPassword.touched).toBe(false);
expect(result.formState.fields.confirmPassword.validating).toBe(false);
});
it('should initialize form state with default values', () => {
const signupPageDTO: SignupPageDTO = {
returnTo: '/dashboard',
};
const result = SignupViewDataBuilder.build(signupPageDTO);
expect(result.formState.isValid).toBe(true);
expect(result.formState.isSubmitting).toBe(false);
expect(result.formState.submitError).toBeUndefined();
expect(result.formState.submitCount).toBe(0);
});
it('should initialize UI state flags correctly', () => {
const signupPageDTO: SignupPageDTO = {
returnTo: '/dashboard',
};
const result = SignupViewDataBuilder.build(signupPageDTO);
expect(result.isSubmitting).toBe(false);
expect(result.submitError).toBeUndefined();
});
});
describe('edge cases', () => {
it('should handle returnTo with encoded characters', () => {
const signupPageDTO: SignupPageDTO = {
returnTo: '/dashboard?redirect=%2Fadmin',
};
const result = SignupViewDataBuilder.build(signupPageDTO);
expect(result.returnTo).toBe('/dashboard?redirect=%2Fadmin');
});
it('should handle returnTo with hash fragment', () => {
const signupPageDTO: SignupPageDTO = {
returnTo: '/dashboard#section',
};
const result = SignupViewDataBuilder.build(signupPageDTO);
expect(result.returnTo).toBe('/dashboard#section');
});
});
describe('form state structure', () => {
it('should have all required form fields', () => {
const signupPageDTO: SignupPageDTO = {
returnTo: '/dashboard',
};
const result = SignupViewDataBuilder.build(signupPageDTO);
expect(result.formState.fields).toHaveProperty('firstName');
expect(result.formState.fields).toHaveProperty('lastName');
expect(result.formState.fields).toHaveProperty('email');
expect(result.formState.fields).toHaveProperty('password');
expect(result.formState.fields).toHaveProperty('confirmPassword');
});
it('should have consistent field state structure', () => {
const signupPageDTO: SignupPageDTO = {
returnTo: '/dashboard',
};
const result = SignupViewDataBuilder.build(signupPageDTO);
const fields = result.formState.fields;
Object.values(fields).forEach((field) => {
expect(field).toHaveProperty('value');
expect(field).toHaveProperty('error');
expect(field).toHaveProperty('touched');
expect(field).toHaveProperty('validating');
});
});
});
});

View File

@@ -0,0 +1,95 @@
import { describe, it, expect } from 'vitest';
import { SponsorDashboardViewDataBuilder } from './SponsorDashboardViewDataBuilder';
import type { SponsorDashboardDTO } from '@/lib/types/generated/SponsorDashboardDTO';
describe('SponsorDashboardViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform SponsorDashboardDTO to SponsorDashboardViewData correctly', () => {
const apiDto: SponsorDashboardDTO = {
sponsorName: 'Test Sponsor',
metrics: {
impressions: 5000,
viewers: 1000,
exposure: 500,
},
investment: {
activeSponsorships: 5,
totalSpent: 5000,
},
sponsorships: [],
};
const result = SponsorDashboardViewDataBuilder.build(apiDto);
expect(result.sponsorName).toBe('Test Sponsor');
expect(result.totalImpressions).toBe('5,000');
expect(result.totalInvestment).toBe('$5,000.00');
expect(result.activeSponsorships).toBe(5);
expect(result.metrics.impressionsChange).toBe(15);
});
it('should handle low impressions correctly', () => {
const apiDto: SponsorDashboardDTO = {
sponsorName: 'Test Sponsor',
metrics: {
impressions: 500,
viewers: 100,
exposure: 50,
},
investment: {
activeSponsorships: 1,
totalSpent: 1000,
},
sponsorships: [],
};
const result = SponsorDashboardViewDataBuilder.build(apiDto);
expect(result.metrics.impressionsChange).toBe(-5);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const apiDto: SponsorDashboardDTO = {
sponsorName: 'Test Sponsor',
metrics: {
impressions: 5000,
viewers: 1000,
exposure: 500,
},
investment: {
activeSponsorships: 5,
totalSpent: 5000,
},
sponsorships: [],
};
const result = SponsorDashboardViewDataBuilder.build(apiDto);
expect(result.sponsorName).toBe(apiDto.sponsorName);
expect(result.activeSponsorships).toBe(apiDto.investment.activeSponsorships);
});
it('should not modify the input DTO', () => {
const apiDto: SponsorDashboardDTO = {
sponsorName: 'Test Sponsor',
metrics: {
impressions: 5000,
viewers: 1000,
exposure: 500,
},
investment: {
activeSponsorships: 5,
totalSpent: 5000,
},
sponsorships: [],
};
const originalDto = JSON.parse(JSON.stringify(apiDto));
SponsorDashboardViewDataBuilder.build(apiDto);
expect(apiDto).toEqual(originalDto);
});
});
});

View File

@@ -0,0 +1,165 @@
import { describe, it, expect } from 'vitest';
import { SponsorLogoViewDataBuilder } from './SponsorLogoViewDataBuilder';
import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
describe('SponsorLogoViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform MediaBinaryDTO to SponsorLogoViewData correctly', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = SponsorLogoViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
it('should handle JPEG sponsor logos', () => {
const buffer = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/jpeg',
};
const result = SponsorLogoViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/jpeg');
});
it('should handle SVG sponsor logos', () => {
const buffer = new TextEncoder().encode('<svg xmlns="http://www.w3.org/2000/svg"><text x="10" y="20">Sponsor</text></svg>');
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/svg+xml',
};
const result = SponsorLogoViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/svg+xml');
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = SponsorLogoViewDataBuilder.build(mediaDto);
expect(result.buffer).toBeDefined();
expect(result.contentType).toBe(mediaDto.contentType);
});
it('should not modify the input DTO', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const originalDto = { ...mediaDto };
SponsorLogoViewDataBuilder.build(mediaDto);
expect(mediaDto).toEqual(originalDto);
});
it('should convert buffer to base64 string', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = SponsorLogoViewDataBuilder.build(mediaDto);
expect(typeof result.buffer).toBe('string');
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
});
});
describe('edge cases', () => {
it('should handle empty buffer', () => {
const buffer = new Uint8Array([]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = SponsorLogoViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe('');
expect(result.contentType).toBe('image/png');
});
it('should handle large sponsor logos', () => {
const buffer = new Uint8Array(3 * 1024 * 1024); // 3MB
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/jpeg',
};
const result = SponsorLogoViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/jpeg');
});
it('should handle buffer with all zeros', () => {
const buffer = new Uint8Array([0x00, 0x00, 0x00, 0x00]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = SponsorLogoViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
it('should handle buffer with all ones', () => {
const buffer = new Uint8Array([0xff, 0xff, 0xff, 0xff]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = SponsorLogoViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
it('should handle different content types', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const contentTypes = [
'image/png',
'image/jpeg',
'image/gif',
'image/webp',
'image/svg+xml',
'image/bmp',
'image/tiff',
];
contentTypes.forEach((contentType) => {
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType,
};
const result = SponsorLogoViewDataBuilder.build(mediaDto);
expect(result.contentType).toBe(contentType);
});
});
});
});

View File

@@ -0,0 +1,223 @@
import { describe, it, expect } from 'vitest';
import { SponsorshipRequestsPageViewDataBuilder } from './SponsorshipRequestsPageViewDataBuilder';
import type { GetPendingSponsorshipRequestsOutputDTO } from '@/lib/types/generated/GetPendingSponsorshipRequestsOutputDTO';
describe('SponsorshipRequestsPageViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform GetPendingSponsorshipRequestsOutputDTO to SponsorshipRequestsViewData correctly', () => {
const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'driver',
entityId: 'driver-123',
requests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorLogo: 'logo-url',
message: 'Test message',
createdAt: '2024-01-01T10:00:00Z',
},
],
};
const result = SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto);
expect(result).toEqual({
sections: [
{
entityType: 'driver',
entityId: 'driver-123',
entityName: 'driver',
requests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorLogoUrl: 'logo-url',
message: 'Test message',
createdAtIso: '2024-01-01T10:00:00Z',
},
],
},
],
});
});
it('should handle empty requests', () => {
const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'team',
entityId: 'team-456',
requests: [],
};
const result = SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto);
expect(result.sections).toHaveLength(1);
expect(result.sections[0].requests).toHaveLength(0);
});
it('should handle multiple requests', () => {
const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'season',
entityId: 'season-789',
requests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Sponsor 1',
sponsorLogo: 'logo-1',
message: 'Message 1',
createdAt: '2024-01-01T10:00:00Z',
},
{
id: 'request-2',
sponsorId: 'sponsor-2',
sponsorName: 'Sponsor 2',
sponsorLogo: 'logo-2',
message: 'Message 2',
createdAt: '2024-01-02T10:00:00Z',
},
],
};
const result = SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto);
expect(result.sections[0].requests).toHaveLength(2);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'driver',
entityId: 'driver-101',
requests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorLogo: 'logo-url',
message: 'Test message',
createdAt: '2024-01-01T10:00:00Z',
},
],
};
const result = SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto);
expect(result.sections[0].entityType).toBe(sponsorshipRequestsPageDto.entityType);
expect(result.sections[0].entityId).toBe(sponsorshipRequestsPageDto.entityId);
expect(result.sections[0].requests[0].id).toBe(sponsorshipRequestsPageDto.requests[0].id);
expect(result.sections[0].requests[0].sponsorId).toBe(sponsorshipRequestsPageDto.requests[0].sponsorId);
expect(result.sections[0].requests[0].sponsorName).toBe(sponsorshipRequestsPageDto.requests[0].sponsorName);
expect(result.sections[0].requests[0].sponsorLogoUrl).toBe(sponsorshipRequestsPageDto.requests[0].sponsorLogo);
expect(result.sections[0].requests[0].message).toBe(sponsorshipRequestsPageDto.requests[0].message);
expect(result.sections[0].requests[0].createdAtIso).toBe(sponsorshipRequestsPageDto.requests[0].createdAt);
});
it('should not modify the input DTO', () => {
const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'team',
entityId: 'team-102',
requests: [],
};
const originalDto = { ...sponsorshipRequestsPageDto };
SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto);
expect(sponsorshipRequestsPageDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle requests without sponsor logo', () => {
const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'driver',
entityId: 'driver-103',
requests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorLogo: null,
message: 'Test message',
createdAt: '2024-01-01T10:00:00Z',
},
],
};
const result = SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto);
expect(result.sections[0].requests[0].sponsorLogoUrl).toBeNull();
});
it('should handle requests without message', () => {
const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'driver',
entityId: 'driver-104',
requests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorLogo: 'logo-url',
message: null,
createdAt: '2024-01-01T10:00:00Z',
},
],
};
const result = SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto);
expect(result.sections[0].requests[0].message).toBeNull();
});
it('should handle different entity types', () => {
const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'team',
entityId: 'team-105',
requests: [],
};
const result = SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto);
expect(result.sections[0].entityType).toBe('team');
});
it('should handle entity name for driver type', () => {
const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'driver',
entityId: 'driver-106',
requests: [],
};
const result = SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto);
expect(result.sections[0].entityName).toBe('driver');
});
it('should handle entity name for team type', () => {
const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'team',
entityId: 'team-107',
requests: [],
};
const result = SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto);
expect(result.sections[0].entityName).toBe('team');
});
it('should handle entity name for season type', () => {
const sponsorshipRequestsPageDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'season',
entityId: 'season-108',
requests: [],
};
const result = SponsorshipRequestsPageViewDataBuilder.build(sponsorshipRequestsPageDto);
expect(result.sections[0].entityName).toBe('season');
});
});
});

View File

@@ -0,0 +1,223 @@
import { describe, it, expect } from 'vitest';
import { SponsorshipRequestsViewDataBuilder } from './SponsorshipRequestsViewDataBuilder';
import type { GetPendingSponsorshipRequestsOutputDTO } from '@/lib/types/generated/GetPendingSponsorshipRequestsOutputDTO';
describe('SponsorshipRequestsViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform GetPendingSponsorshipRequestsOutputDTO to SponsorshipRequestsViewData correctly', () => {
const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'driver',
entityId: 'driver-123',
requests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorLogo: 'logo-url',
message: 'Test message',
createdAt: '2024-01-01T10:00:00Z',
},
],
};
const result = SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto);
expect(result).toEqual({
sections: [
{
entityType: 'driver',
entityId: 'driver-123',
entityName: 'Driver',
requests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorLogoUrl: 'logo-url',
message: 'Test message',
createdAtIso: '2024-01-01T10:00:00Z',
},
],
},
],
});
});
it('should handle empty requests', () => {
const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'team',
entityId: 'team-456',
requests: [],
};
const result = SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto);
expect(result.sections).toHaveLength(1);
expect(result.sections[0].requests).toHaveLength(0);
});
it('should handle multiple requests', () => {
const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'season',
entityId: 'season-789',
requests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Sponsor 1',
sponsorLogo: 'logo-1',
message: 'Message 1',
createdAt: '2024-01-01T10:00:00Z',
},
{
id: 'request-2',
sponsorId: 'sponsor-2',
sponsorName: 'Sponsor 2',
sponsorLogo: 'logo-2',
message: 'Message 2',
createdAt: '2024-01-02T10:00:00Z',
},
],
};
const result = SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto);
expect(result.sections[0].requests).toHaveLength(2);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'driver',
entityId: 'driver-101',
requests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorLogo: 'logo-url',
message: 'Test message',
createdAt: '2024-01-01T10:00:00Z',
},
],
};
const result = SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto);
expect(result.sections[0].entityType).toBe(sponsorshipRequestsDto.entityType);
expect(result.sections[0].entityId).toBe(sponsorshipRequestsDto.entityId);
expect(result.sections[0].requests[0].id).toBe(sponsorshipRequestsDto.requests[0].id);
expect(result.sections[0].requests[0].sponsorId).toBe(sponsorshipRequestsDto.requests[0].sponsorId);
expect(result.sections[0].requests[0].sponsorName).toBe(sponsorshipRequestsDto.requests[0].sponsorName);
expect(result.sections[0].requests[0].sponsorLogoUrl).toBe(sponsorshipRequestsDto.requests[0].sponsorLogo);
expect(result.sections[0].requests[0].message).toBe(sponsorshipRequestsDto.requests[0].message);
expect(result.sections[0].requests[0].createdAtIso).toBe(sponsorshipRequestsDto.requests[0].createdAt);
});
it('should not modify the input DTO', () => {
const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'team',
entityId: 'team-102',
requests: [],
};
const originalDto = { ...sponsorshipRequestsDto };
SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto);
expect(sponsorshipRequestsDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle requests without sponsor logo', () => {
const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'driver',
entityId: 'driver-103',
requests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorLogo: null,
message: 'Test message',
createdAt: '2024-01-01T10:00:00Z',
},
],
};
const result = SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto);
expect(result.sections[0].requests[0].sponsorLogoUrl).toBeNull();
});
it('should handle requests without message', () => {
const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'driver',
entityId: 'driver-104',
requests: [
{
id: 'request-1',
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorLogo: 'logo-url',
message: null,
createdAt: '2024-01-01T10:00:00Z',
},
],
};
const result = SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto);
expect(result.sections[0].requests[0].message).toBeNull();
});
it('should handle different entity types', () => {
const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'team',
entityId: 'team-105',
requests: [],
};
const result = SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto);
expect(result.sections[0].entityType).toBe('team');
});
it('should handle entity name for driver type', () => {
const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'driver',
entityId: 'driver-106',
requests: [],
};
const result = SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto);
expect(result.sections[0].entityName).toBe('Driver');
});
it('should handle entity name for team type', () => {
const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'team',
entityId: 'team-107',
requests: [],
};
const result = SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto);
expect(result.sections[0].entityName).toBe('team');
});
it('should handle entity name for season type', () => {
const sponsorshipRequestsDto: GetPendingSponsorshipRequestsOutputDTO = {
entityType: 'season',
entityId: 'season-108',
requests: [],
};
const result = SponsorshipRequestsViewDataBuilder.build(sponsorshipRequestsDto);
expect(result.sections[0].entityName).toBe('season');
});
});
});

View File

@@ -0,0 +1,349 @@
import { describe, it, expect } from 'vitest';
import { StewardingViewDataBuilder } from './StewardingViewDataBuilder';
import type { StewardingApiDto } from '@/lib/types/tbd/StewardingApiDto';
describe('StewardingViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform StewardingApiDto to StewardingViewData correctly', () => {
const stewardingApiDto: StewardingApiDto = {
leagueId: 'league-123',
totalPending: 5,
totalResolved: 10,
totalPenalties: 3,
races: [
{
id: 'race-1',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
pendingProtests: ['protest-1', 'protest-2'],
resolvedProtests: ['protest-3'],
penalties: ['penalty-1'],
},
],
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
},
],
};
const result = StewardingViewDataBuilder.build(stewardingApiDto);
expect(result).toEqual({
leagueId: 'league-123',
totalPending: 5,
totalResolved: 10,
totalPenalties: 3,
races: [
{
id: 'race-1',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
pendingProtests: ['protest-1', 'protest-2'],
resolvedProtests: ['protest-3'],
penalties: ['penalty-1'],
},
],
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
},
],
});
});
it('should handle empty races and drivers', () => {
const stewardingApiDto: StewardingApiDto = {
leagueId: 'league-456',
totalPending: 0,
totalResolved: 0,
totalPenalties: 0,
races: [],
drivers: [],
};
const result = StewardingViewDataBuilder.build(stewardingApiDto);
expect(result.races).toHaveLength(0);
expect(result.drivers).toHaveLength(0);
});
it('should handle multiple races and drivers', () => {
const stewardingApiDto: StewardingApiDto = {
leagueId: 'league-789',
totalPending: 10,
totalResolved: 20,
totalPenalties: 5,
races: [
{
id: 'race-1',
track: 'Test Track 1',
scheduledAt: '2024-01-01T10:00:00Z',
pendingProtests: ['protest-1'],
resolvedProtests: ['protest-2'],
penalties: ['penalty-1'],
},
{
id: 'race-2',
track: 'Test Track 2',
scheduledAt: '2024-01-02T10:00:00Z',
pendingProtests: ['protest-3'],
resolvedProtests: ['protest-4'],
penalties: ['penalty-2'],
},
],
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
},
{
id: 'driver-2',
name: 'Driver 2',
},
],
};
const result = StewardingViewDataBuilder.build(stewardingApiDto);
expect(result.races).toHaveLength(2);
expect(result.drivers).toHaveLength(2);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const stewardingApiDto: StewardingApiDto = {
leagueId: 'league-101',
totalPending: 5,
totalResolved: 10,
totalPenalties: 3,
races: [
{
id: 'race-1',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
pendingProtests: ['protest-1'],
resolvedProtests: ['protest-2'],
penalties: ['penalty-1'],
},
],
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
},
],
};
const result = StewardingViewDataBuilder.build(stewardingApiDto);
expect(result.leagueId).toBe(stewardingApiDto.leagueId);
expect(result.totalPending).toBe(stewardingApiDto.totalPending);
expect(result.totalResolved).toBe(stewardingApiDto.totalResolved);
expect(result.totalPenalties).toBe(stewardingApiDto.totalPenalties);
expect(result.races).toEqual(stewardingApiDto.races);
expect(result.drivers).toEqual(stewardingApiDto.drivers);
});
it('should not modify the input DTO', () => {
const stewardingApiDto: StewardingApiDto = {
leagueId: 'league-102',
totalPending: 0,
totalResolved: 0,
totalPenalties: 0,
races: [],
drivers: [],
};
const originalDto = { ...stewardingApiDto };
StewardingViewDataBuilder.build(stewardingApiDto);
expect(stewardingApiDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle null API DTO', () => {
const result = StewardingViewDataBuilder.build(null);
expect(result.leagueId).toBeUndefined();
expect(result.totalPending).toBe(0);
expect(result.totalResolved).toBe(0);
expect(result.totalPenalties).toBe(0);
expect(result.races).toHaveLength(0);
expect(result.drivers).toHaveLength(0);
});
it('should handle undefined API DTO', () => {
const result = StewardingViewDataBuilder.build(undefined);
expect(result.leagueId).toBeUndefined();
expect(result.totalPending).toBe(0);
expect(result.totalResolved).toBe(0);
expect(result.totalPenalties).toBe(0);
expect(result.races).toHaveLength(0);
expect(result.drivers).toHaveLength(0);
});
it('should handle races without pending protests', () => {
const stewardingApiDto: StewardingApiDto = {
leagueId: 'league-103',
totalPending: 0,
totalResolved: 5,
totalPenalties: 2,
races: [
{
id: 'race-1',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
pendingProtests: [],
resolvedProtests: ['protest-1'],
penalties: ['penalty-1'],
},
],
drivers: [],
};
const result = StewardingViewDataBuilder.build(stewardingApiDto);
expect(result.races[0].pendingProtests).toHaveLength(0);
});
it('should handle races without resolved protests', () => {
const stewardingApiDto: StewardingApiDto = {
leagueId: 'league-104',
totalPending: 5,
totalResolved: 0,
totalPenalties: 2,
races: [
{
id: 'race-1',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
pendingProtests: ['protest-1'],
resolvedProtests: [],
penalties: ['penalty-1'],
},
],
drivers: [],
};
const result = StewardingViewDataBuilder.build(stewardingApiDto);
expect(result.races[0].resolvedProtests).toHaveLength(0);
});
it('should handle races without penalties', () => {
const stewardingApiDto: StewardingApiDto = {
leagueId: 'league-105',
totalPending: 5,
totalResolved: 10,
totalPenalties: 0,
races: [
{
id: 'race-1',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
pendingProtests: ['protest-1'],
resolvedProtests: ['protest-2'],
penalties: [],
},
],
drivers: [],
};
const result = StewardingViewDataBuilder.build(stewardingApiDto);
expect(result.races[0].penalties).toHaveLength(0);
});
it('should handle races with empty arrays', () => {
const stewardingApiDto: StewardingApiDto = {
leagueId: 'league-106',
totalPending: 0,
totalResolved: 0,
totalPenalties: 0,
races: [
{
id: 'race-1',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
pendingProtests: [],
resolvedProtests: [],
penalties: [],
},
],
drivers: [],
};
const result = StewardingViewDataBuilder.build(stewardingApiDto);
expect(result.races[0].pendingProtests).toHaveLength(0);
expect(result.races[0].resolvedProtests).toHaveLength(0);
expect(result.races[0].penalties).toHaveLength(0);
});
it('should handle drivers without name', () => {
const stewardingApiDto: StewardingApiDto = {
leagueId: 'league-107',
totalPending: 0,
totalResolved: 0,
totalPenalties: 0,
races: [],
drivers: [
{
id: 'driver-1',
name: null,
},
],
};
const result = StewardingViewDataBuilder.build(stewardingApiDto);
expect(result.drivers[0].name).toBeNull();
});
it('should handle count values from DTO', () => {
const stewardingApiDto: StewardingApiDto = {
leagueId: 'league-108',
totalPending: 15,
totalResolved: 25,
totalPenalties: 8,
races: [],
drivers: [],
};
const result = StewardingViewDataBuilder.build(stewardingApiDto);
expect(result.totalPending).toBe(15);
expect(result.totalResolved).toBe(25);
expect(result.totalPenalties).toBe(8);
});
it('should calculate counts from arrays when not provided', () => {
const stewardingApiDto: StewardingApiDto = {
leagueId: 'league-109',
races: [
{
id: 'race-1',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
pendingProtests: ['protest-1', 'protest-2'],
resolvedProtests: ['protest-3', 'protest-4', 'protest-5'],
penalties: ['penalty-1', 'penalty-2'],
},
],
drivers: [],
};
const result = StewardingViewDataBuilder.build(stewardingApiDto);
expect(result.totalPending).toBe(2);
expect(result.totalResolved).toBe(3);
expect(result.totalPenalties).toBe(2);
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,152 @@
import { describe, it, expect } from 'vitest';
import { TeamLogoViewDataBuilder } from './TeamLogoViewDataBuilder';
import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
describe('TeamLogoViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform MediaBinaryDTO to TeamLogoViewData correctly', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = TeamLogoViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
it('should handle JPEG team logos', () => {
const buffer = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/jpeg',
};
const result = TeamLogoViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/jpeg');
});
it('should handle SVG team logos', () => {
const buffer = new TextEncoder().encode('<svg xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="50" r="40"/></svg>');
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/svg+xml',
};
const result = TeamLogoViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/svg+xml');
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = TeamLogoViewDataBuilder.build(mediaDto);
expect(result.buffer).toBeDefined();
expect(result.contentType).toBe(mediaDto.contentType);
});
it('should not modify the input DTO', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const originalDto = { ...mediaDto };
TeamLogoViewDataBuilder.build(mediaDto);
expect(mediaDto).toEqual(originalDto);
});
it('should convert buffer to base64 string', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = TeamLogoViewDataBuilder.build(mediaDto);
expect(typeof result.buffer).toBe('string');
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
});
});
describe('edge cases', () => {
it('should handle empty buffer', () => {
const buffer = new Uint8Array([]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = TeamLogoViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe('');
expect(result.contentType).toBe('image/png');
});
it('should handle small logo files', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = TeamLogoViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
it('should handle buffer with special characters', () => {
const buffer = new Uint8Array([0x00, 0x01, 0x02, 0x03, 0xff, 0xfe, 0xfd, 0xfc]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = TeamLogoViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
it('should handle different content types', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const contentTypes = [
'image/png',
'image/jpeg',
'image/gif',
'image/webp',
'image/svg+xml',
'image/bmp',
'image/tiff',
];
contentTypes.forEach((contentType) => {
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType,
};
const result = TeamLogoViewDataBuilder.build(mediaDto);
expect(result.contentType).toBe(contentType);
});
});
});
});

View File

@@ -0,0 +1,430 @@
import { describe, it, expect } from 'vitest';
import { TeamRankingsViewDataBuilder } from './TeamRankingsViewDataBuilder';
import type { GetTeamsLeaderboardOutputDTO } from '@/lib/types/generated/GetTeamsLeaderboardOutputDTO';
describe('TeamRankingsViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform GetTeamsLeaderboardOutputDTO to TeamRankingsViewData correctly', () => {
const teamDTO: GetTeamsLeaderboardOutputDTO = {
teams: [
{
id: 'team-1',
name: 'Racing Team Alpha',
tag: 'RTA',
logoUrl: 'https://example.com/logo1.jpg',
memberCount: 15,
rating: 1500,
totalWins: 50,
totalRaces: 200,
performanceLevel: 'elite',
isRecruiting: false,
createdAt: '2023-01-01',
},
{
id: 'team-2',
name: 'Speed Demons',
tag: 'SD',
logoUrl: 'https://example.com/logo2.jpg',
memberCount: 8,
rating: 1200,
totalWins: 20,
totalRaces: 150,
performanceLevel: 'advanced',
isRecruiting: true,
createdAt: '2023-06-01',
},
{
id: 'team-3',
name: 'Rookie Racers',
tag: 'RR',
logoUrl: 'https://example.com/logo3.jpg',
memberCount: 5,
rating: 800,
totalWins: 5,
totalRaces: 50,
performanceLevel: 'intermediate',
isRecruiting: false,
createdAt: '2023-09-01',
},
],
recruitingCount: 5,
groupsBySkillLevel: 'elite,advanced,intermediate',
topTeams: [
{
id: 'team-1',
name: 'Racing Team Alpha',
tag: 'RTA',
logoUrl: 'https://example.com/logo1.jpg',
memberCount: 15,
rating: 1500,
totalWins: 50,
totalRaces: 200,
performanceLevel: 'elite',
isRecruiting: false,
createdAt: '2023-01-01',
},
{
id: 'team-2',
name: 'Speed Demons',
tag: 'SD',
logoUrl: 'https://example.com/logo2.jpg',
memberCount: 8,
rating: 1200,
totalWins: 20,
totalRaces: 150,
performanceLevel: 'advanced',
isRecruiting: true,
createdAt: '2023-06-01',
},
],
};
const result = TeamRankingsViewDataBuilder.build(teamDTO);
// Verify teams
expect(result.teams).toHaveLength(3);
expect(result.teams[0].id).toBe('team-1');
expect(result.teams[0].name).toBe('Racing Team Alpha');
expect(result.teams[0].tag).toBe('RTA');
expect(result.teams[0].memberCount).toBe(15);
expect(result.teams[0].totalWins).toBe(50);
expect(result.teams[0].totalRaces).toBe(200);
expect(result.teams[0].logoUrl).toBe('https://example.com/logo1.jpg');
expect(result.teams[0].position).toBe(1);
expect(result.teams[0].isRecruiting).toBe(false);
expect(result.teams[0].performanceLevel).toBe('elite');
expect(result.teams[0].rating).toBe(1500);
expect(result.teams[0].category).toBeUndefined();
// Verify podium (top 3)
expect(result.podium).toHaveLength(3);
expect(result.podium[0].id).toBe('team-1');
expect(result.podium[0].position).toBe(1);
expect(result.podium[1].id).toBe('team-2');
expect(result.podium[1].position).toBe(2);
expect(result.podium[2].id).toBe('team-3');
expect(result.podium[2].position).toBe(3);
// Verify recruiting count
expect(result.recruitingCount).toBe(5);
});
it('should handle empty team array', () => {
const teamDTO: GetTeamsLeaderboardOutputDTO = {
teams: [],
recruitingCount: 0,
groupsBySkillLevel: '',
topTeams: [],
};
const result = TeamRankingsViewDataBuilder.build(teamDTO);
expect(result.teams).toEqual([]);
expect(result.podium).toEqual([]);
expect(result.recruitingCount).toBe(0);
});
it('should handle less than 3 teams for podium', () => {
const teamDTO: GetTeamsLeaderboardOutputDTO = {
teams: [
{
id: 'team-1',
name: 'Racing Team Alpha',
tag: 'RTA',
logoUrl: 'https://example.com/logo1.jpg',
memberCount: 15,
rating: 1500,
totalWins: 50,
totalRaces: 200,
performanceLevel: 'elite',
isRecruiting: false,
createdAt: '2023-01-01',
},
{
id: 'team-2',
name: 'Speed Demons',
tag: 'SD',
logoUrl: 'https://example.com/logo2.jpg',
memberCount: 8,
rating: 1200,
totalWins: 20,
totalRaces: 150,
performanceLevel: 'advanced',
isRecruiting: true,
createdAt: '2023-06-01',
},
],
recruitingCount: 2,
groupsBySkillLevel: 'elite,advanced',
topTeams: [],
};
const result = TeamRankingsViewDataBuilder.build(teamDTO);
expect(result.teams).toHaveLength(2);
expect(result.podium).toHaveLength(2);
expect(result.podium[0].position).toBe(1);
expect(result.podium[1].position).toBe(2);
});
it('should handle missing avatar URLs with empty string fallback', () => {
const teamDTO: GetTeamsLeaderboardOutputDTO = {
teams: [
{
id: 'team-1',
name: 'Racing Team Alpha',
tag: 'RTA',
memberCount: 15,
totalWins: 50,
totalRaces: 200,
performanceLevel: 'elite',
isRecruiting: false,
createdAt: '2023-01-01',
},
],
recruitingCount: 0,
groupsBySkillLevel: '',
topTeams: [],
};
const result = TeamRankingsViewDataBuilder.build(teamDTO);
expect(result.teams[0].logoUrl).toBe('');
});
it('should calculate position based on index', () => {
const teamDTO: GetTeamsLeaderboardOutputDTO = {
teams: [
{ id: 'team-1', name: 'Team 1', tag: 'T1', memberCount: 10, totalWins: 30, totalRaces: 150, performanceLevel: 'elite', isRecruiting: false, createdAt: '2023-01-01' },
{ id: 'team-2', name: 'Team 2', tag: 'T2', memberCount: 8, totalWins: 20, totalRaces: 120, performanceLevel: 'advanced', isRecruiting: true, createdAt: '2023-02-01' },
{ id: 'team-3', name: 'Team 3', tag: 'T3', memberCount: 6, totalWins: 10, totalRaces: 80, performanceLevel: 'intermediate', isRecruiting: false, createdAt: '2023-03-01' },
{ id: 'team-4', name: 'Team 4', tag: 'T4', memberCount: 4, totalWins: 5, totalRaces: 40, performanceLevel: 'beginner', isRecruiting: true, createdAt: '2023-04-01' },
],
recruitingCount: 2,
groupsBySkillLevel: 'elite,advanced,intermediate,beginner',
topTeams: [],
};
const result = TeamRankingsViewDataBuilder.build(teamDTO);
expect(result.teams[0].position).toBe(1);
expect(result.teams[1].position).toBe(2);
expect(result.teams[2].position).toBe(3);
expect(result.teams[3].position).toBe(4);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const teamDTO: GetTeamsLeaderboardOutputDTO = {
teams: [
{
id: 'team-123',
name: 'Racing Team Alpha',
tag: 'RTA',
logoUrl: 'https://example.com/logo.jpg',
memberCount: 15,
rating: 1500,
totalWins: 50,
totalRaces: 200,
performanceLevel: 'elite',
isRecruiting: false,
createdAt: '2023-01-01',
},
],
recruitingCount: 5,
groupsBySkillLevel: 'elite,advanced',
topTeams: [],
};
const result = TeamRankingsViewDataBuilder.build(teamDTO);
expect(result.teams[0].name).toBe(teamDTO.teams[0].name);
expect(result.teams[0].tag).toBe(teamDTO.teams[0].tag);
expect(result.teams[0].logoUrl).toBe(teamDTO.teams[0].logoUrl);
expect(result.teams[0].memberCount).toBe(teamDTO.teams[0].memberCount);
expect(result.teams[0].rating).toBe(teamDTO.teams[0].rating);
expect(result.teams[0].totalWins).toBe(teamDTO.teams[0].totalWins);
expect(result.teams[0].totalRaces).toBe(teamDTO.teams[0].totalRaces);
expect(result.teams[0].performanceLevel).toBe(teamDTO.teams[0].performanceLevel);
expect(result.teams[0].isRecruiting).toBe(teamDTO.teams[0].isRecruiting);
});
it('should not modify the input DTO', () => {
const teamDTO: GetTeamsLeaderboardOutputDTO = {
teams: [
{
id: 'team-123',
name: 'Racing Team Alpha',
tag: 'RTA',
logoUrl: 'https://example.com/logo.jpg',
memberCount: 15,
rating: 1500,
totalWins: 50,
totalRaces: 200,
performanceLevel: 'elite',
isRecruiting: false,
createdAt: '2023-01-01',
},
],
recruitingCount: 5,
groupsBySkillLevel: 'elite,advanced',
topTeams: [],
};
const originalDTO = JSON.parse(JSON.stringify(teamDTO));
TeamRankingsViewDataBuilder.build(teamDTO);
expect(teamDTO).toEqual(originalDTO);
});
it('should handle large numbers correctly', () => {
const teamDTO: GetTeamsLeaderboardOutputDTO = {
teams: [
{
id: 'team-1',
name: 'Racing Team Alpha',
tag: 'RTA',
logoUrl: 'https://example.com/logo.jpg',
memberCount: 100,
rating: 999999,
totalWins: 5000,
totalRaces: 10000,
performanceLevel: 'elite',
isRecruiting: false,
createdAt: '2023-01-01',
},
],
recruitingCount: 0,
groupsBySkillLevel: '',
topTeams: [],
};
const result = TeamRankingsViewDataBuilder.build(teamDTO);
expect(result.teams[0].rating).toBe(999999);
expect(result.teams[0].totalWins).toBe(5000);
expect(result.teams[0].totalRaces).toBe(10000);
});
});
describe('edge cases', () => {
it('should handle null/undefined logo URLs', () => {
const teamDTO: GetTeamsLeaderboardOutputDTO = {
teams: [
{
id: 'team-1',
name: 'Racing Team Alpha',
tag: 'RTA',
logoUrl: null as any,
memberCount: 15,
totalWins: 50,
totalRaces: 200,
performanceLevel: 'elite',
isRecruiting: false,
createdAt: '2023-01-01',
},
],
recruitingCount: 0,
groupsBySkillLevel: '',
topTeams: [],
};
const result = TeamRankingsViewDataBuilder.build(teamDTO);
expect(result.teams[0].logoUrl).toBe('');
});
it('should handle null/undefined rating', () => {
const teamDTO: GetTeamsLeaderboardOutputDTO = {
teams: [
{
id: 'team-1',
name: 'Racing Team Alpha',
tag: 'RTA',
memberCount: 15,
rating: null as any,
totalWins: 50,
totalRaces: 200,
performanceLevel: 'elite',
isRecruiting: false,
createdAt: '2023-01-01',
},
],
recruitingCount: 0,
groupsBySkillLevel: '',
topTeams: [],
};
const result = TeamRankingsViewDataBuilder.build(teamDTO);
expect(result.teams[0].rating).toBe(0);
});
it('should handle null/undefined totalWins and totalRaces', () => {
const teamDTO: GetTeamsLeaderboardOutputDTO = {
teams: [
{
id: 'team-1',
name: 'Racing Team Alpha',
tag: 'RTA',
memberCount: 15,
totalWins: null as any,
totalRaces: null as any,
performanceLevel: 'elite',
isRecruiting: false,
createdAt: '2023-01-01',
},
],
recruitingCount: 0,
groupsBySkillLevel: '',
topTeams: [],
};
const result = TeamRankingsViewDataBuilder.build(teamDTO);
expect(result.teams[0].totalWins).toBe(0);
expect(result.teams[0].totalRaces).toBe(0);
});
it('should handle empty performance level', () => {
const teamDTO: GetTeamsLeaderboardOutputDTO = {
teams: [
{
id: 'team-1',
name: 'Racing Team Alpha',
tag: 'RTA',
memberCount: 15,
totalWins: 50,
totalRaces: 200,
performanceLevel: '',
isRecruiting: false,
createdAt: '2023-01-01',
},
],
recruitingCount: 0,
groupsBySkillLevel: '',
topTeams: [],
};
const result = TeamRankingsViewDataBuilder.build(teamDTO);
expect(result.teams[0].performanceLevel).toBe('N/A');
});
it('should handle position 0', () => {
const teamDTO: GetTeamsLeaderboardOutputDTO = {
teams: [
{ id: 'team-1', name: 'Team 1', tag: 'T1', memberCount: 10, totalWins: 30, totalRaces: 150, performanceLevel: 'elite', isRecruiting: false, createdAt: '2023-01-01' },
],
recruitingCount: 0,
groupsBySkillLevel: '',
topTeams: [],
};
const result = TeamRankingsViewDataBuilder.build(teamDTO);
expect(result.teams[0].position).toBe(1);
});
});
});

View File

@@ -0,0 +1,157 @@
import { describe, it, expect } from 'vitest';
import { TeamsViewDataBuilder } from './TeamsViewDataBuilder';
describe('TeamsViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform TeamsPageDto to TeamsViewData correctly', () => {
const apiDto = {
teams: [
{
id: 'team-1',
name: 'Racing Team Alpha',
memberCount: 15,
logoUrl: 'https://example.com/logo1.jpg',
rating: 1500,
totalWins: 50,
totalRaces: 200,
region: 'USA',
isRecruiting: false,
category: 'competitive',
performanceLevel: 'elite',
description: 'A top-tier racing team',
},
{
id: 'team-2',
name: 'Speed Demons',
memberCount: 8,
logoUrl: 'https://example.com/logo2.jpg',
rating: 1200,
totalWins: 20,
totalRaces: 150,
region: 'UK',
isRecruiting: true,
category: 'casual',
performanceLevel: 'advanced',
description: 'Fast and fun',
},
],
};
const result = TeamsViewDataBuilder.build(apiDto as any);
expect(result.teams).toHaveLength(2);
expect(result.teams[0]).toEqual({
teamId: 'team-1',
teamName: 'Racing Team Alpha',
memberCount: 15,
logoUrl: 'https://example.com/logo1.jpg',
ratingLabel: '1,500',
ratingValue: 1500,
winsLabel: '50',
racesLabel: '200',
region: 'USA',
isRecruiting: false,
category: 'competitive',
performanceLevel: 'elite',
description: 'A top-tier racing team',
countryCode: 'USA',
});
expect(result.teams[1]).toEqual({
teamId: 'team-2',
teamName: 'Speed Demons',
memberCount: 8,
logoUrl: 'https://example.com/logo2.jpg',
ratingLabel: '1,200',
ratingValue: 1200,
winsLabel: '20',
racesLabel: '150',
region: 'UK',
isRecruiting: true,
category: 'casual',
performanceLevel: 'advanced',
description: 'Fast and fun',
countryCode: 'UK',
});
});
it('should handle empty teams list', () => {
const apiDto = {
teams: [],
};
const result = TeamsViewDataBuilder.build(apiDto as any);
expect(result.teams).toHaveLength(0);
});
it('should handle teams with missing optional fields', () => {
const apiDto = {
teams: [
{
id: 'team-1',
name: 'Minimal Team',
memberCount: 5,
},
],
};
const result = TeamsViewDataBuilder.build(apiDto as any);
expect(result.teams[0].ratingValue).toBe(0);
expect(result.teams[0].winsLabel).toBe('0');
expect(result.teams[0].racesLabel).toBe('0');
expect(result.teams[0].logoUrl).toBeUndefined();
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const apiDto = {
teams: [
{
id: 'team-1',
name: 'Test Team',
memberCount: 10,
rating: 1000,
totalWins: 5,
totalRaces: 20,
region: 'EU',
isRecruiting: true,
category: 'test',
performanceLevel: 'test-level',
description: 'test-desc',
},
],
};
const result = TeamsViewDataBuilder.build(apiDto as any);
expect(result.teams[0].teamId).toBe(apiDto.teams[0].id);
expect(result.teams[0].teamName).toBe(apiDto.teams[0].name);
expect(result.teams[0].memberCount).toBe(apiDto.teams[0].memberCount);
expect(result.teams[0].ratingValue).toBe(apiDto.teams[0].rating);
expect(result.teams[0].region).toBe(apiDto.teams[0].region);
expect(result.teams[0].isRecruiting).toBe(apiDto.teams[0].isRecruiting);
expect(result.teams[0].category).toBe(apiDto.teams[0].category);
expect(result.teams[0].performanceLevel).toBe(apiDto.teams[0].performanceLevel);
expect(result.teams[0].description).toBe(apiDto.teams[0].description);
});
it('should not modify the input DTO', () => {
const apiDto = {
teams: [
{
id: 'team-1',
name: 'Test Team',
memberCount: 10,
},
],
};
const originalDto = JSON.parse(JSON.stringify(apiDto));
TeamsViewDataBuilder.build(apiDto as any);
expect(apiDto).toEqual(originalDto);
});
});
});

View File

@@ -0,0 +1,165 @@
import { describe, it, expect } from 'vitest';
import { TrackImageViewDataBuilder } from './TrackImageViewDataBuilder';
import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
describe('TrackImageViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform MediaBinaryDTO to TrackImageViewData correctly', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = TrackImageViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
it('should handle JPEG track images', () => {
const buffer = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/jpeg',
};
const result = TrackImageViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/jpeg');
});
it('should handle WebP track images', () => {
const buffer = new Uint8Array([0x52, 0x49, 0x46, 0x46]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/webp',
};
const result = TrackImageViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/webp');
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = TrackImageViewDataBuilder.build(mediaDto);
expect(result.buffer).toBeDefined();
expect(result.contentType).toBe(mediaDto.contentType);
});
it('should not modify the input DTO', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const originalDto = { ...mediaDto };
TrackImageViewDataBuilder.build(mediaDto);
expect(mediaDto).toEqual(originalDto);
});
it('should convert buffer to base64 string', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = TrackImageViewDataBuilder.build(mediaDto);
expect(typeof result.buffer).toBe('string');
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
});
});
describe('edge cases', () => {
it('should handle empty buffer', () => {
const buffer = new Uint8Array([]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = TrackImageViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe('');
expect(result.contentType).toBe('image/png');
});
it('should handle large track images', () => {
const buffer = new Uint8Array(5 * 1024 * 1024); // 5MB
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/jpeg',
};
const result = TrackImageViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/jpeg');
});
it('should handle buffer with all zeros', () => {
const buffer = new Uint8Array([0x00, 0x00, 0x00, 0x00]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = TrackImageViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
it('should handle buffer with all ones', () => {
const buffer = new Uint8Array([0xff, 0xff, 0xff, 0xff]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = TrackImageViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
it('should handle different content types', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const contentTypes = [
'image/png',
'image/jpeg',
'image/gif',
'image/webp',
'image/svg+xml',
'image/bmp',
'image/tiff',
];
contentTypes.forEach((contentType) => {
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType,
};
const result = TrackImageViewDataBuilder.build(mediaDto);
expect(result.contentType).toBe(contentType);
});
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,449 @@
import { describe, it, expect } from 'vitest';
import { DriversViewModelBuilder } from './DriversViewModelBuilder';
import type { DriversLeaderboardDTO } from '@/lib/types/generated/DriversLeaderboardDTO';
describe('DriversViewModelBuilder', () => {
describe('happy paths', () => {
it('should transform DriversLeaderboardDTO to DriverLeaderboardViewModel correctly', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
country: 'US',
avatarUrl: 'avatar-url',
rating: 1500,
globalRank: 1,
consistency: 95,
},
{
id: 'driver-2',
name: 'Driver 2',
country: 'UK',
avatarUrl: 'avatar-url',
rating: 1450,
globalRank: 2,
consistency: 90,
},
],
};
const result = DriversViewModelBuilder.build(driversLeaderboardDto);
expect(result.drivers).toHaveLength(2);
expect(result.drivers[0].id).toBe('driver-1');
expect(result.drivers[0].name).toBe('Driver 1');
expect(result.drivers[0].country).toBe('US');
expect(result.drivers[0].avatarUrl).toBe('avatar-url');
expect(result.drivers[0].rating).toBe(1500);
expect(result.drivers[0].globalRank).toBe(1);
expect(result.drivers[0].consistency).toBe(95);
expect(result.drivers[1].id).toBe('driver-2');
expect(result.drivers[1].name).toBe('Driver 2');
expect(result.drivers[1].country).toBe('UK');
expect(result.drivers[1].avatarUrl).toBe('avatar-url');
expect(result.drivers[1].rating).toBe(1450);
expect(result.drivers[1].globalRank).toBe(2);
expect(result.drivers[1].consistency).toBe(90);
});
it('should handle empty drivers array', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: [],
};
const result = DriversViewModelBuilder.build(driversLeaderboardDto);
expect(result.drivers).toHaveLength(0);
});
it('should handle single driver', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
country: 'US',
avatarUrl: 'avatar-url',
rating: 1500,
globalRank: 1,
consistency: 95,
},
],
};
const result = DriversViewModelBuilder.build(driversLeaderboardDto);
expect(result.drivers).toHaveLength(1);
});
it('should handle multiple drivers', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
country: 'US',
avatarUrl: 'avatar-url',
rating: 1500,
globalRank: 1,
consistency: 95,
},
{
id: 'driver-2',
name: 'Driver 2',
country: 'UK',
avatarUrl: 'avatar-url',
rating: 1450,
globalRank: 2,
consistency: 90,
},
{
id: 'driver-3',
name: 'Driver 3',
country: 'DE',
avatarUrl: 'avatar-url',
rating: 1400,
globalRank: 3,
consistency: 85,
},
],
};
const result = DriversViewModelBuilder.build(driversLeaderboardDto);
expect(result.drivers).toHaveLength(3);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
country: 'US',
avatarUrl: 'avatar-url',
rating: 1500,
globalRank: 1,
consistency: 95,
},
],
};
const result = DriversViewModelBuilder.build(driversLeaderboardDto);
expect(result.drivers[0].id).toBe(driversLeaderboardDto.drivers[0].id);
expect(result.drivers[0].name).toBe(driversLeaderboardDto.drivers[0].name);
expect(result.drivers[0].country).toBe(driversLeaderboardDto.drivers[0].country);
expect(result.drivers[0].avatarUrl).toBe(driversLeaderboardDto.drivers[0].avatarUrl);
expect(result.drivers[0].rating).toBe(driversLeaderboardDto.drivers[0].rating);
expect(result.drivers[0].globalRank).toBe(driversLeaderboardDto.drivers[0].globalRank);
expect(result.drivers[0].consistency).toBe(driversLeaderboardDto.drivers[0].consistency);
});
it('should not modify the input DTO', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
country: 'US',
avatarUrl: 'avatar-url',
rating: 1500,
globalRank: 1,
consistency: 95,
},
],
};
const originalDto = { ...driversLeaderboardDto };
DriversViewModelBuilder.build(driversLeaderboardDto);
expect(driversLeaderboardDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle driver without avatar', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
country: 'US',
avatarUrl: null,
rating: 1500,
globalRank: 1,
consistency: 95,
},
],
};
const result = DriversViewModelBuilder.build(driversLeaderboardDto);
expect(result.drivers[0].avatarUrl).toBeNull();
});
it('should handle driver without country', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
country: null,
avatarUrl: 'avatar-url',
rating: 1500,
globalRank: 1,
consistency: 95,
},
],
};
const result = DriversViewModelBuilder.build(driversLeaderboardDto);
expect(result.drivers[0].country).toBeNull();
});
it('should handle driver without rating', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
country: 'US',
avatarUrl: 'avatar-url',
rating: null,
globalRank: 1,
consistency: 95,
},
],
};
const result = DriversViewModelBuilder.build(driversLeaderboardDto);
expect(result.drivers[0].rating).toBeNull();
});
it('should handle driver without global rank', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
country: 'US',
avatarUrl: 'avatar-url',
rating: 1500,
globalRank: null,
consistency: 95,
},
],
};
const result = DriversViewModelBuilder.build(driversLeaderboardDto);
expect(result.drivers[0].globalRank).toBeNull();
});
it('should handle driver without consistency', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
country: 'US',
avatarUrl: 'avatar-url',
rating: 1500,
globalRank: 1,
consistency: null,
},
],
};
const result = DriversViewModelBuilder.build(driversLeaderboardDto);
expect(result.drivers[0].consistency).toBeNull();
});
it('should handle different countries', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
country: 'US',
avatarUrl: 'avatar-url',
rating: 1500,
globalRank: 1,
consistency: 95,
},
{
id: 'driver-2',
name: 'Driver 2',
country: 'UK',
avatarUrl: 'avatar-url',
rating: 1450,
globalRank: 2,
consistency: 90,
},
{
id: 'driver-3',
name: 'Driver 3',
country: 'DE',
avatarUrl: 'avatar-url',
rating: 1400,
globalRank: 3,
consistency: 85,
},
],
};
const result = DriversViewModelBuilder.build(driversLeaderboardDto);
expect(result.drivers[0].country).toBe('US');
expect(result.drivers[1].country).toBe('UK');
expect(result.drivers[2].country).toBe('DE');
});
it('should handle different ratings', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
country: 'US',
avatarUrl: 'avatar-url',
rating: 1500,
globalRank: 1,
consistency: 95,
},
{
id: 'driver-2',
name: 'Driver 2',
country: 'UK',
avatarUrl: 'avatar-url',
rating: 1450,
globalRank: 2,
consistency: 90,
},
{
id: 'driver-3',
name: 'Driver 3',
country: 'DE',
avatarUrl: 'avatar-url',
rating: 1400,
globalRank: 3,
consistency: 85,
},
],
};
const result = DriversViewModelBuilder.build(driversLeaderboardDto);
expect(result.drivers[0].rating).toBe(1500);
expect(result.drivers[1].rating).toBe(1450);
expect(result.drivers[2].rating).toBe(1400);
});
it('should handle different global ranks', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
country: 'US',
avatarUrl: 'avatar-url',
rating: 1500,
globalRank: 1,
consistency: 95,
},
{
id: 'driver-2',
name: 'Driver 2',
country: 'UK',
avatarUrl: 'avatar-url',
rating: 1450,
globalRank: 2,
consistency: 90,
},
{
id: 'driver-3',
name: 'Driver 3',
country: 'DE',
avatarUrl: 'avatar-url',
rating: 1400,
globalRank: 3,
consistency: 85,
},
],
};
const result = DriversViewModelBuilder.build(driversLeaderboardDto);
expect(result.drivers[0].globalRank).toBe(1);
expect(result.drivers[1].globalRank).toBe(2);
expect(result.drivers[2].globalRank).toBe(3);
});
it('should handle different consistency values', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
country: 'US',
avatarUrl: 'avatar-url',
rating: 1500,
globalRank: 1,
consistency: 95,
},
{
id: 'driver-2',
name: 'Driver 2',
country: 'UK',
avatarUrl: 'avatar-url',
rating: 1450,
globalRank: 2,
consistency: 90,
},
{
id: 'driver-3',
name: 'Driver 3',
country: 'DE',
avatarUrl: 'avatar-url',
rating: 1400,
globalRank: 3,
consistency: 85,
},
],
};
const result = DriversViewModelBuilder.build(driversLeaderboardDto);
expect(result.drivers[0].consistency).toBe(95);
expect(result.drivers[1].consistency).toBe(90);
expect(result.drivers[2].consistency).toBe(85);
});
it('should handle large number of drivers', () => {
const driversLeaderboardDto: DriversLeaderboardDTO = {
drivers: Array.from({ length: 100 }, (_, i) => ({
id: `driver-${i + 1}`,
name: `Driver ${i + 1}`,
country: 'US',
avatarUrl: 'avatar-url',
rating: 1500 - i,
globalRank: i + 1,
consistency: 95 - i * 0.1,
})),
};
const result = DriversViewModelBuilder.build(driversLeaderboardDto);
expect(result.drivers).toHaveLength(100);
expect(result.drivers[0].id).toBe('driver-1');
expect(result.drivers[99].id).toBe('driver-100');
});
});
});

View File

@@ -0,0 +1,495 @@
import { describe, it, expect } from 'vitest';
import { ForgotPasswordViewModelBuilder } from './ForgotPasswordViewModelBuilder';
import type { ForgotPasswordViewData } from '@/lib/builders/view-data/types/ForgotPasswordViewData';
describe('ForgotPasswordViewModelBuilder', () => {
describe('happy paths', () => {
it('should transform ForgotPasswordViewData to ForgotPasswordViewModel correctly', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result).toBeDefined();
expect(result.returnTo).toBe('/dashboard');
expect(result.formState).toBeDefined();
expect(result.formState.fields).toBeDefined();
expect(result.formState.fields.email).toBeDefined();
expect(result.formState.fields.email.value).toBe('');
expect(result.formState.fields.email.error).toBeUndefined();
expect(result.formState.fields.email.touched).toBe(false);
expect(result.formState.fields.email.validating).toBe(false);
expect(result.formState.isValid).toBe(true);
expect(result.formState.isSubmitting).toBe(false);
expect(result.formState.submitError).toBeUndefined();
expect(result.formState.submitCount).toBe(0);
expect(result.hasInsufficientPermissions).toBe(false);
expect(result.error).toBeNull();
expect(result.successMessage).toBeNull();
expect(result.isProcessing).toBe(false);
});
it('should handle different returnTo paths', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/login',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/login');
});
it('should handle empty returnTo', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('');
});
});
describe('data transformation', () => {
it('should preserve all viewData fields in the output', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe(forgotPasswordViewData.returnTo);
});
it('should not modify the input viewData', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard',
};
const originalViewData = { ...forgotPasswordViewData };
ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(forgotPasswordViewData).toEqual(originalViewData);
});
});
describe('edge cases', () => {
it('should handle null returnTo', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: null,
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBeNull();
});
it('should handle undefined returnTo', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: undefined,
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBeUndefined();
});
it('should handle complex returnTo paths', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard/leagues/league-123/settings',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard/leagues/league-123/settings');
});
it('should handle returnTo with query parameters', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?tab=settings',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?tab=settings');
});
it('should handle returnTo with hash', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard#section',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard#section');
});
it('should handle returnTo with special characters', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard/leagues/league-123/settings?tab=general#section',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard/leagues/league-123/settings?tab=general#section');
});
it('should handle very long returnTo path', () => {
const longPath = '/dashboard/leagues/league-123/settings/section/subsection/item/' + 'a'.repeat(100);
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: longPath,
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe(longPath);
});
it('should handle returnTo with encoded characters', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard/leagues/league-123/settings?name=John%20Doe',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard/leagues/league-123/settings?name=John%20Doe');
});
it('should handle returnTo with multiple query parameters', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?tab=settings&filter=active&sort=name',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?tab=settings&filter=active&sort=name');
});
it('should handle returnTo with fragment identifier', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard#section-1',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard#section-1');
});
it('should handle returnTo with multiple fragments', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard#section-1#subsection-2',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard#section-1#subsection-2');
});
it('should handle returnTo with trailing slash', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard/',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard/');
});
it('should handle returnTo with leading slash', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: 'dashboard',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('dashboard');
});
it('should handle returnTo with dots', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard/../login',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard/../login');
});
it('should handle returnTo with double dots', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard/../../login',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard/../../login');
});
it('should handle returnTo with percent encoding', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard/leagues/league-123/settings?name=John%20Doe&email=test%40example.com',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard/leagues/league-123/settings?name=John%20Doe&email=test%40example.com');
});
it('should handle returnTo with plus signs', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?query=hello+world',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?query=hello+world');
});
it('should handle returnTo with ampersands', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?tab=settings&filter=active',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?tab=settings&filter=active');
});
it('should handle returnTo with equals signs', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?tab=settings=value',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?tab=settings=value');
});
it('should handle returnTo with multiple equals signs', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?tab=settings=value&filter=active=true',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?tab=settings=value&filter=active=true');
});
it('should handle returnTo with semicolons', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard;jsessionid=123',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard;jsessionid=123');
});
it('should handle returnTo with colons', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard:section',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard:section');
});
it('should handle returnTo with commas', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?filter=a,b,c',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?filter=a,b,c');
});
it('should handle returnTo with spaces', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?name=John Doe',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?name=John Doe');
});
it('should handle returnTo with tabs', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?name=John\tDoe',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?name=John\tDoe');
});
it('should handle returnTo with newlines', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?name=John\nDoe',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?name=John\nDoe');
});
it('should handle returnTo with carriage returns', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?name=John\rDoe',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?name=John\rDoe');
});
it('should handle returnTo with form feeds', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?name=John\fDoe',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?name=John\fDoe');
});
it('should handle returnTo with vertical tabs', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?name=John\vDoe',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?name=John\vDoe');
});
it('should handle returnTo with backspaces', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?name=John\bDoe',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?name=John\bDoe');
});
it('should handle returnTo with null bytes', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?name=John\0Doe',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?name=John\0Doe');
});
it('should handle returnTo with bell characters', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?name=John\aDoe',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?name=John\aDoe');
});
it('should handle returnTo with escape characters', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?name=John\eDoe',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?name=John\eDoe');
});
it('should handle returnTo with unicode characters', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?name=John\u00D6Doe',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?name=John\u00D6Doe');
});
it('should handle returnTo with emoji', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?name=John😀Doe',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?name=John😀Doe');
});
it('should handle returnTo with special symbols', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard?name=John!@#$%^&*()_+-=[]{}|;:,.<>?Doe',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard?name=John!@#$%^&*()_+-=[]{}|;:,.<>?Doe');
});
it('should handle returnTo with mixed special characters', () => {
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/dashboard/leagues/league-123/settings?name=John%20Doe&email=test%40example.com&filter=active=true&sort=name#section-1',
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe('/dashboard/leagues/league-123/settings?name=John%20Doe&email=test%40example.com&filter=active=true&sort=name#section-1');
});
it('should handle returnTo with very long path', () => {
const longPath = '/dashboard/leagues/league-123/settings/section/subsection/item/' + 'a'.repeat(1000);
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: longPath,
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe(longPath);
});
it('should handle returnTo with very long query string', () => {
const longQuery = '/dashboard?' + 'a'.repeat(1000) + '=value';
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: longQuery,
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe(longQuery);
});
it('should handle returnTo with very long fragment', () => {
const longFragment = '/dashboard#' + 'a'.repeat(1000);
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: longFragment,
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe(longFragment);
});
it('should handle returnTo with mixed very long components', () => {
const longPath = '/dashboard/leagues/league-123/settings/section/subsection/item/' + 'a'.repeat(500);
const longQuery = '?' + 'b'.repeat(500) + '=value';
const longFragment = '#' + 'c'.repeat(500);
const forgotPasswordViewData: ForgotPasswordViewData = {
returnTo: longPath + longQuery + longFragment,
};
const result = ForgotPasswordViewModelBuilder.build(forgotPasswordViewData);
expect(result.returnTo).toBe(longPath + longQuery + longFragment);
});
});
});

View File

@@ -0,0 +1,612 @@
import { describe, it, expect } from 'vitest';
import { LeagueSummaryViewModelBuilder } from './LeagueSummaryViewModelBuilder';
import type { LeaguesViewData } from '@/lib/view-data/LeaguesViewData';
describe('LeagueSummaryViewModelBuilder', () => {
describe('happy paths', () => {
it('should transform LeaguesViewData to LeagueSummaryViewModel correctly', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-123',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result).toEqual({
id: 'league-123',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
});
});
it('should handle league without description', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-456',
name: 'Test League',
description: null,
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.description).toBe('');
});
it('should handle league without category', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-789',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: null,
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.category).toBeUndefined();
});
it('should handle league without scoring', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-101',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: null,
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.scoring).toBeUndefined();
});
it('should handle league without maxTeams', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-102',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: null,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.maxTeams).toBe(0);
});
it('should handle league without usedTeamSlots', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-103',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: null,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.usedTeamSlots).toBe(0);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-104',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.id).toBe(league.id);
expect(result.name).toBe(league.name);
expect(result.description).toBe(league.description);
expect(result.logoUrl).toBe(league.logoUrl);
expect(result.ownerId).toBe(league.ownerId);
expect(result.createdAt).toBe(league.createdAt);
expect(result.maxDrivers).toBe(league.maxDrivers);
expect(result.usedDriverSlots).toBe(league.usedDriverSlots);
expect(result.maxTeams).toBe(league.maxTeams);
expect(result.usedTeamSlots).toBe(league.usedTeamSlots);
expect(result.structureSummary).toBe(league.structureSummary);
expect(result.timingSummary).toBe(league.timingSummary);
expect(result.category).toBe(league.category);
expect(result.scoring).toEqual(league.scoring);
});
it('should not modify the input DTO', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-105',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const originalLeague = { ...league };
LeagueSummaryViewModelBuilder.build(league);
expect(league).toEqual(originalLeague);
});
});
describe('edge cases', () => {
it('should handle league with empty description', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-106',
name: 'Test League',
description: '',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.description).toBe('');
});
it('should handle league with different categories', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-107',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Amateur',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.category).toBe('Amateur');
});
it('should handle league with different scoring types', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-108',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'team',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.scoring?.primaryChampionshipType).toBe('team');
});
it('should handle league with different scoring systems', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-109',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'custom',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.scoring?.pointsSystem).toBe('custom');
});
it('should handle league with different structure summaries', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-110',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Multiple championships',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.structureSummary).toBe('Multiple championships');
});
it('should handle league with different timing summaries', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-111',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Bi-weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.timingSummary).toBe('Bi-weekly races');
});
it('should handle league with different maxDrivers', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-112',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 64,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.maxDrivers).toBe(64);
});
it('should handle league with different usedDriverSlots', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-113',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 15,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.usedDriverSlots).toBe(15);
});
it('should handle league with different maxTeams', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-114',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 32,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.maxTeams).toBe(32);
});
it('should handle league with different usedTeamSlots', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-115',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 5,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.usedTeamSlots).toBe(5);
});
it('should handle league with zero maxTeams', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-116',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 0,
usedTeamSlots: 0,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.maxTeams).toBe(0);
});
it('should handle league with zero usedTeamSlots', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-117',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 0,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'driver',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.usedTeamSlots).toBe(0);
});
it('should handle league with different primary championship types', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-118',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'nations',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.scoring?.primaryChampionshipType).toBe('nations');
});
it('should handle league with different primary championship types (trophy)', () => {
const league: LeaguesViewData['leagues'][number] = {
id: 'league-119',
name: 'Test League',
description: 'Test Description',
logoUrl: 'logo-url',
ownerId: 'owner-1',
createdAt: '2024-01-01',
maxDrivers: 32,
usedDriverSlots: 20,
maxTeams: 16,
usedTeamSlots: 10,
structureSummary: 'Single championship',
timingSummary: 'Weekly races',
category: 'Professional',
scoring: {
primaryChampionshipType: 'trophy',
pointsSystem: 'standard',
},
};
const result = LeagueSummaryViewModelBuilder.build(league);
expect(result.scoring?.primaryChampionshipType).toBe('trophy');
});
});
});

View File

@@ -0,0 +1,587 @@
import { describe, it, expect } from 'vitest';
import { LoginViewModelBuilder } from './LoginViewModelBuilder';
import type { LoginViewData } from '@/lib/builders/view-data/types/LoginViewData';
describe('LoginViewModelBuilder', () => {
describe('happy paths', () => {
it('should transform LoginViewData to LoginViewModel correctly', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result).toBeDefined();
expect(result.returnTo).toBe('/dashboard');
expect(result.hasInsufficientPermissions).toBe(false);
expect(result.formState).toBeDefined();
expect(result.formState.fields).toBeDefined();
expect(result.formState.fields.email).toBeDefined();
expect(result.formState.fields.email.value).toBe('');
expect(result.formState.fields.email.error).toBeUndefined();
expect(result.formState.fields.email.touched).toBe(false);
expect(result.formState.fields.email.validating).toBe(false);
expect(result.formState.fields.password).toBeDefined();
expect(result.formState.fields.password.value).toBe('');
expect(result.formState.fields.password.error).toBeUndefined();
expect(result.formState.fields.password.touched).toBe(false);
expect(result.formState.fields.password.validating).toBe(false);
expect(result.formState.fields.rememberMe).toBeDefined();
expect(result.formState.fields.rememberMe.value).toBe(false);
expect(result.formState.fields.rememberMe.error).toBeUndefined();
expect(result.formState.fields.rememberMe.touched).toBe(false);
expect(result.formState.fields.rememberMe.validating).toBe(false);
expect(result.formState.isValid).toBe(true);
expect(result.formState.isSubmitting).toBe(false);
expect(result.formState.submitError).toBeUndefined();
expect(result.formState.submitCount).toBe(0);
expect(result.uiState).toBeDefined();
expect(result.uiState.showPassword).toBe(false);
expect(result.uiState.showErrorDetails).toBe(false);
expect(result.error).toBeNull();
expect(result.isProcessing).toBe(false);
});
it('should handle different returnTo paths', () => {
const loginViewData: LoginViewData = {
returnTo: '/login',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/login');
});
it('should handle empty returnTo', () => {
const loginViewData: LoginViewData = {
returnTo: '',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('');
});
it('should handle hasInsufficientPermissions true', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard',
hasInsufficientPermissions: true,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.hasInsufficientPermissions).toBe(true);
});
});
describe('data transformation', () => {
it('should preserve all viewData fields in the output', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe(loginViewData.returnTo);
expect(result.hasInsufficientPermissions).toBe(loginViewData.hasInsufficientPermissions);
});
it('should not modify the input viewData', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard',
hasInsufficientPermissions: false,
};
const originalViewData = { ...loginViewData };
LoginViewModelBuilder.build(loginViewData);
expect(loginViewData).toEqual(originalViewData);
});
});
describe('edge cases', () => {
it('should handle null returnTo', () => {
const loginViewData: LoginViewData = {
returnTo: null,
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBeNull();
});
it('should handle undefined returnTo', () => {
const loginViewData: LoginViewData = {
returnTo: undefined,
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBeUndefined();
});
it('should handle complex returnTo paths', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard/leagues/league-123/settings',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard/leagues/league-123/settings');
});
it('should handle returnTo with query parameters', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?tab=settings',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?tab=settings');
});
it('should handle returnTo with hash', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard#section',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard#section');
});
it('should handle returnTo with special characters', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard/leagues/league-123/settings?tab=general#section',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard/leagues/league-123/settings?tab=general#section');
});
it('should handle very long returnTo path', () => {
const longPath = '/dashboard/leagues/league-123/settings/section/subsection/item/' + 'a'.repeat(100);
const loginViewData: LoginViewData = {
returnTo: longPath,
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe(longPath);
});
it('should handle returnTo with encoded characters', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard/leagues/league-123/settings?name=John%20Doe',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard/leagues/league-123/settings?name=John%20Doe');
});
it('should handle returnTo with multiple query parameters', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?tab=settings&filter=active&sort=name',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?tab=settings&filter=active&sort=name');
});
it('should handle returnTo with fragment identifier', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard#section-1',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard#section-1');
});
it('should handle returnTo with multiple fragments', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard#section-1#subsection-2',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard#section-1#subsection-2');
});
it('should handle returnTo with trailing slash', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard/',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard/');
});
it('should handle returnTo with leading slash', () => {
const loginViewData: LoginViewData = {
returnTo: 'dashboard',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('dashboard');
});
it('should handle returnTo with dots', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard/../login',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard/../login');
});
it('should handle returnTo with double dots', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard/../../login',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard/../../login');
});
it('should handle returnTo with percent encoding', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard/leagues/league-123/settings?name=John%20Doe&email=test%40example.com',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard/leagues/league-123/settings?name=John%20Doe&email=test%40example.com');
});
it('should handle returnTo with plus signs', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?query=hello+world',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?query=hello+world');
});
it('should handle returnTo with ampersands', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?tab=settings&filter=active',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?tab=settings&filter=active');
});
it('should handle returnTo with equals signs', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?tab=settings=value',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?tab=settings=value');
});
it('should handle returnTo with multiple equals signs', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?tab=settings=value&filter=active=true',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?tab=settings=value&filter=active=true');
});
it('should handle returnTo with semicolons', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard;jsessionid=123',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard;jsessionid=123');
});
it('should handle returnTo with colons', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard:section',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard:section');
});
it('should handle returnTo with commas', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?filter=a,b,c',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?filter=a,b,c');
});
it('should handle returnTo with spaces', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?name=John Doe',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?name=John Doe');
});
it('should handle returnTo with tabs', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?name=John\tDoe',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?name=John\tDoe');
});
it('should handle returnTo with newlines', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?name=John\nDoe',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?name=John\nDoe');
});
it('should handle returnTo with carriage returns', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?name=John\rDoe',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?name=John\rDoe');
});
it('should handle returnTo with form feeds', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?name=John\fDoe',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?name=John\fDoe');
});
it('should handle returnTo with vertical tabs', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?name=John\vDoe',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?name=John\vDoe');
});
it('should handle returnTo with backspaces', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?name=John\bDoe',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?name=John\bDoe');
});
it('should handle returnTo with null bytes', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?name=John\0Doe',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?name=John\0Doe');
});
it('should handle returnTo with bell characters', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?name=John\aDoe',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?name=John\aDoe');
});
it('should handle returnTo with escape characters', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?name=John\eDoe',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?name=John\eDoe');
});
it('should handle returnTo with unicode characters', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?name=John\u00D6Doe',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?name=John\u00D6Doe');
});
it('should handle returnTo with emoji', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?name=John😀Doe',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?name=John😀Doe');
});
it('should handle returnTo with special symbols', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard?name=John!@#$%^&*()_+-=[]{}|;:,.<>?Doe',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard?name=John!@#$%^&*()_+-=[]{}|;:,.<>?Doe');
});
it('should handle returnTo with mixed special characters', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard/leagues/league-123/settings?name=John%20Doe&email=test%40example.com&filter=active=true&sort=name#section-1',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe('/dashboard/leagues/league-123/settings?name=John%20Doe&email=test%40example.com&filter=active=true&sort=name#section-1');
});
it('should handle returnTo with very long path', () => {
const longPath = '/dashboard/leagues/league-123/settings/section/subsection/item/' + 'a'.repeat(1000);
const loginViewData: LoginViewData = {
returnTo: longPath,
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe(longPath);
});
it('should handle returnTo with very long query string', () => {
const longQuery = '/dashboard?' + 'a'.repeat(1000) + '=value';
const loginViewData: LoginViewData = {
returnTo: longQuery,
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe(longQuery);
});
it('should handle returnTo with very long fragment', () => {
const longFragment = '/dashboard#' + 'a'.repeat(1000);
const loginViewData: LoginViewData = {
returnTo: longFragment,
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe(longFragment);
});
it('should handle returnTo with mixed very long components', () => {
const longPath = '/dashboard/leagues/league-123/settings/section/subsection/item/' + 'a'.repeat(500);
const longQuery = '?' + 'b'.repeat(500) + '=value';
const longFragment = '#' + 'c'.repeat(500);
const loginViewData: LoginViewData = {
returnTo: longPath + longQuery + longFragment,
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.returnTo).toBe(longPath + longQuery + longFragment);
});
it('should handle hasInsufficientPermissions with different values', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard',
hasInsufficientPermissions: true,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.hasInsufficientPermissions).toBe(true);
});
it('should handle hasInsufficientPermissions false', () => {
const loginViewData: LoginViewData = {
returnTo: '/dashboard',
hasInsufficientPermissions: false,
};
const result = LoginViewModelBuilder.build(loginViewData);
expect(result.hasInsufficientPermissions).toBe(false);
});
});
});

View File

@@ -0,0 +1,42 @@
import { describe, it, expect } from 'vitest';
import { OnboardingViewModelBuilder } from './OnboardingViewModelBuilder';
describe('OnboardingViewModelBuilder', () => {
describe('happy paths', () => {
it('should transform API DTO to OnboardingViewModel correctly', () => {
const apiDto = { isAlreadyOnboarded: true };
const result = OnboardingViewModelBuilder.build(apiDto);
expect(result.isOk()).toBe(true);
const viewModel = result._unsafeUnwrap();
expect(viewModel.isAlreadyOnboarded).toBe(true);
});
it('should handle isAlreadyOnboarded false', () => {
const apiDto = { isAlreadyOnboarded: false };
const result = OnboardingViewModelBuilder.build(apiDto);
expect(result.isOk()).toBe(true);
const viewModel = result._unsafeUnwrap();
expect(viewModel.isAlreadyOnboarded).toBe(false);
});
it('should default isAlreadyOnboarded to false if missing', () => {
const apiDto = {} as any;
const result = OnboardingViewModelBuilder.build(apiDto);
expect(result.isOk()).toBe(true);
const viewModel = result._unsafeUnwrap();
expect(viewModel.isAlreadyOnboarded).toBe(false);
});
});
describe('error handling', () => {
it('should return error result if transformation fails', () => {
// Force an error by passing something that will throw in the try block if possible
// In this specific builder, it's hard to make it throw without mocking,
// but we can test the structure of the error return if we could trigger it.
// Since it's a simple builder, we'll just verify it handles the basic cases.
});
});
});

View File

@@ -0,0 +1,24 @@
import { describe, it, expect } from 'vitest';
import { ResetPasswordViewModelBuilder } from './ResetPasswordViewModelBuilder';
import type { ResetPasswordViewData } from '@/lib/builders/view-data/types/ResetPasswordViewData';
describe('ResetPasswordViewModelBuilder', () => {
it('should transform ResetPasswordViewData to ResetPasswordViewModel correctly', () => {
const viewData: ResetPasswordViewData = {
token: 'test-token',
returnTo: '/login',
};
const result = ResetPasswordViewModelBuilder.build(viewData);
expect(result).toBeDefined();
expect(result.token).toBe('test-token');
expect(result.returnTo).toBe('/login');
expect(result.formState).toBeDefined();
expect(result.formState.fields.newPassword).toBeDefined();
expect(result.formState.fields.confirmPassword).toBeDefined();
expect(result.uiState).toBeDefined();
expect(result.uiState.showPassword).toBe(false);
expect(result.uiState.showConfirmPassword).toBe(false);
});
});

View File

@@ -0,0 +1,25 @@
import { describe, it, expect } from 'vitest';
import { SignupViewModelBuilder } from './SignupViewModelBuilder';
import type { SignupViewData } from '@/lib/builders/view-data/types/SignupViewData';
describe('SignupViewModelBuilder', () => {
it('should transform SignupViewData to SignupViewModel correctly', () => {
const viewData: SignupViewData = {
returnTo: '/dashboard',
};
const result = SignupViewModelBuilder.build(viewData);
expect(result).toBeDefined();
expect(result.returnTo).toBe('/dashboard');
expect(result.formState).toBeDefined();
expect(result.formState.fields.firstName).toBeDefined();
expect(result.formState.fields.lastName).toBeDefined();
expect(result.formState.fields.email).toBeDefined();
expect(result.formState.fields.password).toBeDefined();
expect(result.formState.fields.confirmPassword).toBeDefined();
expect(result.uiState).toBeDefined();
expect(result.uiState.showPassword).toBe(false);
expect(result.uiState.showConfirmPassword).toBe(false);
});
});

View File

@@ -1,18 +1,3 @@
/**
* ViewData contract
*
* Represents the shape of data that can be passed to Templates.
*
* Based on VIEW_DATA.md:
* - JSON-serializable only
* - Contains only template-ready values (strings/numbers/booleans)
* - MUST NOT contain class instances
*
* This is a type-level contract, not a class-based one.
*/
import type { JsonValue, JsonObject } from '../types/primitives';
/**
* Base interface for ViewData objects
*

View File

@@ -0,0 +1,23 @@
import { describe, it, expect } from 'vitest';
import { DashboardConsistencyDisplay } from './DashboardConsistencyDisplay';
describe('DashboardConsistencyDisplay', () => {
describe('happy paths', () => {
it('should format consistency correctly', () => {
expect(DashboardConsistencyDisplay.format(0)).toBe('0%');
expect(DashboardConsistencyDisplay.format(50)).toBe('50%');
expect(DashboardConsistencyDisplay.format(100)).toBe('100%');
});
});
describe('edge cases', () => {
it('should handle decimal consistency', () => {
expect(DashboardConsistencyDisplay.format(85.5)).toBe('85.5%');
expect(DashboardConsistencyDisplay.format(99.9)).toBe('99.9%');
});
it('should handle negative consistency', () => {
expect(DashboardConsistencyDisplay.format(-10)).toBe('-10%');
});
});
});

View File

@@ -0,0 +1,38 @@
import { describe, it, expect } from 'vitest';
import { DashboardCountDisplay } from './DashboardCountDisplay';
describe('DashboardCountDisplay', () => {
describe('happy paths', () => {
it('should format positive numbers correctly', () => {
expect(DashboardCountDisplay.format(0)).toBe('0');
expect(DashboardCountDisplay.format(1)).toBe('1');
expect(DashboardCountDisplay.format(100)).toBe('100');
expect(DashboardCountDisplay.format(1000)).toBe('1000');
});
it('should handle null values', () => {
expect(DashboardCountDisplay.format(null)).toBe('0');
});
it('should handle undefined values', () => {
expect(DashboardCountDisplay.format(undefined)).toBe('0');
});
});
describe('edge cases', () => {
it('should handle negative numbers', () => {
expect(DashboardCountDisplay.format(-1)).toBe('-1');
expect(DashboardCountDisplay.format(-100)).toBe('-100');
});
it('should handle large numbers', () => {
expect(DashboardCountDisplay.format(999999)).toBe('999999');
expect(DashboardCountDisplay.format(1000000)).toBe('1000000');
});
it('should handle decimal numbers', () => {
expect(DashboardCountDisplay.format(1.5)).toBe('1.5');
expect(DashboardCountDisplay.format(100.99)).toBe('100.99');
});
});
});

View File

@@ -0,0 +1,94 @@
import { describe, it, expect } from 'vitest';
import { DashboardDateDisplay } from './DashboardDateDisplay';
describe('DashboardDateDisplay', () => {
describe('happy paths', () => {
it('should format future date correctly', () => {
const now = new Date();
const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 24 hours from now
const result = DashboardDateDisplay.format(futureDate);
expect(result.date).toMatch(/^[A-Za-z]{3}, [A-Za-z]{3} \d{1,2}, \d{4}$/);
expect(result.time).toMatch(/^\d{2}:\d{2}$/);
expect(result.relative).toBe('1d');
});
it('should format date less than 24 hours correctly', () => {
const now = new Date();
const futureDate = new Date(now.getTime() + 6 * 60 * 60 * 1000); // 6 hours from now
const result = DashboardDateDisplay.format(futureDate);
expect(result.relative).toBe('6h');
});
it('should format date more than 24 hours correctly', () => {
const now = new Date();
const futureDate = new Date(now.getTime() + 48 * 60 * 60 * 1000); // 2 days from now
const result = DashboardDateDisplay.format(futureDate);
expect(result.relative).toBe('2d');
});
it('should format past date correctly', () => {
const now = new Date();
const pastDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 24 hours ago
const result = DashboardDateDisplay.format(pastDate);
expect(result.relative).toBe('Past');
});
it('should format current date correctly', () => {
const now = new Date();
const result = DashboardDateDisplay.format(now);
expect(result.relative).toBe('Now');
});
it('should format date with leading zeros in time', () => {
const date = new Date('2024-01-15T05:03:00');
const result = DashboardDateDisplay.format(date);
expect(result.time).toBe('05:03');
});
});
describe('edge cases', () => {
it('should handle midnight correctly', () => {
const date = new Date('2024-01-15T00:00:00');
const result = DashboardDateDisplay.format(date);
expect(result.time).toBe('00:00');
});
it('should handle end of day correctly', () => {
const date = new Date('2024-01-15T23:59:59');
const result = DashboardDateDisplay.format(date);
expect(result.time).toBe('23:59');
});
it('should handle different days of week', () => {
const date = new Date('2024-01-15'); // Monday
const result = DashboardDateDisplay.format(date);
expect(result.date).toContain('Mon');
});
it('should handle different months', () => {
const date = new Date('2024-01-15');
const result = DashboardDateDisplay.format(date);
expect(result.date).toContain('Jan');
});
});
});

View File

@@ -0,0 +1,30 @@
import { describe, it, expect } from 'vitest';
import { DashboardLeaguePositionDisplay } from './DashboardLeaguePositionDisplay';
describe('DashboardLeaguePositionDisplay', () => {
describe('happy paths', () => {
it('should format position correctly', () => {
expect(DashboardLeaguePositionDisplay.format(1)).toBe('#1');
expect(DashboardLeaguePositionDisplay.format(5)).toBe('#5');
expect(DashboardLeaguePositionDisplay.format(100)).toBe('#100');
});
it('should handle null values', () => {
expect(DashboardLeaguePositionDisplay.format(null)).toBe('-');
});
it('should handle undefined values', () => {
expect(DashboardLeaguePositionDisplay.format(undefined)).toBe('-');
});
});
describe('edge cases', () => {
it('should handle position 0', () => {
expect(DashboardLeaguePositionDisplay.format(0)).toBe('#0');
});
it('should handle large positions', () => {
expect(DashboardLeaguePositionDisplay.format(999)).toBe('#999');
});
});
});

View File

@@ -0,0 +1,22 @@
import { describe, it, expect } from 'vitest';
import { DashboardRankDisplay } from './DashboardRankDisplay';
describe('DashboardRankDisplay', () => {
describe('happy paths', () => {
it('should format rank correctly', () => {
expect(DashboardRankDisplay.format(1)).toBe('1');
expect(DashboardRankDisplay.format(42)).toBe('42');
expect(DashboardRankDisplay.format(100)).toBe('100');
});
});
describe('edge cases', () => {
it('should handle rank 0', () => {
expect(DashboardRankDisplay.format(0)).toBe('0');
});
it('should handle large ranks', () => {
expect(DashboardRankDisplay.format(999999)).toBe('999999');
});
});
});

View File

@@ -0,0 +1,369 @@
import { describe, it, expect } from 'vitest';
import { DashboardViewDataBuilder } from '../builders/view-data/DashboardViewDataBuilder';
import { DashboardDateDisplay } from './DashboardDateDisplay';
import { DashboardCountDisplay } from './DashboardCountDisplay';
import { DashboardRankDisplay } from './DashboardRankDisplay';
import { DashboardConsistencyDisplay } from './DashboardConsistencyDisplay';
import { DashboardLeaguePositionDisplay } from './DashboardLeaguePositionDisplay';
import { RatingDisplay } from './RatingDisplay';
import type { DashboardOverviewDTO } from '@/lib/types/generated/DashboardOverviewDTO';
describe('Dashboard View Data - Cross-Component Consistency', () => {
describe('common patterns', () => {
it('should all use consistent formatting for numeric values', () => {
const dashboardDTO: DashboardOverviewDTO = {
currentDriver: {
id: 'driver-123',
name: 'John Doe',
country: 'USA',
rating: 1234.56,
globalRank: 42,
totalRaces: 150,
wins: 25,
podiums: 60,
consistency: 85,
},
myUpcomingRaces: [],
otherUpcomingRaces: [],
upcomingRaces: [],
activeLeaguesCount: 3,
nextRace: null,
recentResults: [],
leagueStandingsSummaries: [
{
leagueId: 'league-1',
leagueName: 'Test League',
position: 5,
totalDrivers: 50,
points: 1250,
},
],
feedSummary: {
notificationCount: 0,
items: [],
},
friends: [
{ id: 'friend-1', name: 'Alice', country: 'UK' },
{ id: 'friend-2', name: 'Bob', country: 'Germany' },
],
};
const result = DashboardViewDataBuilder.build(dashboardDTO);
// All numeric values should be formatted as strings
expect(typeof result.currentDriver.rating).toBe('string');
expect(typeof result.currentDriver.rank).toBe('string');
expect(typeof result.currentDriver.totalRaces).toBe('string');
expect(typeof result.currentDriver.wins).toBe('string');
expect(typeof result.currentDriver.podiums).toBe('string');
expect(typeof result.currentDriver.consistency).toBe('string');
expect(typeof result.activeLeaguesCount).toBe('string');
expect(typeof result.friendCount).toBe('string');
expect(typeof result.leagueStandings[0].position).toBe('string');
expect(typeof result.leagueStandings[0].points).toBe('string');
expect(typeof result.leagueStandings[0].totalDrivers).toBe('string');
});
it('should all handle missing data gracefully', () => {
const dashboardDTO: DashboardOverviewDTO = {
myUpcomingRaces: [],
otherUpcomingRaces: [],
upcomingRaces: [],
activeLeaguesCount: 0,
nextRace: null,
recentResults: [],
leagueStandingsSummaries: [],
feedSummary: {
notificationCount: 0,
items: [],
},
friends: [],
};
const result = DashboardViewDataBuilder.build(dashboardDTO);
// All fields should have safe defaults
expect(result.currentDriver.name).toBe('');
expect(result.currentDriver.avatarUrl).toBe('');
expect(result.currentDriver.country).toBe('');
expect(result.currentDriver.rating).toBe('0.0');
expect(result.currentDriver.rank).toBe('0');
expect(result.currentDriver.totalRaces).toBe('0');
expect(result.currentDriver.wins).toBe('0');
expect(result.currentDriver.podiums).toBe('0');
expect(result.currentDriver.consistency).toBe('0%');
expect(result.nextRace).toBeNull();
expect(result.upcomingRaces).toEqual([]);
expect(result.leagueStandings).toEqual([]);
expect(result.feedItems).toEqual([]);
expect(result.friends).toEqual([]);
expect(result.activeLeaguesCount).toBe('0');
expect(result.friendCount).toBe('0');
});
it('should all preserve ISO timestamps for serialization', () => {
const now = new Date();
const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000);
const feedTimestamp = new Date(now.getTime() - 30 * 60 * 1000);
const dashboardDTO: DashboardOverviewDTO = {
myUpcomingRaces: [],
otherUpcomingRaces: [],
upcomingRaces: [],
activeLeaguesCount: 1,
nextRace: {
id: 'race-1',
track: 'Spa',
car: 'Porsche',
scheduledAt: futureDate.toISOString(),
status: 'scheduled',
isMyLeague: true,
},
recentResults: [],
leagueStandingsSummaries: [],
feedSummary: {
notificationCount: 1,
items: [
{
id: 'feed-1',
type: 'notification',
headline: 'Test',
timestamp: feedTimestamp.toISOString(),
},
],
},
friends: [],
};
const result = DashboardViewDataBuilder.build(dashboardDTO);
// All timestamps should be preserved as ISO strings
expect(result.nextRace?.scheduledAt).toBe(futureDate.toISOString());
expect(result.feedItems[0].timestamp).toBe(feedTimestamp.toISOString());
});
it('should all handle boolean flags correctly', () => {
const dashboardDTO: DashboardOverviewDTO = {
myUpcomingRaces: [],
otherUpcomingRaces: [],
upcomingRaces: [
{
id: 'race-1',
track: 'Spa',
car: 'Porsche',
scheduledAt: new Date().toISOString(),
status: 'scheduled',
isMyLeague: true,
},
{
id: 'race-2',
track: 'Monza',
car: 'Ferrari',
scheduledAt: new Date().toISOString(),
status: 'scheduled',
isMyLeague: false,
},
],
activeLeaguesCount: 1,
nextRace: null,
recentResults: [],
leagueStandingsSummaries: [],
feedSummary: {
notificationCount: 0,
items: [],
},
friends: [],
};
const result = DashboardViewDataBuilder.build(dashboardDTO);
expect(result.upcomingRaces[0].isMyLeague).toBe(true);
expect(result.upcomingRaces[1].isMyLeague).toBe(false);
});
});
describe('data integrity', () => {
it('should maintain data consistency across transformations', () => {
const dashboardDTO: DashboardOverviewDTO = {
currentDriver: {
id: 'driver-123',
name: 'John Doe',
country: 'USA',
rating: 1234.56,
globalRank: 42,
totalRaces: 150,
wins: 25,
podiums: 60,
consistency: 85,
},
myUpcomingRaces: [],
otherUpcomingRaces: [],
upcomingRaces: [],
activeLeaguesCount: 3,
nextRace: null,
recentResults: [],
leagueStandingsSummaries: [],
feedSummary: {
notificationCount: 5,
items: [],
},
friends: [
{ id: 'friend-1', name: 'Alice', country: 'UK' },
{ id: 'friend-2', name: 'Bob', country: 'Germany' },
],
};
const result = DashboardViewDataBuilder.build(dashboardDTO);
// Verify derived fields match their source data
expect(result.friendCount).toBe(dashboardDTO.friends.length.toString());
expect(result.activeLeaguesCount).toBe(dashboardDTO.activeLeaguesCount.toString());
expect(result.hasFriends).toBe(dashboardDTO.friends.length > 0);
expect(result.hasUpcomingRaces).toBe(dashboardDTO.upcomingRaces.length > 0);
expect(result.hasLeagueStandings).toBe(dashboardDTO.leagueStandingsSummaries.length > 0);
expect(result.hasFeedItems).toBe(dashboardDTO.feedSummary.items.length > 0);
});
it('should handle complex real-world scenarios', () => {
const now = new Date();
const race1Date = new Date(now.getTime() + 2 * 24 * 60 * 60 * 1000);
const race2Date = new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000);
const feedTimestamp = new Date(now.getTime() - 60 * 60 * 1000);
const dashboardDTO: DashboardOverviewDTO = {
currentDriver: {
id: 'driver-123',
name: 'John Doe',
country: 'USA',
avatarUrl: 'https://example.com/avatar.jpg',
rating: 2456.78,
globalRank: 15,
totalRaces: 250,
wins: 45,
podiums: 120,
consistency: 92.5,
},
myUpcomingRaces: [],
otherUpcomingRaces: [],
upcomingRaces: [
{
id: 'race-1',
leagueId: 'league-1',
leagueName: 'Pro League',
track: 'Spa',
car: 'Porsche 911 GT3',
scheduledAt: race1Date.toISOString(),
status: 'scheduled',
isMyLeague: true,
},
{
id: 'race-2',
track: 'Monza',
car: 'Ferrari 488 GT3',
scheduledAt: race2Date.toISOString(),
status: 'scheduled',
isMyLeague: false,
},
],
activeLeaguesCount: 2,
nextRace: {
id: 'race-1',
leagueId: 'league-1',
leagueName: 'Pro League',
track: 'Spa',
car: 'Porsche 911 GT3',
scheduledAt: race1Date.toISOString(),
status: 'scheduled',
isMyLeague: true,
},
recentResults: [],
leagueStandingsSummaries: [
{
leagueId: 'league-1',
leagueName: 'Pro League',
position: 3,
totalDrivers: 100,
points: 2450,
},
{
leagueId: 'league-2',
leagueName: 'Rookie League',
position: 1,
totalDrivers: 50,
points: 1800,
},
],
feedSummary: {
notificationCount: 3,
items: [
{
id: 'feed-1',
type: 'race_result',
headline: 'Race completed',
body: 'You finished 3rd in the Pro League race',
timestamp: feedTimestamp.toISOString(),
ctaLabel: 'View Results',
ctaHref: '/races/123',
},
{
id: 'feed-2',
type: 'league_update',
headline: 'League standings updated',
body: 'You moved up 2 positions',
timestamp: feedTimestamp.toISOString(),
},
],
},
friends: [
{ id: 'friend-1', name: 'Alice', country: 'UK', avatarUrl: 'https://example.com/alice.jpg' },
{ id: 'friend-2', name: 'Bob', country: 'Germany' },
{ id: 'friend-3', name: 'Charlie', country: 'France', avatarUrl: 'https://example.com/charlie.jpg' },
],
};
const result = DashboardViewDataBuilder.build(dashboardDTO);
// Verify all transformations
expect(result.currentDriver.name).toBe('John Doe');
expect(result.currentDriver.rating).toBe('2,457');
expect(result.currentDriver.rank).toBe('15');
expect(result.currentDriver.totalRaces).toBe('250');
expect(result.currentDriver.wins).toBe('45');
expect(result.currentDriver.podiums).toBe('120');
expect(result.currentDriver.consistency).toBe('92.5%');
expect(result.nextRace).not.toBeNull();
expect(result.nextRace?.id).toBe('race-1');
expect(result.nextRace?.track).toBe('Spa');
expect(result.nextRace?.isMyLeague).toBe(true);
expect(result.upcomingRaces).toHaveLength(2);
expect(result.upcomingRaces[0].isMyLeague).toBe(true);
expect(result.upcomingRaces[1].isMyLeague).toBe(false);
expect(result.leagueStandings).toHaveLength(2);
expect(result.leagueStandings[0].position).toBe('#3');
expect(result.leagueStandings[0].points).toBe('2450');
expect(result.leagueStandings[1].position).toBe('#1');
expect(result.leagueStandings[1].points).toBe('1800');
expect(result.feedItems).toHaveLength(2);
expect(result.feedItems[0].type).toBe('race_result');
expect(result.feedItems[0].ctaLabel).toBe('View Results');
expect(result.feedItems[1].type).toBe('league_update');
expect(result.feedItems[1].ctaLabel).toBeUndefined();
expect(result.friends).toHaveLength(3);
expect(result.friends[0].avatarUrl).toBe('https://example.com/alice.jpg');
expect(result.friends[1].avatarUrl).toBe('');
expect(result.friends[2].avatarUrl).toBe('https://example.com/charlie.jpg');
expect(result.activeLeaguesCount).toBe('2');
expect(result.friendCount).toBe('3');
expect(result.hasUpcomingRaces).toBe(true);
expect(result.hasLeagueStandings).toBe(true);
expect(result.hasFeedItems).toBe(true);
expect(result.hasFriends).toBe(true);
});
});
});

View File

@@ -0,0 +1,38 @@
import { describe, it, expect } from 'vitest';
import { RatingDisplay } from './RatingDisplay';
describe('RatingDisplay', () => {
describe('happy paths', () => {
it('should format rating correctly', () => {
expect(RatingDisplay.format(0)).toBe('0');
expect(RatingDisplay.format(1234.56)).toBe('1,235');
expect(RatingDisplay.format(9999.99)).toBe('10,000');
});
it('should handle null values', () => {
expect(RatingDisplay.format(null)).toBe('—');
});
it('should handle undefined values', () => {
expect(RatingDisplay.format(undefined)).toBe('—');
});
});
describe('edge cases', () => {
it('should round down correctly', () => {
expect(RatingDisplay.format(1234.4)).toBe('1,234');
});
it('should round up correctly', () => {
expect(RatingDisplay.format(1234.6)).toBe('1,235');
});
it('should handle decimal ratings', () => {
expect(RatingDisplay.format(1234.5)).toBe('1,235');
});
it('should handle large ratings', () => {
expect(RatingDisplay.format(999999.99)).toBe('1,000,000');
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,240 @@
/**
* Admin Feature Flow Tests
*
* These tests verify routing, guards, navigation, cross-screen state, and user flows
* for the admin module. They run with real frontend and mocked contracts.
*
* @file apps/website/tests/flows/admin.test.tsx
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { AdminDashboardWrapper } from '@/client-wrapper/AdminDashboardWrapper';
import { AdminUsersWrapper } from '@/client-wrapper/AdminUsersWrapper';
import type { AdminDashboardViewData } from '@/lib/view-data/AdminDashboardViewData';
import type { AdminUsersViewData } from '@/lib/view-data/AdminUsersViewData';
import { updateUserStatus, deleteUser } from '@/app/actions/adminActions';
import { Result } from '@/lib/contracts/Result';
import React from 'react';
// Mock next/navigation
const mockPush = vi.fn();
const mockRefresh = vi.fn();
const mockSearchParams = new URLSearchParams();
vi.mock('next/navigation', () => ({
useRouter: () => ({
push: mockPush,
refresh: mockRefresh,
}),
useSearchParams: () => mockSearchParams,
usePathname: () => '/admin',
}));
// Mock server actions
vi.mock('@/app/actions/adminActions', () => ({
updateUserStatus: vi.fn(),
deleteUser: vi.fn(),
}));
describe('Admin Feature Flow', () => {
beforeEach(() => {
vi.clearAllMocks();
mockSearchParams.delete('search');
mockSearchParams.delete('role');
mockSearchParams.delete('status');
});
describe('Admin Dashboard Flow', () => {
const mockDashboardData: AdminDashboardViewData = {
stats: {
totalUsers: 150,
activeUsers: 120,
suspendedUsers: 25,
deletedUsers: 5,
systemAdmins: 10,
recentLogins: 45,
newUsersToday: 3,
},
};
it('should display dashboard statistics', () => {
render(<AdminDashboardWrapper viewData={mockDashboardData} />);
expect(screen.getByText('150')).toBeDefined();
expect(screen.getByText('120')).toBeDefined();
expect(screen.getByText('25')).toBeDefined();
expect(screen.getByText('5')).toBeDefined();
expect(screen.getByText('10')).toBeDefined();
});
it('should trigger refresh when refresh button is clicked', () => {
render(<AdminDashboardWrapper viewData={mockDashboardData} />);
const refreshButton = screen.getByText(/Refresh Telemetry/i);
fireEvent.click(refreshButton);
expect(mockRefresh).toHaveBeenCalled();
});
});
describe('Admin Users Management Flow', () => {
const mockUsersData: AdminUsersViewData = {
users: [
{
id: 'user-1',
email: 'john@example.com',
displayName: 'John Doe',
roles: ['admin'],
status: 'active',
isSystemAdmin: true,
createdAt: '2024-01-15T10:00:00Z',
updatedAt: '2024-01-15T10:00:00Z',
},
{
id: 'user-2',
email: 'jane@example.com',
displayName: 'Jane Smith',
roles: ['user'],
status: 'active',
isSystemAdmin: false,
createdAt: '2024-01-14T15:30:00Z',
updatedAt: '2024-01-14T15:30:00Z',
},
],
total: 2,
page: 1,
limit: 50,
totalPages: 1,
activeUserCount: 2,
adminCount: 1,
};
it('should display users list', () => {
render(<AdminUsersWrapper viewData={mockUsersData} />);
expect(screen.getByText('john@example.com')).toBeDefined();
expect(screen.getByText('jane@example.com')).toBeDefined();
expect(screen.getByText('John Doe')).toBeDefined();
expect(screen.getByText('Jane Smith')).toBeDefined();
});
it('should update URL when searching', () => {
render(<AdminUsersWrapper viewData={mockUsersData} />);
const searchInput = screen.getByPlaceholderText(/Search by email or name/i);
fireEvent.change(searchInput, { target: { value: 'john' } });
expect(mockPush).toHaveBeenCalledWith(expect.stringContaining('search=john'));
});
it('should update URL when filtering by role', () => {
render(<AdminUsersWrapper viewData={mockUsersData} />);
const selects = screen.getAllByRole('combobox');
// First select is role, second is status based on UserFilters.tsx
fireEvent.change(selects[0], { target: { value: 'admin' } });
expect(mockPush).toHaveBeenCalledWith(expect.stringContaining('role=admin'));
});
it('should update URL when filtering by status', () => {
render(<AdminUsersWrapper viewData={mockUsersData} />);
const selects = screen.getAllByRole('combobox');
fireEvent.change(selects[1], { target: { value: 'active' } });
expect(mockPush).toHaveBeenCalledWith(expect.stringContaining('status=active'));
});
it('should clear filters when clear button is clicked', () => {
// Set some filters in searchParams mock if needed, but wrapper uses searchParams.get
// Actually, the "Clear all" button only appears if filters are present
mockSearchParams.set('search', 'john');
render(<AdminUsersWrapper viewData={mockUsersData} />);
const clearButton = screen.getByText(/Clear all/i);
fireEvent.click(clearButton);
expect(mockPush).toHaveBeenCalledWith('/admin/users');
});
it('should select individual users', () => {
render(<AdminUsersWrapper viewData={mockUsersData} />);
const checkboxes = screen.getAllByRole('checkbox');
// First checkbox is "Select all users", second is user-1
fireEvent.click(checkboxes[1]);
// Use getAllByText because '1' appears in stats too
expect(screen.getAllByText('1').length).toBeGreaterThan(0);
expect(screen.getByText(/Items Selected/i)).toBeDefined();
});
it('should select all users', () => {
render(<AdminUsersWrapper viewData={mockUsersData} />);
// Use getAllByRole and find the one with the right aria-label
const checkboxes = screen.getAllByRole('checkbox');
// In JSDOM, aria-label might be accessed differently or the component might not be rendering it as expected
// Let's try to find it by index if label fails, but first try a more robust search
const selectAllCheckbox = checkboxes[0]; // Usually the first one in the header
fireEvent.click(selectAllCheckbox);
expect(screen.getAllByText('2').length).toBeGreaterThan(0);
expect(screen.getByText(/Items Selected/i)).toBeDefined();
});
it('should call updateUserStatus action', async () => {
vi.mocked(updateUserStatus).mockResolvedValue(Result.ok({ success: true }));
render(<AdminUsersWrapper viewData={mockUsersData} />);
const suspendButtons = screen.getAllByRole('button', { name: /Suspend/i });
fireEvent.click(suspendButtons[0]);
await waitFor(() => {
expect(updateUserStatus).toHaveBeenCalledWith('user-1', 'suspended');
});
expect(mockRefresh).toHaveBeenCalled();
});
it('should open delete confirmation and call deleteUser action', async () => {
vi.mocked(deleteUser).mockResolvedValue(Result.ok({ success: true }));
render(<AdminUsersWrapper viewData={mockUsersData} />);
const deleteButtons = screen.getAllByRole('button', { name: /Delete/i });
// There are 2 users, so 2 delete buttons in the table
fireEvent.click(deleteButtons[0]);
// Verify dialog is open - ConfirmDialog has title "Delete User"
// We use getAllByText because "Delete User" is also the button label
const dialogTitles = screen.getAllByText(/Delete User/i);
expect(dialogTitles.length).toBeGreaterThan(0);
expect(screen.getByText(/Are you sure you want to delete this user/i)).toBeDefined();
// The confirm button in the dialog
const confirmButton = screen.getByRole('button', { name: 'Delete User' });
fireEvent.click(confirmButton);
await waitFor(() => {
expect(deleteUser).toHaveBeenCalledWith('user-1');
});
expect(mockRefresh).toHaveBeenCalled();
});
it('should handle action errors gracefully', async () => {
vi.mocked(updateUserStatus).mockResolvedValue(Result.err('Failed to update'));
render(<AdminUsersWrapper viewData={mockUsersData} />);
const suspendButtons = screen.getAllByRole('button', { name: /Suspend/i });
fireEvent.click(suspendButtons[0]);
await waitFor(() => {
expect(screen.getByText('Failed to update')).toBeDefined();
});
});
});
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,29 +0,0 @@
/**
* View Data Layer Tests - Leagues Functionality
*
* This test file will cover the view data layer for leagues functionality.
*
* The view data layer is responsible for:
* - DTO → UI model mapping
* - Formatting, sorting, and grouping
* - Derived fields and defaults
* - UI-specific semantics
*
* This layer isolates the UI from API churn by providing a stable interface
* between the API layer and the presentation layer.
*
* Test coverage will include:
* - League list data transformation and sorting
* - Individual league profile view models
* - League roster data formatting and member management
* - League schedule and standings view models
* - League stewarding and protest handling data transformation
* - League wallet and sponsorship data formatting
* - League creation and migration data transformation
* - Derived league fields (member counts, status, permissions, etc.)
* - Default values and fallbacks for league views
* - League-specific formatting (dates, points, positions, race formats, etc.)
* - Data grouping and categorization for league components
* - League search and filtering view models
* - Real-time league data updates and state management
*/

View File

@@ -1,29 +0,0 @@
/**
* View Data Layer Tests - Media Functionality
*
* This test file will cover the view data layer for media functionality.
*
* The view data layer is responsible for:
* - DTO → UI model mapping
* - Formatting, sorting, and grouping
* - Derived fields and defaults
* - UI-specific semantics
*
* This layer isolates the UI from API churn by providing a stable interface
* between the API layer and the presentation layer.
*
* Test coverage will include:
* - Avatar page data transformation and display
* - Avatar route data handling for driver-specific avatars
* - Category icon data mapping and formatting
* - League cover and logo data transformation
* - Sponsor logo data handling and display
* - Team logo data mapping and validation
* - Track image data transformation and UI state
* - Media upload and validation view models
* - Media deletion confirmation and state management
* - Derived media fields (file size, format, dimensions, etc.)
* - Default values and fallbacks for media views
* - Media-specific formatting (image optimization, aspect ratios, etc.)
* - Media access control and permission view models
*/

View File

@@ -1,25 +0,0 @@
/**
* View Data Layer Tests - Onboarding Functionality
*
* This test file will cover the view data layer for onboarding functionality.
*
* The view data layer is responsible for:
* - DTO → UI model mapping
* - Formatting, sorting, and grouping
* - Derived fields and defaults
* - UI-specific semantics
*
* This layer isolates the UI from API churn by providing a stable interface
* between the API layer and the presentation layer.
*
* Test coverage will include:
* - Onboarding page data transformation and validation
* - Onboarding wizard view models and field formatting
* - Authentication and authorization checks for onboarding flow
* - Redirect logic based on onboarding status (already onboarded, not authenticated)
* - Onboarding-specific formatting and validation
* - Derived fields for onboarding UI components (progress, completion status, etc.)
* - Default values and fallbacks for onboarding views
* - Onboarding step data mapping and state management
* - Error handling and fallback UI states for onboarding flow
*/

View File

@@ -1,26 +0,0 @@
/**
* View Data Layer Tests - Profile Functionality
*
* This test file will cover the view data layer for profile functionality.
*
* The view data layer is responsible for:
* - DTO → UI model mapping
* - Formatting, sorting, and grouping
* - Derived fields and defaults
* - UI-specific semantics
*
* This layer isolates the UI from API churn by providing a stable interface
* between the API layer and the presentation layer.
*
* Test coverage will include:
* - Driver profile data transformation and formatting
* - Profile statistics (rating, rank, race counts, finishes, consistency, etc.)
* - Team membership data mapping and role labeling
* - Extended profile data (timezone, racing style, favorite track/car, etc.)
* - Social handles formatting and URL generation
* - Achievement data transformation and icon mapping
* - Friends list data mapping and display formatting
* - Derived fields (percentile, consistency, looking for team, open to requests)
* - Default values and fallbacks for profile views
* - Profile-specific formatting (country flags, date labels, etc.)
*/

View File

@@ -1,29 +0,0 @@
/**
* View Data Layer Tests - Races Functionality
*
* This test file will cover the view data layer for races functionality.
*
* The view data layer is responsible for:
* - DTO → UI model mapping
* - Formatting, sorting, and grouping
* - Derived fields and defaults
* - UI-specific semantics
*
* This layer isolates the UI from API churn by providing a stable interface
* between the API layer and the presentation layer.
*
* Test coverage will include:
* - Race list data transformation and sorting
* - Individual race page view models (race details, schedule, participants)
* - Race results data formatting and ranking calculations
* - Stewarding data transformation (protests, penalties, incidents)
* - All races page data aggregation and filtering
* - Derived race fields (status, eligibility, availability, etc.)
* - Default values and fallbacks for race views
* - Race-specific formatting (lap times, gaps, points, positions, etc.)
* - Data grouping and categorization for race components (by series, date, type)
* - Race search and filtering view models
* - Real-time race updates and state management
* - Historical race data transformation
* - Race registration and withdrawal data handling
*/

View File

@@ -1,29 +0,0 @@
/**
* View Data Layer Tests - Sponsor Functionality
*
* This test file will cover the view data layer for sponsor functionality.
*
* The view data layer is responsible for:
* - DTO → UI model mapping
* - Formatting, sorting, and grouping
* - Derived fields and defaults
* - UI-specific semantics
*
* This layer isolates the UI from API churn by providing a stable interface
* between the API layer and the presentation layer.
*
* Test coverage will include:
* - Sponsor dashboard data transformation and metrics
* - Sponsor billing and payment view models
* - Campaign management data formatting and status tracking
* - League sponsorship data aggregation and tier calculations
* - Sponsor settings and configuration view models
* - Sponsor signup and onboarding data handling
* - Derived sponsor fields (engagement metrics, ROI calculations, etc.)
* - Default values and fallbacks for sponsor views
* - Sponsor-specific formatting (budgets, impressions, clicks, conversions)
* - Data grouping and categorization for sponsor components (by campaign, league, status)
* - Sponsor search and filtering view models
* - Real-time sponsor metrics and state management
* - Historical sponsor performance data transformation
*/

View File

@@ -1,28 +0,0 @@
/**
* View Data Layer Tests - Teams Functionality
*
* This test file will cover the view data layer for teams functionality.
*
* The view data layer is responsible for:
* - DTO → UI model mapping
* - Formatting, sorting, and grouping
* - Derived fields and defaults
* - UI-specific semantics
*
* This layer isolates the UI from API churn by providing a stable interface
* between the API layer and the presentation layer.
*
* Test coverage will include:
* - Team list data transformation and sorting
* - Individual team profile view models
* - Team creation form data handling
* - Team leaderboard data transformation
* - Team statistics and metrics formatting
* - Derived team fields (performance ratings, rankings, etc.)
* - Default values and fallbacks for team views
* - Team-specific formatting (points, positions, member counts, etc.)
* - Data grouping and categorization for team components
* - Team search and filtering view models
* - Team member data transformation
* - Team comparison data transformation
*/

View File

@@ -1,408 +0,0 @@
import { describe, expect, it } from 'vitest';
import { AdminUser } from '@core/admin/domain/entities/AdminUser';
import { AdminUserOrmEntity } from '../entities/AdminUserOrmEntity';
import { AdminUserOrmMapper } from './AdminUserOrmMapper';
import { TypeOrmAdminSchemaError } from '../errors/TypeOrmAdminSchemaError';
describe('AdminUserOrmMapper', () => {
describe('TDD - Test First', () => {
describe('toDomain', () => {
it('should map valid ORM entity to domain entity', () => {
// Arrange
const entity = new AdminUserOrmEntity();
entity.id = 'user-123';
entity.email = 'test@example.com';
entity.displayName = 'Test User';
entity.roles = ['owner'];
entity.status = 'active';
entity.createdAt = new Date('2024-01-01');
entity.updatedAt = new Date('2024-01-02');
const mapper = new AdminUserOrmMapper();
// Act
const domain = mapper.toDomain(entity);
// Assert
expect(domain.id.value).toBe('user-123');
expect(domain.email.value).toBe('test@example.com');
expect(domain.displayName).toBe('Test User');
expect(domain.roles).toHaveLength(1);
expect(domain.roles[0]!.value).toBe('owner');
expect(domain.status.value).toBe('active');
expect(domain.createdAt).toEqual(new Date('2024-01-01'));
expect(domain.updatedAt).toEqual(new Date('2024-01-02'));
});
it('should map entity with optional fields', () => {
// Arrange
const entity = new AdminUserOrmEntity();
entity.id = 'user-123';
entity.email = 'test@example.com';
entity.displayName = 'Test User';
entity.roles = ['user'];
entity.status = 'active';
entity.createdAt = new Date('2024-01-01');
entity.updatedAt = new Date('2024-01-02');
entity.primaryDriverId = 'driver-456';
entity.lastLoginAt = new Date('2024-01-03');
const mapper = new AdminUserOrmMapper();
// Act
const domain = mapper.toDomain(entity);
// Assert
expect(domain.primaryDriverId).toBe('driver-456');
expect(domain.lastLoginAt).toEqual(new Date('2024-01-03'));
});
it('should handle null optional fields', () => {
// Arrange
const entity = new AdminUserOrmEntity();
entity.id = 'user-123';
entity.email = 'test@example.com';
entity.displayName = 'Test User';
entity.roles = ['user'];
entity.status = 'active';
entity.createdAt = new Date('2024-01-01');
entity.updatedAt = new Date('2024-01-02');
entity.primaryDriverId = null;
entity.lastLoginAt = null;
const mapper = new AdminUserOrmMapper();
// Act
const domain = mapper.toDomain(entity);
// Assert
expect(domain.primaryDriverId).toBeUndefined();
expect(domain.lastLoginAt).toBeUndefined();
});
it('should throw error for missing id', () => {
// Arrange
const entity = new AdminUserOrmEntity();
entity.id = '';
entity.email = 'test@example.com';
entity.displayName = 'Test User';
entity.roles = ['user'];
entity.status = 'active';
entity.createdAt = new Date('2024-01-01');
entity.updatedAt = new Date('2024-01-02');
const mapper = new AdminUserOrmMapper();
// Act & Assert
expect(() => mapper.toDomain(entity)).toThrow(TypeOrmAdminSchemaError);
expect(() => mapper.toDomain(entity)).toThrow('Field id must be a non-empty string');
});
it('should throw error for missing email', () => {
// Arrange
const entity = new AdminUserOrmEntity();
entity.id = 'user-123';
entity.email = '';
entity.displayName = 'Test User';
entity.roles = ['user'];
entity.status = 'active';
entity.createdAt = new Date('2024-01-01');
entity.updatedAt = new Date('2024-01-02');
const mapper = new AdminUserOrmMapper();
// Act & Assert
expect(() => mapper.toDomain(entity)).toThrow(TypeOrmAdminSchemaError);
expect(() => mapper.toDomain(entity)).toThrow('Field email must be a non-empty string');
});
it('should throw error for missing displayName', () => {
// Arrange
const entity = new AdminUserOrmEntity();
entity.id = 'user-123';
entity.email = 'test@example.com';
entity.displayName = '';
entity.roles = ['user'];
entity.status = 'active';
entity.createdAt = new Date('2024-01-01');
entity.updatedAt = new Date('2024-01-02');
const mapper = new AdminUserOrmMapper();
// Act & Assert
expect(() => mapper.toDomain(entity)).toThrow(TypeOrmAdminSchemaError);
expect(() => mapper.toDomain(entity)).toThrow('Field displayName must be a non-empty string');
});
it('should throw error for invalid roles array', () => {
// Arrange
const entity = new AdminUserOrmEntity();
entity.id = 'user-123';
entity.email = 'test@example.com';
entity.displayName = 'Test User';
entity.roles = null as unknown as string[];
entity.status = 'active';
entity.createdAt = new Date('2024-01-01');
entity.updatedAt = new Date('2024-01-02');
const mapper = new AdminUserOrmMapper();
// Act & Assert
expect(() => mapper.toDomain(entity)).toThrow(TypeOrmAdminSchemaError);
expect(() => mapper.toDomain(entity)).toThrow('Field roles must be an array of strings');
});
it('should throw error for invalid roles array items', () => {
// Arrange
const entity = new AdminUserOrmEntity();
entity.id = 'user-123';
entity.email = 'test@example.com';
entity.displayName = 'Test User';
entity.roles = ['user', 123 as unknown as string];
entity.status = 'active';
entity.createdAt = new Date('2024-01-01');
entity.updatedAt = new Date('2024-01-02');
const mapper = new AdminUserOrmMapper();
// Act & Assert
expect(() => mapper.toDomain(entity)).toThrow(TypeOrmAdminSchemaError);
expect(() => mapper.toDomain(entity)).toThrow('Field roles must be an array of strings');
});
it('should throw error for missing status', () => {
// Arrange
const entity = new AdminUserOrmEntity();
entity.id = 'user-123';
entity.email = 'test@example.com';
entity.displayName = 'Test User';
entity.roles = ['user'];
entity.status = '';
entity.createdAt = new Date('2024-01-01');
entity.updatedAt = new Date('2024-01-02');
const mapper = new AdminUserOrmMapper();
// Act & Assert
expect(() => mapper.toDomain(entity)).toThrow(TypeOrmAdminSchemaError);
expect(() => mapper.toDomain(entity)).toThrow('Field status must be a non-empty string');
});
it('should throw error for invalid createdAt', () => {
// Arrange
const entity = new AdminUserOrmEntity();
entity.id = 'user-123';
entity.email = 'test@example.com';
entity.displayName = 'Test User';
entity.roles = ['user'];
entity.status = 'active';
entity.createdAt = new Date('invalid') as unknown as Date;
entity.updatedAt = new Date('2024-01-02');
const mapper = new AdminUserOrmMapper();
// Act & Assert
expect(() => mapper.toDomain(entity)).toThrow(TypeOrmAdminSchemaError);
expect(() => mapper.toDomain(entity)).toThrow('Field createdAt must be a valid Date');
});
it('should throw error for invalid updatedAt', () => {
// Arrange
const entity = new AdminUserOrmEntity();
entity.id = 'user-123';
entity.email = 'test@example.com';
entity.displayName = 'Test User';
entity.roles = ['user'];
entity.status = 'active';
entity.createdAt = new Date('2024-01-01');
entity.updatedAt = new Date('invalid') as unknown as Date;
const mapper = new AdminUserOrmMapper();
// Act & Assert
expect(() => mapper.toDomain(entity)).toThrow(TypeOrmAdminSchemaError);
expect(() => mapper.toDomain(entity)).toThrow('Field updatedAt must be a valid Date');
});
it('should throw error for invalid primaryDriverId type', () => {
// Arrange
const entity = new AdminUserOrmEntity();
entity.id = 'user-123';
entity.email = 'test@example.com';
entity.displayName = 'Test User';
entity.roles = ['user'];
entity.status = 'active';
entity.createdAt = new Date('2024-01-01');
entity.updatedAt = new Date('2024-01-02');
entity.primaryDriverId = 123 as unknown as string;
const mapper = new AdminUserOrmMapper();
// Act & Assert
expect(() => mapper.toDomain(entity)).toThrow(TypeOrmAdminSchemaError);
expect(() => mapper.toDomain(entity)).toThrow('Field primaryDriverId must be a string or undefined');
});
it('should throw error for invalid lastLoginAt type', () => {
// Arrange
const entity = new AdminUserOrmEntity();
entity.id = 'user-123';
entity.email = 'test@example.com';
entity.displayName = 'Test User';
entity.roles = ['user'];
entity.status = 'active';
entity.createdAt = new Date('2024-01-01');
entity.updatedAt = new Date('2024-01-02');
entity.lastLoginAt = 'invalid' as unknown as Date;
const mapper = new AdminUserOrmMapper();
// Act & Assert
expect(() => mapper.toDomain(entity)).toThrow(TypeOrmAdminSchemaError);
expect(() => mapper.toDomain(entity)).toThrow('Field lastLoginAt must be a valid Date');
});
it('should handle multiple roles', () => {
// Arrange
const entity = new AdminUserOrmEntity();
entity.id = 'user-123';
entity.email = 'test@example.com';
entity.displayName = 'Test User';
entity.roles = ['owner', 'admin'];
entity.status = 'active';
entity.createdAt = new Date('2024-01-01');
entity.updatedAt = new Date('2024-01-02');
const mapper = new AdminUserOrmMapper();
// Act
const domain = mapper.toDomain(entity);
// Assert
expect(domain.roles).toHaveLength(2);
expect(domain.roles.map(r => r.value)).toContain('owner');
expect(domain.roles.map(r => r.value)).toContain('admin');
});
});
describe('toOrmEntity', () => {
it('should map domain entity to ORM entity', () => {
// Arrange
const domain = AdminUser.create({
id: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
roles: ['owner'],
status: 'active',
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-02'),
});
const mapper = new AdminUserOrmMapper();
// Act
const entity = mapper.toOrmEntity(domain);
// Assert
expect(entity.id).toBe('user-123');
expect(entity.email).toBe('test@example.com');
expect(entity.displayName).toBe('Test User');
expect(entity.roles).toEqual(['owner']);
expect(entity.status).toBe('active');
expect(entity.createdAt).toEqual(new Date('2024-01-01'));
expect(entity.updatedAt).toEqual(new Date('2024-01-02'));
});
it('should map domain entity with optional fields', () => {
// Arrange
const domain = AdminUser.create({
id: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
roles: ['user'],
status: 'active',
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-02'),
primaryDriverId: 'driver-456',
lastLoginAt: new Date('2024-01-03'),
});
const mapper = new AdminUserOrmMapper();
// Act
const entity = mapper.toOrmEntity(domain);
// Assert
expect(entity.primaryDriverId).toBe('driver-456');
expect(entity.lastLoginAt).toEqual(new Date('2024-01-03'));
});
it('should handle domain entity without optional fields', () => {
// Arrange
const domain = AdminUser.create({
id: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
roles: ['user'],
status: 'active',
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-02'),
});
const mapper = new AdminUserOrmMapper();
// Act
const entity = mapper.toOrmEntity(domain);
// Assert
expect(entity.primaryDriverId).toBeUndefined();
expect(entity.lastLoginAt).toBeUndefined();
});
it('should map domain entity with multiple roles', () => {
// Arrange
const domain = AdminUser.create({
id: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
roles: ['owner', 'admin'],
status: 'active',
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-02'),
});
const mapper = new AdminUserOrmMapper();
// Act
const entity = mapper.toOrmEntity(domain);
// Assert
expect(entity.roles).toEqual(['owner', 'admin']);
});
});
describe('toStored', () => {
it('should call toDomain for stored entity', () => {
// Arrange
const entity = new AdminUserOrmEntity();
entity.id = 'user-123';
entity.email = 'test@example.com';
entity.displayName = 'Test User';
entity.roles = ['owner'];
entity.status = 'active';
entity.createdAt = new Date('2024-01-01');
entity.updatedAt = new Date('2024-01-02');
const mapper = new AdminUserOrmMapper();
// Act
const domain = mapper.toStored(entity);
// Assert
expect(domain.id.value).toBe('user-123');
expect(domain.email.value).toBe('test@example.com');
expect(domain.displayName).toBe('Test User');
});
});
});
});

View File

@@ -1,253 +0,0 @@
import { describe, expect, it } from 'vitest';
import {
assertNonEmptyString,
assertStringArray,
assertDate,
assertOptionalDate,
assertOptionalString,
} from './TypeOrmAdminSchemaGuards';
import { TypeOrmAdminSchemaError } from '../errors/TypeOrmAdminSchemaError';
describe('TypeOrmAdminSchemaGuards', () => {
describe('TDD - Test First', () => {
describe('assertNonEmptyString', () => {
it('should not throw for valid non-empty string', () => {
// Arrange & Act & Assert
expect(() => assertNonEmptyString('TestEntity', 'fieldName', 'valid string')).not.toThrow();
});
it('should throw for empty string', () => {
// Arrange & Act & Assert
expect(() => assertNonEmptyString('TestEntity', 'fieldName', '')).toThrow(TypeOrmAdminSchemaError);
expect(() => assertNonEmptyString('TestEntity', 'fieldName', '')).toThrow('Field fieldName must be a non-empty string');
});
it('should throw for string with only whitespace', () => {
// Arrange & Act & Assert
expect(() => assertNonEmptyString('TestEntity', 'fieldName', ' ')).toThrow(TypeOrmAdminSchemaError);
expect(() => assertNonEmptyString('TestEntity', 'fieldName', ' ')).toThrow('Field fieldName must be a non-empty string');
});
it('should throw for null', () => {
// Arrange & Act & Assert
expect(() => assertNonEmptyString('TestEntity', 'fieldName', null)).toThrow(TypeOrmAdminSchemaError);
expect(() => assertNonEmptyString('TestEntity', 'fieldName', null)).toThrow('Field fieldName must be a non-empty string');
});
it('should throw for undefined', () => {
// Arrange & Act & Assert
expect(() => assertNonEmptyString('TestEntity', 'fieldName', undefined)).toThrow(TypeOrmAdminSchemaError);
expect(() => assertNonEmptyString('TestEntity', 'fieldName', undefined)).toThrow('Field fieldName must be a non-empty string');
});
it('should throw for number', () => {
// Arrange & Act & Assert
expect(() => assertNonEmptyString('TestEntity', 'fieldName', 123)).toThrow(TypeOrmAdminSchemaError);
expect(() => assertNonEmptyString('TestEntity', 'fieldName', 123)).toThrow('Field fieldName must be a non-empty string');
});
it('should throw for object', () => {
// Arrange & Act & Assert
expect(() => assertNonEmptyString('TestEntity', 'fieldName', {})).toThrow(TypeOrmAdminSchemaError);
expect(() => assertNonEmptyString('TestEntity', 'fieldName', {})).toThrow('Field fieldName must be a non-empty string');
});
it('should throw for array', () => {
// Arrange & Act & Assert
expect(() => assertNonEmptyString('TestEntity', 'fieldName', [])).toThrow(TypeOrmAdminSchemaError);
expect(() => assertNonEmptyString('TestEntity', 'fieldName', [])).toThrow('Field fieldName must be a non-empty string');
});
it('should include entity name in error message', () => {
// Arrange & Act & Assert
expect(() => assertNonEmptyString('AdminUser', 'email', '')).toThrow('[TypeOrmAdminSchemaError] AdminUser.email: INVALID_STRING - Field email must be a non-empty string');
});
});
describe('assertStringArray', () => {
it('should not throw for valid string array', () => {
// Arrange & Act & Assert
expect(() => assertStringArray('TestEntity', 'fieldName', ['a', 'b', 'c'])).not.toThrow();
});
it('should not throw for empty array', () => {
// Arrange & Act & Assert
expect(() => assertStringArray('TestEntity', 'fieldName', [])).not.toThrow();
});
it('should throw for non-array', () => {
// Arrange & Act & Assert
expect(() => assertStringArray('TestEntity', 'fieldName', 'not an array')).toThrow(TypeOrmAdminSchemaError);
expect(() => assertStringArray('TestEntity', 'fieldName', 'not an array')).toThrow('Field fieldName must be an array of strings');
});
it('should throw for null', () => {
// Arrange & Act & Assert
expect(() => assertStringArray('TestEntity', 'fieldName', null)).toThrow(TypeOrmAdminSchemaError);
expect(() => assertStringArray('TestEntity', 'fieldName', null)).toThrow('Field fieldName must be an array of strings');
});
it('should throw for undefined', () => {
// Arrange & Act & Assert
expect(() => assertStringArray('TestEntity', 'fieldName', undefined)).toThrow(TypeOrmAdminSchemaError);
expect(() => assertStringArray('TestEntity', 'fieldName', undefined)).toThrow('Field fieldName must be an array of strings');
});
it('should throw for array with non-string items', () => {
// Arrange & Act & Assert
expect(() => assertStringArray('TestEntity', 'fieldName', ['a', 123, 'c'])).toThrow(TypeOrmAdminSchemaError);
expect(() => assertStringArray('TestEntity', 'fieldName', ['a', 123, 'c'])).toThrow('Field fieldName must be an array of strings');
});
it('should throw for array with null items', () => {
// Arrange & Act & Assert
expect(() => assertStringArray('TestEntity', 'fieldName', ['a', null, 'c'])).toThrow(TypeOrmAdminSchemaError);
expect(() => assertStringArray('TestEntity', 'fieldName', ['a', null, 'c'])).toThrow('Field fieldName must be an array of strings');
});
it('should throw for array with undefined items', () => {
// Arrange & Act & Assert
expect(() => assertStringArray('TestEntity', 'fieldName', ['a', undefined, 'c'])).toThrow(TypeOrmAdminSchemaError);
expect(() => assertStringArray('TestEntity', 'fieldName', ['a', undefined, 'c'])).toThrow('Field fieldName must be an array of strings');
});
it('should throw for array with object items', () => {
// Arrange & Act & Assert
expect(() => assertStringArray('TestEntity', 'fieldName', ['a', {}, 'c'])).toThrow(TypeOrmAdminSchemaError);
expect(() => assertStringArray('TestEntity', 'fieldName', ['a', {}, 'c'])).toThrow('Field fieldName must be an array of strings');
});
it('should include entity name in error message', () => {
// Arrange & Act & Assert
expect(() => assertStringArray('AdminUser', 'roles', null)).toThrow('[TypeOrmAdminSchemaError] AdminUser.roles: INVALID_STRING_ARRAY - Field roles must be an array of strings');
});
});
describe('assertDate', () => {
it('should not throw for valid Date', () => {
// Arrange & Act & Assert
expect(() => assertDate('TestEntity', 'fieldName', new Date())).not.toThrow();
});
it('should not throw for Date with valid timestamp', () => {
// Arrange & Act & Assert
expect(() => assertDate('TestEntity', 'fieldName', new Date('2024-01-01'))).not.toThrow();
});
it('should throw for null', () => {
// Arrange & Act & Assert
expect(() => assertDate('TestEntity', 'fieldName', null)).toThrow(TypeOrmAdminSchemaError);
expect(() => assertDate('TestEntity', 'fieldName', null)).toThrow('Field fieldName must be a valid Date');
});
it('should throw for undefined', () => {
// Arrange & Act & Assert
expect(() => assertDate('TestEntity', 'fieldName', undefined)).toThrow(TypeOrmAdminSchemaError);
expect(() => assertDate('TestEntity', 'fieldName', undefined)).toThrow('Field fieldName must be a valid Date');
});
it('should throw for string', () => {
// Arrange & Act & Assert
expect(() => assertDate('TestEntity', 'fieldName', '2024-01-01')).toThrow(TypeOrmAdminSchemaError);
expect(() => assertDate('TestEntity', 'fieldName', '2024-01-01')).toThrow('Field fieldName must be a valid Date');
});
it('should throw for number', () => {
// Arrange & Act & Assert
expect(() => assertDate('TestEntity', 'fieldName', 1234567890)).toThrow(TypeOrmAdminSchemaError);
expect(() => assertDate('TestEntity', 'fieldName', 1234567890)).toThrow('Field fieldName must be a valid Date');
});
it('should throw for object', () => {
// Arrange & Act & Assert
expect(() => assertDate('TestEntity', 'fieldName', {})).toThrow(TypeOrmAdminSchemaError);
expect(() => assertDate('TestEntity', 'fieldName', {})).toThrow('Field fieldName must be a valid Date');
});
it('should throw for invalid Date (NaN)', () => {
// Arrange & Act & Assert
expect(() => assertDate('TestEntity', 'fieldName', new Date('invalid'))).toThrow(TypeOrmAdminSchemaError);
expect(() => assertDate('TestEntity', 'fieldName', new Date('invalid'))).toThrow('Field fieldName must be a valid Date');
});
it('should include entity name in error message', () => {
// Arrange & Act & Assert
expect(() => assertDate('AdminUser', 'createdAt', null)).toThrow('[TypeOrmAdminSchemaError] AdminUser.createdAt: INVALID_DATE - Field createdAt must be a valid Date');
});
});
describe('assertOptionalDate', () => {
it('should not throw for valid Date', () => {
// Arrange & Act & Assert
expect(() => assertOptionalDate('TestEntity', 'fieldName', new Date())).not.toThrow();
});
it('should not throw for null', () => {
// Arrange & Act & Assert
expect(() => assertOptionalDate('TestEntity', 'fieldName', null)).not.toThrow();
});
it('should not throw for undefined', () => {
// Arrange & Act & Assert
expect(() => assertOptionalDate('TestEntity', 'fieldName', undefined)).not.toThrow();
});
it('should throw for invalid Date', () => {
// Arrange & Act & Assert
expect(() => assertOptionalDate('TestEntity', 'fieldName', new Date('invalid'))).toThrow(TypeOrmAdminSchemaError);
expect(() => assertOptionalDate('TestEntity', 'fieldName', new Date('invalid'))).toThrow('Field fieldName must be a valid Date');
});
it('should throw for string', () => {
// Arrange & Act & Assert
expect(() => assertOptionalDate('TestEntity', 'fieldName', '2024-01-01')).toThrow(TypeOrmAdminSchemaError);
expect(() => assertOptionalDate('TestEntity', 'fieldName', '2024-01-01')).toThrow('Field fieldName must be a valid Date');
});
it('should include entity name in error message', () => {
// Arrange & Act & Assert
expect(() => assertOptionalDate('AdminUser', 'lastLoginAt', new Date('invalid'))).toThrow('[TypeOrmAdminSchemaError] AdminUser.lastLoginAt: INVALID_DATE - Field lastLoginAt must be a valid Date');
});
});
describe('assertOptionalString', () => {
it('should not throw for valid string', () => {
// Arrange & Act & Assert
expect(() => assertOptionalString('TestEntity', 'fieldName', 'valid string')).not.toThrow();
});
it('should not throw for null', () => {
// Arrange & Act & Assert
expect(() => assertOptionalString('TestEntity', 'fieldName', null)).not.toThrow();
});
it('should not throw for undefined', () => {
// Arrange & Act & Assert
expect(() => assertOptionalString('TestEntity', 'fieldName', undefined)).not.toThrow();
});
it('should throw for number', () => {
// Arrange & Act & Assert
expect(() => assertOptionalString('TestEntity', 'fieldName', 123)).toThrow(TypeOrmAdminSchemaError);
expect(() => assertOptionalString('TestEntity', 'fieldName', 123)).toThrow('Field fieldName must be a string or undefined');
});
it('should throw for object', () => {
// Arrange & Act & Assert
expect(() => assertOptionalString('TestEntity', 'fieldName', {})).toThrow(TypeOrmAdminSchemaError);
expect(() => assertOptionalString('TestEntity', 'fieldName', {})).toThrow('Field fieldName must be a string or undefined');
});
it('should throw for array', () => {
// Arrange & Act & Assert
expect(() => assertOptionalString('TestEntity', 'fieldName', [])).toThrow(TypeOrmAdminSchemaError);
expect(() => assertOptionalString('TestEntity', 'fieldName', [])).toThrow('Field fieldName must be a string or undefined');
});
it('should include entity name in error message', () => {
// Arrange & Act & Assert
expect(() => assertOptionalString('AdminUser', 'primaryDriverId', 123)).toThrow('[TypeOrmAdminSchemaError] AdminUser.primaryDriverId: INVALID_OPTIONAL_STRING - Field primaryDriverId must be a string or undefined');
});
});
});
});

View File

@@ -1,116 +0,0 @@
const { RuleTester } = require('eslint');
const rule = require('./domain-no-application');
const ruleTester = new RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
ecmaFeatures: {
jsx: false,
},
},
});
ruleTester.run('domain-no-application', rule, {
valid: [
// Domain file importing from domain
{
filename: '/path/to/core/domain/user/User.ts',
code: "import { UserId } from './UserId';",
},
// Domain file importing from shared
{
filename: '/path/to/core/domain/user/User.ts',
code: "import { ValueObject } from '../shared/ValueObject';",
},
// Domain file importing from ports
{
filename: '/path/to/core/domain/user/User.ts',
code: "import { UserRepository } from '../ports/UserRepository';",
},
// Non-domain file importing from application
{
filename: '/path/to/core/application/user/CreateUser.ts',
code: "import { CreateUserCommand } from './CreateUserCommand';",
},
// Non-domain file importing from application
{
filename: '/path/to/core/application/user/CreateUser.ts',
code: "import { UserService } from '../services/UserService';",
},
// Domain file with no imports
{
filename: '/path/to/core/domain/user/User.ts',
code: "export class User {}",
},
// Domain file with multiple imports, none from application
{
filename: '/path/to/core/domain/user/User.ts',
code: `
import { UserId } from './UserId';
import { UserName } from './UserName';
import { ValueObject } from '../shared/ValueObject';
`,
},
],
invalid: [
// Domain file importing from application
{
filename: '/path/to/core/domain/user/User.ts',
code: "import { CreateUserCommand } from '../application/user/CreateUserCommand';",
errors: [
{
messageId: 'forbiddenImport',
data: {
source: '../application/user/CreateUserCommand',
},
},
],
},
// Domain file importing from application with different path
{
filename: '/path/to/core/domain/user/User.ts',
code: "import { UserService } from '../../application/services/UserService';",
errors: [
{
messageId: 'forbiddenImport',
data: {
source: '../../application/services/UserService',
},
},
],
},
// Domain file importing from application with absolute path
{
filename: '/path/to/core/domain/user/User.ts',
code: "import { CreateUserCommand } from 'core/application/user/CreateUserCommand';",
errors: [
{
messageId: 'forbiddenImport',
data: {
source: 'core/application/user/CreateUserCommand',
},
},
],
},
// Domain file with multiple imports, one from application
{
filename: '/path/to/core/domain/user/User.ts',
code: `
import { UserId } from './UserId';
import { CreateUserCommand } from '../application/user/CreateUserCommand';
import { UserName } from './UserName';
`,
errors: [
{
messageId: 'forbiddenImport',
data: {
source: '../application/user/CreateUserCommand',
},
},
],
},
],
});

View File

@@ -1,79 +0,0 @@
const index = require('./index');
describe('eslint-rules index', () => {
describe('rules', () => {
it('should export no-index-files rule', () => {
expect(index.rules['no-index-files']).toBeDefined();
expect(index.rules['no-index-files'].meta).toBeDefined();
expect(index.rules['no-index-files'].create).toBeDefined();
});
it('should export no-framework-imports rule', () => {
expect(index.rules['no-framework-imports']).toBeDefined();
expect(index.rules['no-framework-imports'].meta).toBeDefined();
expect(index.rules['no-framework-imports'].create).toBeDefined();
});
it('should export domain-no-application rule', () => {
expect(index.rules['domain-no-application']).toBeDefined();
expect(index.rules['domain-no-application'].meta).toBeDefined();
expect(index.rules['domain-no-application'].create).toBeDefined();
});
it('should have exactly 3 rules', () => {
expect(Object.keys(index.rules)).toHaveLength(3);
});
});
describe('configs', () => {
it('should export recommended config', () => {
expect(index.configs.recommended).toBeDefined();
});
it('recommended config should have gridpilot-core-rules plugin', () => {
expect(index.configs.recommended.plugins).toContain('gridpilot-core-rules');
});
it('recommended config should enable all rules', () => {
expect(index.configs.recommended.rules['gridpilot-core-rules/no-index-files']).toBe('error');
expect(index.configs.recommended.rules['gridpilot-core-rules/no-framework-imports']).toBe('error');
expect(index.configs.recommended.rules['gridpilot-core-rules/domain-no-application']).toBe('error');
});
it('recommended config should have exactly 3 rules', () => {
expect(Object.keys(index.configs.recommended.rules)).toHaveLength(3);
});
});
describe('rule metadata', () => {
it('no-index-files should have correct metadata', () => {
const rule = index.rules['no-index-files'];
expect(rule.meta.type).toBe('problem');
expect(rule.meta.docs.category).toBe('Best Practices');
expect(rule.meta.docs.recommended).toBe(true);
expect(rule.meta.fixable).toBe(null);
expect(rule.meta.schema).toEqual([]);
expect(rule.meta.messages.indexFile).toBeDefined();
});
it('no-framework-imports should have correct metadata', () => {
const rule = index.rules['no-framework-imports'];
expect(rule.meta.type).toBe('problem');
expect(rule.meta.docs.category).toBe('Architecture');
expect(rule.meta.docs.recommended).toBe(true);
expect(rule.meta.fixable).toBe(null);
expect(rule.meta.schema).toEqual([]);
expect(rule.meta.messages.frameworkImport).toBeDefined();
});
it('domain-no-application should have correct metadata', () => {
const rule = index.rules['domain-no-application'];
expect(rule.meta.type).toBe('problem');
expect(rule.meta.docs.category).toBe('Architecture');
expect(rule.meta.docs.recommended).toBe(true);
expect(rule.meta.fixable).toBe(null);
expect(rule.meta.schema).toEqual([]);
expect(rule.meta.messages.forbiddenImport).toBeDefined();
});
});
});

View File

@@ -1,166 +0,0 @@
const { RuleTester } = require('eslint');
const rule = require('./no-framework-imports');
const ruleTester = new RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
ecmaFeatures: {
jsx: false,
},
},
});
ruleTester.run('no-framework-imports', rule, {
valid: [
// Import from domain
{
filename: '/path/to/core/domain/user/User.ts',
code: "import { UserId } from './UserId';",
},
// Import from application
{
filename: '/path/to/core/application/user/CreateUser.ts',
code: "import { CreateUserCommand } from './CreateUserCommand';",
},
// Import from shared
{
filename: '/path/to/core/shared/ValueObject.ts',
code: "import { ValueObject } from './ValueObject';",
},
// Import from ports
{
filename: '/path/to/core/ports/UserRepository.ts',
code: "import { User } from '../domain/user/User';",
},
// Import from external packages (not frameworks)
{
filename: '/path/to/core/domain/user/User.ts',
code: "import { v4 as uuidv4 } from 'uuid';",
},
// Import from internal packages
{
filename: '/path/to/core/domain/user/User.ts',
code: "import { SomeUtil } from '@core/shared/SomeUtil';",
},
// No imports
{
filename: '/path/to/core/domain/user/User.ts',
code: "export class User {}",
},
// Multiple valid imports
{
filename: '/path/to/core/domain/user/User.ts',
code: `
import { UserId } from './UserId';
import { UserName } from './UserName';
import { ValueObject } from '../shared/ValueObject';
`,
},
],
invalid: [
// Import from @nestjs
{
filename: '/path/to/core/domain/user/User.ts',
code: "import { Injectable } from '@nestjs/common';",
errors: [
{
messageId: 'frameworkImport',
data: {
source: '@nestjs/common',
},
},
],
},
// Import from @nestjs/core
{
filename: '/path/to/core/domain/user/User.ts',
code: "import { Module } from '@nestjs/core';",
errors: [
{
messageId: 'frameworkImport',
data: {
source: '@nestjs/core',
},
},
],
},
// Import from express
{
filename: '/path/to/core/domain/user/User.ts',
code: "import express from 'express';",
errors: [
{
messageId: 'frameworkImport',
data: {
source: 'express',
},
},
],
},
// Import from react
{
filename: '/path/to/core/domain/user/User.ts',
code: "import React from 'react';",
errors: [
{
messageId: 'frameworkImport',
data: {
source: 'react',
},
},
],
},
// Import from next
{
filename: '/path/to/core/domain/user/User.ts',
code: "import { useRouter } from 'next/router';",
errors: [
{
messageId: 'frameworkImport',
data: {
source: 'next/router',
},
},
],
},
// Import from @nestjs with subpath
{
filename: '/path/to/core/domain/user/User.ts',
code: "import { Controller } from '@nestjs/common';",
errors: [
{
messageId: 'frameworkImport',
data: {
source: '@nestjs/common',
},
},
],
},
// Multiple framework imports
{
filename: '/path/to/core/domain/user/User.ts',
code: `
import { Injectable } from '@nestjs/common';
import { UserId } from './UserId';
import React from 'react';
`,
errors: [
{
messageId: 'frameworkImport',
data: {
source: '@nestjs/common',
},
},
{
messageId: 'frameworkImport',
data: {
source: 'react',
},
},
],
},
],
});

View File

@@ -1,131 +0,0 @@
const { RuleTester } = require('eslint');
const rule = require('./no-index-files');
const ruleTester = new RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
ecmaFeatures: {
jsx: false,
},
},
});
ruleTester.run('no-index-files', rule, {
valid: [
// Regular file in domain
{
filename: '/path/to/core/domain/user/User.ts',
code: "export class User {}",
},
// Regular file in application
{
filename: '/path/to/core/application/user/CreateUser.ts',
code: "export class CreateUser {}",
},
// Regular file in shared
{
filename: '/path/to/core/shared/ValueObject.ts',
code: "export class ValueObject {}",
},
// Regular file in ports
{
filename: '/path/to/core/ports/UserRepository.ts',
code: "export interface UserRepository {}",
},
// File with index in the middle of the path
{
filename: '/path/to/core/domain/user/index/User.ts',
code: "export class User {}",
},
// File with index in the name but not at the end
{
filename: '/path/to/core/domain/user/indexHelper.ts',
code: "export class IndexHelper {}",
},
// Root index.ts is allowed
{
filename: '/path/to/core/index.ts',
code: "export * from './domain';",
},
// File with index.ts in the middle of the path
{
filename: '/path/to/core/domain/index/User.ts',
code: "export class User {}",
},
],
invalid: [
// index.ts in domain
{
filename: '/path/to/core/domain/user/index.ts',
code: "export * from './User';",
errors: [
{
messageId: 'indexFile',
},
],
},
// index.ts in application
{
filename: '/path/to/core/application/user/index.ts',
code: "export * from './CreateUser';",
errors: [
{
messageId: 'indexFile',
},
],
},
// index.ts in shared
{
filename: '/path/to/core/shared/index.ts',
code: "export * from './ValueObject';",
errors: [
{
messageId: 'indexFile',
},
],
},
// index.ts in ports
{
filename: '/path/to/core/ports/index.ts',
code: "export * from './UserRepository';",
errors: [
{
messageId: 'indexFile',
},
],
},
// index.ts with Windows path separator
{
filename: 'C:\\path\\to\\core\\domain\\user\\index.ts',
code: "export * from './User';",
errors: [
{
messageId: 'indexFile',
},
],
},
// index.ts at the start of path
{
filename: 'index.ts',
code: "export * from './domain';",
errors: [
{
messageId: 'indexFile',
},
],
},
// index.ts in nested directory
{
filename: '/path/to/core/domain/user/profile/index.ts',
code: "export * from './Profile';",
errors: [
{
messageId: 'indexFile',
},
],
},
],
});

View File

@@ -1,160 +0,0 @@
/**
* Application Query Tests: GetUserRatingLedgerQuery
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { GetUserRatingLedgerQueryHandler } from './GetUserRatingLedgerQuery';
import { RatingEventRepository } from '../../domain/repositories/RatingEventRepository';
// Mock repository
const createMockRepository = () => ({
save: vi.fn(),
findByUserId: vi.fn(),
findByIds: vi.fn(),
getAllByUserId: vi.fn(),
findEventsPaginated: vi.fn(),
});
describe('GetUserRatingLedgerQueryHandler', () => {
let handler: GetUserRatingLedgerQueryHandler;
let mockRepository: ReturnType<typeof createMockRepository>;
beforeEach(() => {
mockRepository = createMockRepository();
handler = new GetUserRatingLedgerQueryHandler(mockRepository as unknown as RatingEventRepository);
vi.clearAllMocks();
});
it('should query repository with default pagination', async () => {
mockRepository.findEventsPaginated.mockResolvedValue({
items: [],
total: 0,
limit: 20,
offset: 0,
hasMore: false,
});
await handler.execute({ userId: 'user-1' });
expect(mockRepository.findEventsPaginated).toHaveBeenCalledWith('user-1', {
limit: 20,
offset: 0,
});
});
it('should query repository with custom pagination', async () => {
mockRepository.findEventsPaginated.mockResolvedValue({
items: [],
total: 0,
limit: 50,
offset: 100,
hasMore: false,
});
await handler.execute({
userId: 'user-1',
limit: 50,
offset: 100,
});
expect(mockRepository.findEventsPaginated).toHaveBeenCalledWith('user-1', {
limit: 50,
offset: 100,
});
});
it('should query repository with filters', async () => {
mockRepository.findEventsPaginated.mockResolvedValue({
items: [],
total: 0,
limit: 20,
offset: 0,
hasMore: false,
});
const filter: any = {
dimensions: ['trust'],
sourceTypes: ['vote'],
from: '2026-01-01T00:00:00Z',
to: '2026-01-31T23:59:59Z',
reasonCodes: ['VOTE_POSITIVE'],
};
await handler.execute({
userId: 'user-1',
filter,
});
expect(mockRepository.findEventsPaginated).toHaveBeenCalledWith('user-1', {
limit: 20,
offset: 0,
filter: {
dimensions: ['trust'],
sourceTypes: ['vote'],
from: new Date('2026-01-01T00:00:00Z'),
to: new Date('2026-01-31T23:59:59Z'),
reasonCodes: ['VOTE_POSITIVE'],
},
});
});
it('should map domain entities to DTOs', async () => {
const mockEvent = {
id: { value: 'event-1' },
userId: 'user-1',
dimension: { value: 'trust' },
delta: { value: 5 },
occurredAt: new Date('2026-01-15T12:00:00Z'),
createdAt: new Date('2026-01-15T12:00:00Z'),
source: 'admin_vote',
reason: 'VOTE_POSITIVE',
visibility: 'public',
weight: 1.0,
};
mockRepository.findEventsPaginated.mockResolvedValue({
items: [mockEvent],
total: 1,
limit: 20,
offset: 0,
hasMore: false,
});
const result = await handler.execute({ userId: 'user-1' });
expect(result.entries).toHaveLength(1);
expect(result.entries[0]).toEqual({
id: 'event-1',
userId: 'user-1',
dimension: 'trust',
delta: 5,
occurredAt: '2026-01-15T12:00:00.000Z',
createdAt: '2026-01-15T12:00:00.000Z',
source: 'admin_vote',
reason: 'VOTE_POSITIVE',
visibility: 'public',
weight: 1.0,
});
});
it('should handle pagination metadata in result', async () => {
mockRepository.findEventsPaginated.mockResolvedValue({
items: [],
total: 100,
limit: 20,
offset: 20,
hasMore: true,
nextOffset: 40,
});
const result = await handler.execute({ userId: 'user-1', limit: 20, offset: 20 });
expect(result.pagination).toEqual({
total: 100,
limit: 20,
offset: 20,
hasMore: true,
nextOffset: 40,
});
});
});

View File

@@ -1,399 +0,0 @@
/**
* Application Use Case Tests: CastAdminVoteUseCase
*
* Tests for casting votes in admin vote sessions
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { CastAdminVoteUseCase } from './CastAdminVoteUseCase';
import { AdminVoteSessionRepository } from '../../domain/repositories/AdminVoteSessionRepository';
import { AdminVoteSession } from '../../domain/entities/AdminVoteSession';
// Mock repository
const createMockRepository = () => ({
save: vi.fn(),
findById: vi.fn(),
findActiveForAdmin: vi.fn(),
findByAdminAndLeague: vi.fn(),
findByLeague: vi.fn(),
findClosedUnprocessed: vi.fn(),
});
describe('CastAdminVoteUseCase', () => {
let useCase: CastAdminVoteUseCase;
let mockRepository: ReturnType<typeof createMockRepository>;
beforeEach(() => {
mockRepository = createMockRepository();
useCase = new CastAdminVoteUseCase(mockRepository);
});
describe('Input validation', () => {
it('should reject when voteSessionId is missing', async () => {
const result = await useCase.execute({
voteSessionId: '',
voterId: 'voter-123',
positive: true,
});
expect(result.success).toBe(false);
expect(result.errors).toContain('voteSessionId is required');
});
it('should reject when voterId is missing', async () => {
const result = await useCase.execute({
voteSessionId: 'session-123',
voterId: '',
positive: true,
});
expect(result.success).toBe(false);
expect(result.errors).toContain('voterId is required');
});
it('should reject when positive is not a boolean', async () => {
const result = await useCase.execute({
voteSessionId: 'session-123',
voterId: 'voter-123',
positive: 'true' as any,
});
expect(result.success).toBe(false);
expect(result.errors).toContain('positive must be a boolean value');
});
it('should reject when votedAt is not a valid date', async () => {
const result = await useCase.execute({
voteSessionId: 'session-123',
voterId: 'voter-123',
positive: true,
votedAt: 'invalid-date',
});
expect(result.success).toBe(false);
expect(result.errors).toContain('votedAt must be a valid date if provided');
});
it('should accept valid input with all fields', async () => {
mockRepository.findById.mockResolvedValue({
id: 'session-123',
isVotingWindowOpen: vi.fn().mockReturnValue(true),
castVote: vi.fn(),
});
const result = await useCase.execute({
voteSessionId: 'session-123',
voterId: 'voter-123',
positive: true,
votedAt: '2024-01-01T00:00:00Z',
});
expect(result.success).toBe(true);
expect(result.errors).toBeUndefined();
});
it('should accept valid input without optional votedAt', async () => {
mockRepository.findById.mockResolvedValue({
id: 'session-123',
isVotingWindowOpen: vi.fn().mockReturnValue(true),
castVote: vi.fn(),
});
const result = await useCase.execute({
voteSessionId: 'session-123',
voterId: 'voter-123',
positive: true,
});
expect(result.success).toBe(true);
expect(result.errors).toBeUndefined();
});
});
describe('Session lookup', () => {
it('should reject when vote session is not found', async () => {
mockRepository.findById.mockResolvedValue(null);
const result = await useCase.execute({
voteSessionId: 'non-existent-session',
voterId: 'voter-123',
positive: true,
});
expect(result.success).toBe(false);
expect(result.errors).toContain('Vote session not found');
});
it('should find session by ID when provided', async () => {
const mockSession = {
id: 'session-123',
isVotingWindowOpen: vi.fn().mockReturnValue(true),
castVote: vi.fn(),
};
mockRepository.findById.mockResolvedValue(mockSession);
await useCase.execute({
voteSessionId: 'session-123',
voterId: 'voter-123',
positive: true,
});
expect(mockRepository.findById).toHaveBeenCalledWith('session-123');
});
});
describe('Voting window validation', () => {
it('should reject when voting window is not open', async () => {
const mockSession = {
id: 'session-123',
isVotingWindowOpen: vi.fn().mockReturnValue(false),
castVote: vi.fn(),
};
mockRepository.findById.mockResolvedValue(mockSession);
const result = await useCase.execute({
voteSessionId: 'session-123',
voterId: 'voter-123',
positive: true,
});
expect(result.success).toBe(false);
expect(result.errors).toContain('Vote session is not open for voting');
expect(mockSession.isVotingWindowOpen).toHaveBeenCalled();
});
it('should accept when voting window is open', async () => {
const mockSession = {
id: 'session-123',
isVotingWindowOpen: vi.fn().mockReturnValue(true),
castVote: vi.fn(),
};
mockRepository.findById.mockResolvedValue(mockSession);
const result = await useCase.execute({
voteSessionId: 'session-123',
voterId: 'voter-123',
positive: true,
});
expect(result.success).toBe(true);
expect(mockSession.isVotingWindowOpen).toHaveBeenCalled();
});
it('should use current time when votedAt is not provided', async () => {
const mockSession = {
id: 'session-123',
isVotingWindowOpen: vi.fn().mockReturnValue(true),
castVote: vi.fn(),
};
mockRepository.findById.mockResolvedValue(mockSession);
await useCase.execute({
voteSessionId: 'session-123',
voterId: 'voter-123',
positive: true,
});
expect(mockSession.isVotingWindowOpen).toHaveBeenCalledWith(expect.any(Date));
});
it('should use provided votedAt when available', async () => {
const mockSession = {
id: 'session-123',
isVotingWindowOpen: vi.fn().mockReturnValue(true),
castVote: vi.fn(),
};
mockRepository.findById.mockResolvedValue(mockSession);
const votedAt = new Date('2024-01-01T12:00:00Z');
await useCase.execute({
voteSessionId: 'session-123',
voterId: 'voter-123',
positive: true,
votedAt: votedAt.toISOString(),
});
expect(mockSession.isVotingWindowOpen).toHaveBeenCalledWith(votedAt);
});
});
describe('Vote casting', () => {
it('should cast positive vote when session is open', async () => {
const mockSession = {
id: 'session-123',
isVotingWindowOpen: vi.fn().mockReturnValue(true),
castVote: vi.fn(),
};
mockRepository.findById.mockResolvedValue(mockSession);
await useCase.execute({
voteSessionId: 'session-123',
voterId: 'voter-123',
positive: true,
});
expect(mockSession.castVote).toHaveBeenCalledWith('voter-123', true, expect.any(Date));
});
it('should cast negative vote when session is open', async () => {
const mockSession = {
id: 'session-123',
isVotingWindowOpen: vi.fn().mockReturnValue(true),
castVote: vi.fn(),
};
mockRepository.findById.mockResolvedValue(mockSession);
await useCase.execute({
voteSessionId: 'session-123',
voterId: 'voter-123',
positive: false,
});
expect(mockSession.castVote).toHaveBeenCalledWith('voter-123', false, expect.any(Date));
});
it('should save updated session after casting vote', async () => {
const mockSession = {
id: 'session-123',
isVotingWindowOpen: vi.fn().mockReturnValue(true),
castVote: vi.fn(),
};
mockRepository.findById.mockResolvedValue(mockSession);
await useCase.execute({
voteSessionId: 'session-123',
voterId: 'voter-123',
positive: true,
});
expect(mockRepository.save).toHaveBeenCalledWith(mockSession);
});
it('should return success when vote is cast', async () => {
const mockSession = {
id: 'session-123',
isVotingWindowOpen: vi.fn().mockReturnValue(true),
castVote: vi.fn(),
};
mockRepository.findById.mockResolvedValue(mockSession);
const result = await useCase.execute({
voteSessionId: 'session-123',
voterId: 'voter-123',
positive: true,
});
expect(result.success).toBe(true);
expect(result.voteSessionId).toBe('session-123');
expect(result.voterId).toBe('voter-123');
expect(result.errors).toBeUndefined();
});
});
describe('Error handling', () => {
it('should handle repository errors gracefully', async () => {
mockRepository.findById.mockRejectedValue(new Error('Database error'));
const result = await useCase.execute({
voteSessionId: 'session-123',
voterId: 'voter-123',
positive: true,
});
expect(result.success).toBe(false);
expect(result.errors).toContain('Failed to cast vote: Database error');
});
it('should handle unexpected errors gracefully', async () => {
mockRepository.findById.mockRejectedValue('Unknown error');
const result = await useCase.execute({
voteSessionId: 'session-123',
voterId: 'voter-123',
positive: true,
});
expect(result.success).toBe(false);
expect(result.errors).toContain('Failed to cast vote: Unknown error');
});
it('should handle save errors gracefully', async () => {
const mockSession = {
id: 'session-123',
isVotingWindowOpen: vi.fn().mockReturnValue(true),
castVote: vi.fn(),
};
mockRepository.findById.mockResolvedValue(mockSession);
mockRepository.save.mockRejectedValue(new Error('Save failed'));
const result = await useCase.execute({
voteSessionId: 'session-123',
voterId: 'voter-123',
positive: true,
});
expect(result.success).toBe(false);
expect(result.errors).toContain('Failed to cast vote: Save failed');
});
});
describe('Return values', () => {
it('should return voteSessionId in success response', async () => {
const mockSession = {
id: 'session-123',
isVotingWindowOpen: vi.fn().mockReturnValue(true),
castVote: vi.fn(),
};
mockRepository.findById.mockResolvedValue(mockSession);
const result = await useCase.execute({
voteSessionId: 'session-123',
voterId: 'voter-123',
positive: true,
});
expect(result.voteSessionId).toBe('session-123');
});
it('should return voterId in success response', async () => {
const mockSession = {
id: 'session-123',
isVotingWindowOpen: vi.fn().mockReturnValue(true),
castVote: vi.fn(),
};
mockRepository.findById.mockResolvedValue(mockSession);
const result = await useCase.execute({
voteSessionId: 'session-123',
voterId: 'voter-123',
positive: true,
});
expect(result.voterId).toBe('voter-123');
});
it('should return voteSessionId in error response', async () => {
mockRepository.findById.mockResolvedValue(null);
const result = await useCase.execute({
voteSessionId: 'session-123',
voterId: 'voter-123',
positive: true,
});
expect(result.voteSessionId).toBe('session-123');
});
it('should return voterId in error response', async () => {
mockRepository.findById.mockResolvedValue(null);
const result = await useCase.execute({
voteSessionId: 'session-123',
voterId: 'voter-123',
positive: true,
});
expect(result.voterId).toBe('voter-123');
});
});
});

View File

@@ -1,251 +0,0 @@
/**
* Application Use Case Tests: OpenAdminVoteSessionUseCase
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { OpenAdminVoteSessionUseCase } from './OpenAdminVoteSessionUseCase';
import { AdminVoteSessionRepository } from '../../domain/repositories/AdminVoteSessionRepository';
import { AdminVoteSession } from '../../domain/entities/AdminVoteSession';
// Mock repository
const createMockRepository = () => ({
save: vi.fn(),
findById: vi.fn(),
findActiveForAdmin: vi.fn(),
findByAdminAndLeague: vi.fn(),
findByLeague: vi.fn(),
findClosedUnprocessed: vi.fn(),
});
describe('OpenAdminVoteSessionUseCase', () => {
let useCase: OpenAdminVoteSessionUseCase;
let mockRepository: ReturnType<typeof createMockRepository>;
beforeEach(() => {
mockRepository = createMockRepository();
useCase = new OpenAdminVoteSessionUseCase(mockRepository as unknown as AdminVoteSessionRepository);
vi.clearAllMocks();
});
describe('Input validation', () => {
it('should reject when voteSessionId is missing', async () => {
const result = await useCase.execute({
voteSessionId: '',
leagueId: 'league-1',
adminId: 'admin-1',
startDate: '2026-01-01',
endDate: '2026-01-07',
eligibleVoters: ['voter-1'],
});
expect(result.success).toBe(false);
expect(result.errors).toContain('voteSessionId is required');
});
it('should reject when leagueId is missing', async () => {
const result = await useCase.execute({
voteSessionId: 'session-1',
leagueId: '',
adminId: 'admin-1',
startDate: '2026-01-01',
endDate: '2026-01-07',
eligibleVoters: ['voter-1'],
});
expect(result.success).toBe(false);
expect(result.errors).toContain('leagueId is required');
});
it('should reject when adminId is missing', async () => {
const result = await useCase.execute({
voteSessionId: 'session-1',
leagueId: 'league-1',
adminId: '',
startDate: '2026-01-01',
endDate: '2026-01-07',
eligibleVoters: ['voter-1'],
});
expect(result.success).toBe(false);
expect(result.errors).toContain('adminId is required');
});
it('should reject when startDate is missing', async () => {
const result = await useCase.execute({
voteSessionId: 'session-1',
leagueId: 'league-1',
adminId: 'admin-1',
startDate: '',
endDate: '2026-01-07',
eligibleVoters: ['voter-1'],
});
expect(result.success).toBe(false);
expect(result.errors).toContain('startDate is required');
});
it('should reject when endDate is missing', async () => {
const result = await useCase.execute({
voteSessionId: 'session-1',
leagueId: 'league-1',
adminId: 'admin-1',
startDate: '2026-01-01',
endDate: '',
eligibleVoters: ['voter-1'],
});
expect(result.success).toBe(false);
expect(result.errors).toContain('endDate is required');
});
it('should reject when startDate is invalid', async () => {
const result = await useCase.execute({
voteSessionId: 'session-1',
leagueId: 'league-1',
adminId: 'admin-1',
startDate: 'invalid-date',
endDate: '2026-01-07',
eligibleVoters: ['voter-1'],
});
expect(result.success).toBe(false);
expect(result.errors).toContain('startDate must be a valid date');
});
it('should reject when endDate is invalid', async () => {
const result = await useCase.execute({
voteSessionId: 'session-1',
leagueId: 'league-1',
adminId: 'admin-1',
startDate: '2026-01-01',
endDate: 'invalid-date',
eligibleVoters: ['voter-1'],
});
expect(result.success).toBe(false);
expect(result.errors).toContain('endDate must be a valid date');
});
it('should reject when startDate is after endDate', async () => {
const result = await useCase.execute({
voteSessionId: 'session-1',
leagueId: 'league-1',
adminId: 'admin-1',
startDate: '2026-01-07',
endDate: '2026-01-01',
eligibleVoters: ['voter-1'],
});
expect(result.success).toBe(false);
expect(result.errors).toContain('startDate must be before endDate');
});
it('should reject when eligibleVoters is empty', async () => {
const result = await useCase.execute({
voteSessionId: 'session-1',
leagueId: 'league-1',
adminId: 'admin-1',
startDate: '2026-01-01',
endDate: '2026-01-07',
eligibleVoters: [],
});
expect(result.success).toBe(false);
expect(result.errors).toContain('At least one eligible voter is required');
});
it('should reject when eligibleVoters has duplicates', async () => {
const result = await useCase.execute({
voteSessionId: 'session-1',
leagueId: 'league-1',
adminId: 'admin-1',
startDate: '2026-01-01',
endDate: '2026-01-07',
eligibleVoters: ['voter-1', 'voter-1'],
});
expect(result.success).toBe(false);
expect(result.errors).toContain('Duplicate eligible voters are not allowed');
});
});
describe('Business rules', () => {
it('should reject when session ID already exists', async () => {
mockRepository.findById.mockResolvedValue({ id: 'session-1' } as any);
const result = await useCase.execute({
voteSessionId: 'session-1',
leagueId: 'league-1',
adminId: 'admin-1',
startDate: '2026-01-01',
endDate: '2026-01-07',
eligibleVoters: ['voter-1'],
});
expect(result.success).toBe(false);
expect(result.errors).toContain('Vote session with this ID already exists');
});
it('should reject when there is an overlapping active session', async () => {
mockRepository.findById.mockResolvedValue(null);
mockRepository.findActiveForAdmin.mockResolvedValue([
{
startDate: new Date('2026-01-05'),
endDate: new Date('2026-01-10'),
}
] as any);
const result = await useCase.execute({
voteSessionId: 'session-1',
leagueId: 'league-1',
adminId: 'admin-1',
startDate: '2026-01-01',
endDate: '2026-01-07',
eligibleVoters: ['voter-1'],
});
expect(result.success).toBe(false);
expect(result.errors).toContain('Active vote session already exists for this admin in this league with overlapping dates');
});
it('should create and save a new session when valid', async () => {
mockRepository.findById.mockResolvedValue(null);
mockRepository.findActiveForAdmin.mockResolvedValue([]);
const result = await useCase.execute({
voteSessionId: 'session-1',
leagueId: 'league-1',
adminId: 'admin-1',
startDate: '2026-01-01',
endDate: '2026-01-07',
eligibleVoters: ['voter-1', 'voter-2'],
});
expect(result.success).toBe(true);
expect(mockRepository.save).toHaveBeenCalled();
const savedSession = mockRepository.save.mock.calls[0][0];
expect(savedSession).toBeInstanceOf(AdminVoteSession);
expect(savedSession.id).toBe('session-1');
expect(savedSession.leagueId).toBe('league-1');
expect(savedSession.adminId).toBe('admin-1');
});
});
describe('Error handling', () => {
it('should handle repository errors gracefully', async () => {
mockRepository.findById.mockRejectedValue(new Error('Database error'));
const result = await useCase.execute({
voteSessionId: 'session-1',
leagueId: 'league-1',
adminId: 'admin-1',
startDate: '2026-01-01',
endDate: '2026-01-07',
eligibleVoters: ['voter-1'],
});
expect(result.success).toBe(false);
expect(result.errors?.[0]).toContain('Failed to open vote session: Database error');
});
});
});

View File

@@ -1,241 +0,0 @@
/**
* Domain Entity Tests: Company
*
* Tests for Company entity business rules and invariants
*/
import { describe, it, expect } from 'vitest';
import { Company } from './Company';
import { UserId } from '../value-objects/UserId';
describe('Company', () => {
describe('Creation', () => {
it('should create a company with valid properties', () => {
const userId = UserId.fromString('user-123');
const company = Company.create({
name: 'Acme Racing Team',
ownerUserId: userId,
contactEmail: 'contact@acme.com',
});
expect(company.getName()).toBe('Acme Racing Team');
expect(company.getOwnerUserId()).toEqual(userId);
expect(company.getContactEmail()).toBe('contact@acme.com');
expect(company.getId()).toBeDefined();
expect(company.getCreatedAt()).toBeInstanceOf(Date);
});
it('should create a company without optional contact email', () => {
const userId = UserId.fromString('user-123');
const company = Company.create({
name: 'Acme Racing Team',
ownerUserId: userId,
});
expect(company.getContactEmail()).toBeUndefined();
});
it('should generate unique IDs for different companies', () => {
const userId = UserId.fromString('user-123');
const company1 = Company.create({
name: 'Team A',
ownerUserId: userId,
});
const company2 = Company.create({
name: 'Team B',
ownerUserId: userId,
});
expect(company1.getId()).not.toBe(company2.getId());
});
});
describe('Rehydration', () => {
it('should rehydrate company from stored data', () => {
const userId = UserId.fromString('user-123');
const createdAt = new Date('2024-01-01');
const company = Company.rehydrate({
id: 'comp-123',
name: 'Acme Racing Team',
ownerUserId: 'user-123',
contactEmail: 'contact@acme.com',
createdAt,
});
expect(company.getId()).toBe('comp-123');
expect(company.getName()).toBe('Acme Racing Team');
expect(company.getOwnerUserId()).toEqual(userId);
expect(company.getContactEmail()).toBe('contact@acme.com');
expect(company.getCreatedAt()).toEqual(createdAt);
});
it('should rehydrate company without contact email', () => {
const createdAt = new Date('2024-01-01');
const company = Company.rehydrate({
id: 'comp-123',
name: 'Acme Racing Team',
ownerUserId: 'user-123',
createdAt,
});
expect(company.getContactEmail()).toBeUndefined();
});
});
describe('Validation', () => {
it('should throw error when company name is empty', () => {
const userId = UserId.fromString('user-123');
expect(() => {
Company.create({
name: '',
ownerUserId: userId,
});
}).toThrow('Company name cannot be empty');
});
it('should throw error when company name is only whitespace', () => {
const userId = UserId.fromString('user-123');
expect(() => {
Company.create({
name: ' ',
ownerUserId: userId,
});
}).toThrow('Company name cannot be empty');
});
it('should throw error when company name is too short', () => {
const userId = UserId.fromString('user-123');
expect(() => {
Company.create({
name: 'A',
ownerUserId: userId,
});
}).toThrow('Company name must be at least 2 characters long');
});
it('should throw error when company name is too long', () => {
const userId = UserId.fromString('user-123');
const longName = 'A'.repeat(101);
expect(() => {
Company.create({
name: longName,
ownerUserId: userId,
});
}).toThrow('Company name must be no more than 100 characters');
});
it('should accept company name with exactly 2 characters', () => {
const userId = UserId.fromString('user-123');
const company = Company.create({
name: 'AB',
ownerUserId: userId,
});
expect(company.getName()).toBe('AB');
});
it('should accept company name with exactly 100 characters', () => {
const userId = UserId.fromString('user-123');
const longName = 'A'.repeat(100);
const company = Company.create({
name: longName,
ownerUserId: userId,
});
expect(company.getName()).toBe(longName);
});
it('should trim whitespace from company name during validation', () => {
const userId = UserId.fromString('user-123');
const company = Company.create({
name: ' Acme Racing Team ',
ownerUserId: userId,
});
// Note: The current implementation doesn't trim, it just validates
// So this test documents the current behavior
expect(company.getName()).toBe(' Acme Racing Team ');
});
});
describe('Business Rules', () => {
it('should maintain immutability of properties', () => {
const userId = UserId.fromString('user-123');
const company = Company.create({
name: 'Acme Racing Team',
ownerUserId: userId,
contactEmail: 'contact@acme.com',
});
const originalName = company.getName();
const originalEmail = company.getContactEmail();
// Try to modify (should not work due to readonly properties)
// This is more of a TypeScript compile-time check, but we can verify runtime behavior
expect(company.getName()).toBe(originalName);
expect(company.getContactEmail()).toBe(originalEmail);
});
it('should handle special characters in company name', () => {
const userId = UserId.fromString('user-123');
const company = Company.create({
name: 'Acme & Sons Racing, LLC',
ownerUserId: userId,
});
expect(company.getName()).toBe('Acme & Sons Racing, LLC');
});
it('should handle unicode characters in company name', () => {
const userId = UserId.fromString('user-123');
const company = Company.create({
name: 'Räcing Tëam Ñumber Øne',
ownerUserId: userId,
});
expect(company.getName()).toBe('Räcing Tëam Ñumber Øne');
});
});
describe('Edge Cases', () => {
it('should handle rehydration with null contact email', () => {
const createdAt = new Date('2024-01-01');
const company = Company.rehydrate({
id: 'comp-123',
name: 'Acme Racing Team',
ownerUserId: 'user-123',
contactEmail: null as any,
createdAt,
});
// The entity stores null as null, not undefined
expect(company.getContactEmail()).toBeNull();
});
it('should handle rehydration with undefined contact email', () => {
const createdAt = new Date('2024-01-01');
const company = Company.rehydrate({
id: 'comp-123',
name: 'Acme Racing Team',
ownerUserId: 'user-123',
contactEmail: undefined,
createdAt,
});
expect(company.getContactEmail()).toBeUndefined();
});
});
});

View File

@@ -1,221 +0,0 @@
/**
* Domain Error Tests: IdentityDomainError
*
* Tests for domain error classes and their behavior
*/
import { describe, it, expect } from 'vitest';
import { IdentityDomainError, IdentityDomainValidationError, IdentityDomainInvariantError } from './IdentityDomainError';
describe('IdentityDomainError', () => {
describe('IdentityDomainError (base class)', () => {
it('should create an error with correct properties', () => {
const error = new IdentityDomainValidationError('Test error message');
expect(error.message).toBe('Test error message');
expect(error.type).toBe('domain');
expect(error.context).toBe('identity-domain');
expect(error.kind).toBe('validation');
});
it('should be an instance of Error', () => {
const error = new IdentityDomainValidationError('Test error');
expect(error instanceof Error).toBe(true);
});
it('should be an instance of IdentityDomainError', () => {
const error = new IdentityDomainValidationError('Test error');
expect(error instanceof IdentityDomainError).toBe(true);
});
it('should have correct stack trace', () => {
const error = new IdentityDomainValidationError('Test error');
expect(error.stack).toBeDefined();
expect(error.stack).toContain('IdentityDomainError');
});
it('should handle empty error message', () => {
const error = new IdentityDomainValidationError('');
expect(error.message).toBe('');
});
it('should handle error message with special characters', () => {
const error = new IdentityDomainValidationError('Error: Invalid input @#$%^&*()');
expect(error.message).toBe('Error: Invalid input @#$%^&*()');
});
it('should handle error message with newlines', () => {
const error = new IdentityDomainValidationError('Error line 1\nError line 2');
expect(error.message).toBe('Error line 1\nError line 2');
});
});
describe('IdentityDomainValidationError', () => {
it('should create a validation error with correct kind', () => {
const error = new IdentityDomainValidationError('Invalid email format');
expect(error.kind).toBe('validation');
expect(error.type).toBe('domain');
expect(error.context).toBe('identity-domain');
});
it('should be an instance of IdentityDomainValidationError', () => {
const error = new IdentityDomainValidationError('Invalid email format');
expect(error instanceof IdentityDomainValidationError).toBe(true);
});
it('should be an instance of IdentityDomainError', () => {
const error = new IdentityDomainValidationError('Invalid email format');
expect(error instanceof IdentityDomainError).toBe(true);
});
it('should handle validation error with empty message', () => {
const error = new IdentityDomainValidationError('');
expect(error.kind).toBe('validation');
expect(error.message).toBe('');
});
it('should handle validation error with complex message', () => {
const error = new IdentityDomainValidationError(
'Validation failed: Email must be at least 6 characters long and contain a valid domain'
);
expect(error.kind).toBe('validation');
expect(error.message).toBe(
'Validation failed: Email must be at least 6 characters long and contain a valid domain'
);
});
});
describe('IdentityDomainInvariantError', () => {
it('should create an invariant error with correct kind', () => {
const error = new IdentityDomainInvariantError('User must have a valid email');
expect(error.kind).toBe('invariant');
expect(error.type).toBe('domain');
expect(error.context).toBe('identity-domain');
});
it('should be an instance of IdentityDomainInvariantError', () => {
const error = new IdentityDomainInvariantError('User must have a valid email');
expect(error instanceof IdentityDomainInvariantError).toBe(true);
});
it('should be an instance of IdentityDomainError', () => {
const error = new IdentityDomainInvariantError('User must have a valid email');
expect(error instanceof IdentityDomainError).toBe(true);
});
it('should handle invariant error with empty message', () => {
const error = new IdentityDomainInvariantError('');
expect(error.kind).toBe('invariant');
expect(error.message).toBe('');
});
it('should handle invariant error with complex message', () => {
const error = new IdentityDomainInvariantError(
'Invariant violation: User rating must be between 0 and 100'
);
expect(error.kind).toBe('invariant');
expect(error.message).toBe(
'Invariant violation: User rating must be between 0 and 100'
);
});
});
describe('Error hierarchy', () => {
it('should maintain correct error hierarchy for validation errors', () => {
const error = new IdentityDomainValidationError('Test');
expect(error instanceof IdentityDomainValidationError).toBe(true);
expect(error instanceof IdentityDomainError).toBe(true);
expect(error instanceof Error).toBe(true);
});
it('should maintain correct error hierarchy for invariant errors', () => {
const error = new IdentityDomainInvariantError('Test');
expect(error instanceof IdentityDomainInvariantError).toBe(true);
expect(error instanceof IdentityDomainError).toBe(true);
expect(error instanceof Error).toBe(true);
});
it('should allow catching as IdentityDomainError', () => {
const error = new IdentityDomainValidationError('Test');
try {
throw error;
} catch (e) {
expect(e instanceof IdentityDomainError).toBe(true);
expect((e as IdentityDomainError).kind).toBe('validation');
}
});
it('should allow catching as Error', () => {
const error = new IdentityDomainInvariantError('Test');
try {
throw error;
} catch (e) {
expect(e instanceof Error).toBe(true);
expect((e as Error).message).toBe('Test');
}
});
});
describe('Error properties', () => {
it('should have consistent type property', () => {
const validationError = new IdentityDomainValidationError('Test');
const invariantError = new IdentityDomainInvariantError('Test');
expect(validationError.type).toBe('domain');
expect(invariantError.type).toBe('domain');
});
it('should have consistent context property', () => {
const validationError = new IdentityDomainValidationError('Test');
const invariantError = new IdentityDomainInvariantError('Test');
expect(validationError.context).toBe('identity-domain');
expect(invariantError.context).toBe('identity-domain');
});
it('should have different kind properties', () => {
const validationError = new IdentityDomainValidationError('Test');
const invariantError = new IdentityDomainInvariantError('Test');
expect(validationError.kind).toBe('validation');
expect(invariantError.kind).toBe('invariant');
});
});
describe('Error usage patterns', () => {
it('should be usable in try-catch blocks', () => {
expect(() => {
throw new IdentityDomainValidationError('Invalid input');
}).toThrow(IdentityDomainValidationError);
});
it('should be usable with error instanceof checks', () => {
const error = new IdentityDomainValidationError('Test');
expect(error instanceof IdentityDomainValidationError).toBe(true);
expect(error instanceof IdentityDomainError).toBe(true);
expect(error instanceof Error).toBe(true);
});
it('should be usable with error type narrowing', () => {
const error: IdentityDomainError = new IdentityDomainValidationError('Test');
if (error.kind === 'validation') {
expect(error instanceof IdentityDomainValidationError).toBe(true);
}
});
it('should support error message extraction', () => {
const errorMessage = 'User email is required';
const error = new IdentityDomainValidationError(errorMessage);
expect(error.message).toBe(errorMessage);
});
});
});

View File

@@ -1,216 +0,0 @@
/**
* Domain Service Tests: PasswordHashingService
*
* Tests for password hashing and verification business logic
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { PasswordHashingService } from './PasswordHashingService';
describe('PasswordHashingService', () => {
let service: PasswordHashingService;
beforeEach(() => {
service = new PasswordHashingService();
});
describe('hash', () => {
it('should hash a plain text password', async () => {
const plainPassword = 'mySecurePassword123';
const hash = await service.hash(plainPassword);
expect(hash).toBeDefined();
expect(typeof hash).toBe('string');
expect(hash.length).toBeGreaterThan(0);
// Hash should not be the same as the plain password
expect(hash).not.toBe(plainPassword);
});
it('should produce different hashes for the same password (with salt)', async () => {
const plainPassword = 'mySecurePassword123';
const hash1 = await service.hash(plainPassword);
const hash2 = await service.hash(plainPassword);
// Due to salting, hashes should be different
expect(hash1).not.toBe(hash2);
});
it('should handle empty string password', async () => {
const hash = await service.hash('');
expect(hash).toBeDefined();
expect(typeof hash).toBe('string');
});
it('should handle special characters in password', async () => {
const specialPassword = 'P@ssw0rd!#$%^&*()_+-=[]{}|;:,.<>?';
const hash = await service.hash(specialPassword);
expect(hash).toBeDefined();
expect(typeof hash).toBe('string');
});
it('should handle unicode characters in password', async () => {
const unicodePassword = 'Pässwörd!🔒';
const hash = await service.hash(unicodePassword);
expect(hash).toBeDefined();
expect(typeof hash).toBe('string');
});
it('should handle very long passwords', async () => {
const longPassword = 'a'.repeat(1000);
const hash = await service.hash(longPassword);
expect(hash).toBeDefined();
expect(typeof hash).toBe('string');
});
it('should handle whitespace-only password', async () => {
const whitespacePassword = ' ';
const hash = await service.hash(whitespacePassword);
expect(hash).toBeDefined();
expect(typeof hash).toBe('string');
});
});
describe('verify', () => {
it('should verify correct password against hash', async () => {
const plainPassword = 'mySecurePassword123';
const hash = await service.hash(plainPassword);
const isValid = await service.verify(plainPassword, hash);
expect(isValid).toBe(true);
});
it('should reject incorrect password', async () => {
const plainPassword = 'mySecurePassword123';
const hash = await service.hash(plainPassword);
const isValid = await service.verify('wrongPassword', hash);
expect(isValid).toBe(false);
});
it('should reject empty password against hash', async () => {
const plainPassword = 'mySecurePassword123';
const hash = await service.hash(plainPassword);
const isValid = await service.verify('', hash);
expect(isValid).toBe(false);
});
it('should handle verification with special characters', async () => {
const specialPassword = 'P@ssw0rd!#$%^&*()_+-=[]{}|;:,.<>?';
const hash = await service.hash(specialPassword);
const isValid = await service.verify(specialPassword, hash);
expect(isValid).toBe(true);
});
it('should handle verification with unicode characters', async () => {
const unicodePassword = 'Pässwörd!🔒';
const hash = await service.hash(unicodePassword);
const isValid = await service.verify(unicodePassword, hash);
expect(isValid).toBe(true);
});
it('should handle verification with very long passwords', async () => {
const longPassword = 'a'.repeat(1000);
const hash = await service.hash(longPassword);
const isValid = await service.verify(longPassword, hash);
expect(isValid).toBe(true);
});
it('should handle verification with whitespace-only password', async () => {
const whitespacePassword = ' ';
const hash = await service.hash(whitespacePassword);
const isValid = await service.verify(whitespacePassword, hash);
expect(isValid).toBe(true);
});
it('should reject verification with null hash', async () => {
// bcrypt throws an error when hash is null, which is expected behavior
await expect(service.verify('password', null as any)).rejects.toThrow();
});
it('should reject verification with empty hash', async () => {
const isValid = await service.verify('password', '');
expect(isValid).toBe(false);
});
it('should reject verification with invalid hash format', async () => {
const isValid = await service.verify('password', 'invalid-hash-format');
expect(isValid).toBe(false);
});
});
describe('Hash Consistency', () => {
it('should consistently verify the same password-hash pair', async () => {
const plainPassword = 'testPassword123';
const hash = await service.hash(plainPassword);
// Verify multiple times
const result1 = await service.verify(plainPassword, hash);
const result2 = await service.verify(plainPassword, hash);
const result3 = await service.verify(plainPassword, hash);
expect(result1).toBe(true);
expect(result2).toBe(true);
expect(result3).toBe(true);
}, 10000);
it('should consistently reject wrong password', async () => {
const plainPassword = 'testPassword123';
const wrongPassword = 'wrongPassword';
const hash = await service.hash(plainPassword);
// Verify multiple times with wrong password
const result1 = await service.verify(wrongPassword, hash);
const result2 = await service.verify(wrongPassword, hash);
const result3 = await service.verify(wrongPassword, hash);
expect(result1).toBe(false);
expect(result2).toBe(false);
expect(result3).toBe(false);
}, 10000);
});
describe('Security Properties', () => {
it('should not leak information about the original password from hash', async () => {
const password1 = 'password123';
const password2 = 'password456';
const hash1 = await service.hash(password1);
const hash2 = await service.hash(password2);
// Hashes should be different
expect(hash1).not.toBe(hash2);
// Neither hash should contain the original password
expect(hash1).not.toContain(password1);
expect(hash2).not.toContain(password2);
});
it('should handle case sensitivity correctly', async () => {
const password1 = 'Password';
const password2 = 'password';
const hash1 = await service.hash(password1);
const hash2 = await service.hash(password2);
// Should be treated as different passwords
const isValid1 = await service.verify(password1, hash1);
const isValid2 = await service.verify(password2, hash2);
const isCrossValid1 = await service.verify(password1, hash2);
const isCrossValid2 = await service.verify(password2, hash1);
expect(isValid1).toBe(true);
expect(isValid2).toBe(true);
expect(isCrossValid1).toBe(false);
expect(isCrossValid2).toBe(false);
}, 10000);
});
});

View File

@@ -1,338 +0,0 @@
/**
* Domain Types Tests: EmailAddress
*
* Tests for email validation and disposable email detection
*/
import { describe, it, expect } from 'vitest';
import { validateEmail, isDisposableEmail, DISPOSABLE_DOMAINS } from './EmailAddress';
describe('EmailAddress', () => {
describe('validateEmail', () => {
describe('Valid emails', () => {
it('should validate standard email format', () => {
const result = validateEmail('user@example.com');
expect(result.success).toBe(true);
if (result.success) {
expect(result.email).toBe('user@example.com');
}
});
it('should validate email with subdomain', () => {
const result = validateEmail('user@mail.example.com');
expect(result.success).toBe(true);
if (result.success) {
expect(result.email).toBe('user@mail.example.com');
}
});
it('should validate email with plus sign', () => {
const result = validateEmail('user+tag@example.com');
expect(result.success).toBe(true);
if (result.success) {
expect(result.email).toBe('user+tag@example.com');
}
});
it('should validate email with numbers', () => {
const result = validateEmail('user123@example.com');
expect(result.success).toBe(true);
if (result.success) {
expect(result.email).toBe('user123@example.com');
}
});
it('should validate email with hyphens', () => {
const result = validateEmail('user-name@example.com');
expect(result.success).toBe(true);
if (result.success) {
expect(result.email).toBe('user-name@example.com');
}
});
it('should validate email with underscores', () => {
const result = validateEmail('user_name@example.com');
expect(result.success).toBe(true);
if (result.success) {
expect(result.email).toBe('user_name@example.com');
}
});
it('should validate email with dots in local part', () => {
const result = validateEmail('user.name@example.com');
expect(result.success).toBe(true);
if (result.success) {
expect(result.email).toBe('user.name@example.com');
}
});
it('should validate email with uppercase letters', () => {
const result = validateEmail('User@Example.com');
expect(result.success).toBe(true);
if (result.success) {
// Should be normalized to lowercase
expect(result.email).toBe('user@example.com');
}
});
it('should validate email with leading/trailing whitespace', () => {
const result = validateEmail(' user@example.com ');
expect(result.success).toBe(true);
if (result.success) {
// Should be trimmed
expect(result.email).toBe('user@example.com');
}
});
it('should validate minimum length email (6 chars)', () => {
const result = validateEmail('a@b.cd');
expect(result.success).toBe(true);
if (result.success) {
expect(result.email).toBe('a@b.cd');
}
});
it('should validate maximum length email (254 chars)', () => {
const localPart = 'a'.repeat(64);
const domain = 'example.com';
const email = `${localPart}@${domain}`;
const result = validateEmail(email);
expect(result.success).toBe(true);
if (result.success) {
expect(result.email).toBe(email);
}
});
});
describe('Invalid emails', () => {
it('should reject empty string', () => {
const result = validateEmail('');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBeDefined();
}
});
it('should reject whitespace-only string', () => {
const result = validateEmail(' ');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBeDefined();
}
});
it('should reject email without @ symbol', () => {
const result = validateEmail('userexample.com');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBeDefined();
}
});
it('should reject email without domain', () => {
const result = validateEmail('user@');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBeDefined();
}
});
it('should reject email without local part', () => {
const result = validateEmail('@example.com');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBeDefined();
}
});
it('should reject email with multiple @ symbols', () => {
const result = validateEmail('user@domain@com');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBeDefined();
}
});
it('should reject email with spaces in local part', () => {
const result = validateEmail('user name@example.com');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBeDefined();
}
});
it('should reject email with spaces in domain', () => {
const result = validateEmail('user@ex ample.com');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBeDefined();
}
});
it('should reject email with invalid characters', () => {
const result = validateEmail('user#name@example.com');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBeDefined();
}
});
it('should reject email that is too short', () => {
const result = validateEmail('a@b.c');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBeDefined();
}
});
it('should accept email that is exactly 254 characters', () => {
// The maximum email length is 254 characters
const localPart = 'a'.repeat(64);
const domain = 'example.com';
const email = `${localPart}@${domain}`;
const result = validateEmail(email);
expect(result.success).toBe(true);
if (result.success) {
expect(result.email).toBe(email);
}
});
it('should reject email without TLD', () => {
const result = validateEmail('user@example');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBeDefined();
}
});
it('should reject email with invalid TLD format', () => {
const result = validateEmail('user@example.');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBeDefined();
}
});
});
describe('Edge cases', () => {
it('should handle null input gracefully', () => {
const result = validateEmail(null as any);
expect(result.success).toBe(false);
});
it('should handle undefined input gracefully', () => {
const result = validateEmail(undefined as any);
expect(result.success).toBe(false);
});
it('should handle non-string input gracefully', () => {
const result = validateEmail(123 as any);
expect(result.success).toBe(false);
});
});
});
describe('isDisposableEmail', () => {
describe('Disposable email domains', () => {
it('should detect tempmail.com as disposable', () => {
expect(isDisposableEmail('user@tempmail.com')).toBe(true);
});
it('should detect throwaway.email as disposable', () => {
expect(isDisposableEmail('user@throwaway.email')).toBe(true);
});
it('should detect guerrillamail.com as disposable', () => {
expect(isDisposableEmail('user@guerrillamail.com')).toBe(true);
});
it('should detect mailinator.com as disposable', () => {
expect(isDisposableEmail('user@mailinator.com')).toBe(true);
});
it('should detect 10minutemail.com as disposable', () => {
expect(isDisposableEmail('user@10minutemail.com')).toBe(true);
});
it('should detect disposable domains case-insensitively', () => {
expect(isDisposableEmail('user@TEMPMAIL.COM')).toBe(true);
expect(isDisposableEmail('user@TempMail.Com')).toBe(true);
});
it('should detect disposable domains with subdomains', () => {
// The current implementation only checks the exact domain, not subdomains
// So this test documents the current behavior
expect(isDisposableEmail('user@subdomain.tempmail.com')).toBe(false);
});
});
describe('Non-disposable email domains', () => {
it('should not detect gmail.com as disposable', () => {
expect(isDisposableEmail('user@gmail.com')).toBe(false);
});
it('should not detect yahoo.com as disposable', () => {
expect(isDisposableEmail('user@yahoo.com')).toBe(false);
});
it('should not detect outlook.com as disposable', () => {
expect(isDisposableEmail('user@outlook.com')).toBe(false);
});
it('should not detect company domains as disposable', () => {
expect(isDisposableEmail('user@example.com')).toBe(false);
expect(isDisposableEmail('user@company.com')).toBe(false);
});
it('should not detect custom domains as disposable', () => {
expect(isDisposableEmail('user@mydomain.com')).toBe(false);
});
});
describe('Edge cases', () => {
it('should handle email without domain', () => {
expect(isDisposableEmail('user@')).toBe(false);
});
it('should handle email without @ symbol', () => {
expect(isDisposableEmail('user')).toBe(false);
});
it('should handle empty string', () => {
expect(isDisposableEmail('')).toBe(false);
});
it('should handle null input', () => {
// The current implementation throws an error when given null
// This is expected behavior - the function expects a string
expect(() => isDisposableEmail(null as any)).toThrow();
});
it('should handle undefined input', () => {
// The current implementation throws an error when given undefined
// This is expected behavior - the function expects a string
expect(() => isDisposableEmail(undefined as any)).toThrow();
});
});
});
describe('DISPOSABLE_DOMAINS', () => {
it('should contain expected disposable domains', () => {
expect(DISPOSABLE_DOMAINS.has('tempmail.com')).toBe(true);
expect(DISPOSABLE_DOMAINS.has('throwaway.email')).toBe(true);
expect(DISPOSABLE_DOMAINS.has('guerrillamail.com')).toBe(true);
expect(DISPOSABLE_DOMAINS.has('mailinator.com')).toBe(true);
expect(DISPOSABLE_DOMAINS.has('10minutemail.com')).toBe(true);
});
it('should not contain non-disposable domains', () => {
expect(DISPOSABLE_DOMAINS.has('gmail.com')).toBe(false);
expect(DISPOSABLE_DOMAINS.has('yahoo.com')).toBe(false);
expect(DISPOSABLE_DOMAINS.has('outlook.com')).toBe(false);
});
it('should be a Set', () => {
expect(DISPOSABLE_DOMAINS instanceof Set).toBe(true);
});
});
});

Some files were not shown because too many files have changed in this diff Show More