Files
gridpilot.gg/core/shared/domain/Service.test.ts
Marc Mintel 093eece3d7
Some checks failed
Contract Testing / contract-tests (pull_request) Failing after 5m47s
Contract Testing / contract-snapshot (pull_request) Has been skipped
core tests
2026-01-22 18:20:33 +01:00

375 lines
11 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import {
DomainService,
DomainCalculationService,
ResultDomainCalculationService,
DomainValidationService,
DomainFactoryService,
DomainServiceAlias,
DomainCalculationServiceAlias,
ResultDomainCalculationServiceAlias,
DomainValidationServiceAlias,
DomainFactoryServiceAlias
} from './Service';
import { Result } from './Result';
describe('Service', () => {
describe('DomainService interface', () => {
it('should have optional serviceName property', () => {
const service: DomainService = {
serviceName: 'TestService'
};
expect(service.serviceName).toBe('TestService');
});
it('should work without serviceName', () => {
const service: DomainService = {};
expect(service.serviceName).toBeUndefined();
});
it('should support different service implementations', () => {
const service1: DomainService = { serviceName: 'Service1' };
const service2: DomainService = { serviceName: 'Service2' };
const service3: DomainService = {};
expect(service1.serviceName).toBe('Service1');
expect(service2.serviceName).toBe('Service2');
expect(service3.serviceName).toBeUndefined();
});
});
describe('DomainCalculationService interface', () => {
it('should have calculate method', () => {
const service: DomainCalculationService<number, number> = {
calculate: (input: number) => input * 2
};
expect(service.calculate(5)).toBe(10);
});
it('should support different input and output types', () => {
const stringService: DomainCalculationService<string, string> = {
calculate: (input: string) => input.toUpperCase()
};
const objectService: DomainCalculationService<{ x: number; y: number }, number> = {
calculate: (input) => input.x + input.y
};
expect(stringService.calculate('hello')).toBe('HELLO');
expect(objectService.calculate({ x: 3, y: 4 })).toBe(7);
});
it('should support complex calculations', () => {
interface CalculationInput {
values: number[];
operation: 'sum' | 'average' | 'max';
}
const calculator: DomainCalculationService<CalculationInput, number> = {
calculate: (input) => {
switch (input.operation) {
case 'sum':
return input.values.reduce((a, b) => a + b, 0);
case 'average':
return input.values.reduce((a, b) => a + b, 0) / input.values.length;
case 'max':
return Math.max(...input.values);
}
}
};
expect(calculator.calculate({ values: [1, 2, 3], operation: 'sum' })).toBe(6);
expect(calculator.calculate({ values: [1, 2, 3], operation: 'average' })).toBe(2);
expect(calculator.calculate({ values: [1, 2, 3], operation: 'max' })).toBe(3);
});
});
describe('ResultDomainCalculationService interface', () => {
it('should have calculate method returning Result', () => {
const service: ResultDomainCalculationService<number, number, string> = {
calculate: (input: number) => {
if (input < 0) {
return Result.err('Input must be non-negative');
}
return Result.ok(input * 2);
}
};
const successResult = service.calculate(5);
expect(successResult.isOk()).toBe(true);
expect(successResult.unwrap()).toBe(10);
const errorResult = service.calculate(-1);
expect(errorResult.isErr()).toBe(true);
expect(errorResult.unwrapErr()).toBe('Input must be non-negative');
});
it('should support validation logic', () => {
interface ValidationResult {
isValid: boolean;
errors: string[];
}
const validator: ResultDomainCalculationService<string, ValidationResult, string> = {
calculate: (input: string) => {
const errors: string[] = [];
if (input.length < 3) {
errors.push('Must be at least 3 characters');
}
if (!input.match(/^[a-zA-Z]+$/)) {
errors.push('Must contain only letters');
}
if (errors.length > 0) {
return Result.err(errors.join(', '));
}
return Result.ok({ isValid: true, errors: [] });
}
};
const validResult = validator.calculate('Hello');
expect(validResult.isOk()).toBe(true);
expect(validResult.unwrap()).toEqual({ isValid: true, errors: [] });
const invalidResult = validator.calculate('ab');
expect(invalidResult.isErr()).toBe(true);
expect(invalidResult.unwrapErr()).toBe('Must be at least 3 characters');
});
it('should support complex business rules', () => {
interface DiscountInput {
basePrice: number;
customerType: 'regular' | 'premium' | 'vip';
hasCoupon: boolean;
}
const discountCalculator: ResultDomainCalculationService<DiscountInput, number, string> = {
calculate: (input) => {
let discount = 0;
// Customer type discount
switch (input.customerType) {
case 'premium':
discount += 0.1;
break;
case 'vip':
discount += 0.2;
break;
}
// Coupon discount
if (input.hasCoupon) {
discount += 0.05;
}
// Validate price
if (input.basePrice <= 0) {
return Result.err('Price must be positive');
}
const finalPrice = input.basePrice * (1 - discount);
return Result.ok(finalPrice);
}
};
const vipWithCoupon = discountCalculator.calculate({
basePrice: 100,
customerType: 'vip',
hasCoupon: true
});
expect(vipWithCoupon.isOk()).toBe(true);
expect(vipWithCoupon.unwrap()).toBe(75); // 100 * (1 - 0.2 - 0.05) = 75
const invalidPrice = discountCalculator.calculate({
basePrice: 0,
customerType: 'regular',
hasCoupon: false
});
expect(invalidPrice.isErr()).toBe(true);
expect(invalidPrice.unwrapErr()).toBe('Price must be positive');
});
});
describe('DomainValidationService interface', () => {
it('should have validate method returning Result', () => {
const service: DomainValidationService<string, boolean, string> = {
validate: (input: string) => {
if (input.length === 0) {
return Result.err('Input cannot be empty');
}
return Result.ok(true);
}
};
const validResult = service.validate('test');
expect(validResult.isOk()).toBe(true);
expect(validResult.unwrap()).toBe(true);
const invalidResult = service.validate('');
expect(invalidResult.isErr()).toBe(true);
expect(invalidResult.unwrapErr()).toBe('Input cannot be empty');
});
it('should support validation of complex objects', () => {
interface UserInput {
email: string;
password: string;
age: number;
}
const userValidator: DomainValidationService<UserInput, UserInput, string> = {
validate: (input) => {
const errors: string[] = [];
if (!input.email.includes('@')) {
errors.push('Invalid email format');
}
if (input.password.length < 8) {
errors.push('Password must be at least 8 characters');
}
if (input.age < 18) {
errors.push('Must be at least 18 years old');
}
if (errors.length > 0) {
return Result.err(errors.join(', '));
}
return Result.ok(input);
}
};
const validUser = userValidator.validate({
email: 'john@example.com',
password: 'securepassword',
age: 25
});
expect(validUser.isOk()).toBe(true);
expect(validUser.unwrap()).toEqual({
email: 'john@example.com',
password: 'securepassword',
age: 25
});
const invalidUser = userValidator.validate({
email: 'invalid-email',
password: 'short',
age: 15
});
expect(invalidUser.isErr()).toBe(true);
expect(invalidUser.unwrapErr()).toContain('Invalid email format');
expect(invalidUser.unwrapErr()).toContain('Password must be at least 8 characters');
expect(invalidUser.unwrapErr()).toContain('Must be at least 18 years old');
});
});
describe('DomainFactoryService interface', () => {
it('should have create method', () => {
const service: DomainFactoryService<string, { id: number; value: string }> = {
create: (input: string) => ({
id: input.length,
value: input.toUpperCase()
})
};
const result = service.create('test');
expect(result).toEqual({ id: 4, value: 'TEST' });
});
it('should support creating complex objects', () => {
interface CreateUserInput {
name: string;
email: string;
}
interface User {
id: string;
name: string;
email: string;
createdAt: Date;
}
const userFactory: DomainFactoryService<CreateUserInput, User> = {
create: (input) => ({
id: `user-${Date.now()}`,
name: input.name,
email: input.email,
createdAt: new Date()
})
};
const user = userFactory.create({
name: 'John Doe',
email: 'john@example.com'
});
expect(user.id).toMatch(/^user-\d+$/);
expect(user.name).toBe('John Doe');
expect(user.email).toBe('john@example.com');
expect(user.createdAt).toBeInstanceOf(Date);
});
it('should support creating value objects', () => {
interface AddressProps {
street: string;
city: string;
zipCode: string;
}
const addressFactory: DomainFactoryService<AddressProps, AddressProps> = {
create: (input) => ({
street: input.street.trim(),
city: input.city.trim(),
zipCode: input.zipCode.trim()
})
};
const address = addressFactory.create({
street: ' 123 Main St ',
city: ' New York ',
zipCode: ' 10001 '
});
expect(address.street).toBe('123 Main St');
expect(address.city).toBe('New York');
expect(address.zipCode).toBe('10001');
});
});
describe('ServiceAlias types', () => {
it('should be assignable to their base interfaces', () => {
const service1: DomainServiceAlias = { serviceName: 'Test' };
const service2: DomainCalculationServiceAlias<number, number> = {
calculate: (x) => x * 2
};
const service3: ResultDomainCalculationServiceAlias<number, number, string> = {
calculate: (x) => Result.ok(x * 2)
};
const service4: DomainValidationServiceAlias<string, boolean, string> = {
validate: (x) => Result.ok(x.length > 0)
};
const service5: DomainFactoryServiceAlias<string, string> = {
create: (x) => x.toUpperCase()
};
expect(service1.serviceName).toBe('Test');
expect(service2.calculate(5)).toBe(10);
expect(service3.calculate(5).isOk()).toBe(true);
expect(service4.validate('test').isOk()).toBe(true);
expect(service5.create('test')).toBe('TEST');
});
});
});