wip league admin tools
This commit is contained in:
38
core/racing/domain/value-objects/RaceName.test.ts
Normal file
38
core/racing/domain/value-objects/RaceName.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { RaceName } from './RaceName';
|
||||
|
||||
describe('RaceName', () => {
|
||||
it('creates a valid name and exposes stable value/toString', () => {
|
||||
const name = RaceName.fromString('Valid Race Name');
|
||||
expect(name.value).toBe('Valid Race Name');
|
||||
expect(name.toString()).toBe('Valid Race Name');
|
||||
});
|
||||
|
||||
it('trims leading/trailing whitespace', () => {
|
||||
const name = RaceName.fromString(' Valid Race Name ');
|
||||
expect(name.value).toBe('Valid Race Name');
|
||||
});
|
||||
|
||||
it('rejects empty/blank values', () => {
|
||||
expect(() => RaceName.fromString('')).toThrow('Race name cannot be empty');
|
||||
expect(() => RaceName.fromString(' ')).toThrow('Race name cannot be empty');
|
||||
});
|
||||
|
||||
it('rejects names shorter than 3 characters (after trim)', () => {
|
||||
expect(() => RaceName.fromString('ab')).toThrow('Race name must be at least 3 characters long');
|
||||
expect(() => RaceName.fromString(' ab ')).toThrow('Race name must be at least 3 characters long');
|
||||
});
|
||||
|
||||
it('rejects names longer than 100 characters (after trim)', () => {
|
||||
expect(() => RaceName.fromString('a'.repeat(101))).toThrow('Race name must not exceed 100 characters');
|
||||
expect(() => RaceName.fromString(` ${'a'.repeat(101)} `)).toThrow('Race name must not exceed 100 characters');
|
||||
});
|
||||
|
||||
it('equals compares by normalized value', () => {
|
||||
const a = RaceName.fromString(' Test Race ');
|
||||
const b = RaceName.fromString('Test Race');
|
||||
const c = RaceName.fromString('Different');
|
||||
expect(a.equals(b)).toBe(true);
|
||||
expect(a.equals(c)).toBe(false);
|
||||
});
|
||||
});
|
||||
82
core/racing/domain/value-objects/RaceStatus.test.ts
Normal file
82
core/racing/domain/value-objects/RaceStatus.test.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { RaceStatus, type RaceStatusValue } from './RaceStatus';
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
|
||||
describe('RaceStatus', () => {
|
||||
it('creates a status and exposes stable value/toString/props', () => {
|
||||
const status = RaceStatus.create('scheduled');
|
||||
expect(status.value).toBe('scheduled');
|
||||
expect(status.toString()).toBe('scheduled');
|
||||
expect(status.props).toEqual({ value: 'scheduled' });
|
||||
});
|
||||
|
||||
it('rejects missing value', () => {
|
||||
expect(() => RaceStatus.create('' as unknown as RaceStatusValue)).toThrow(
|
||||
RacingDomainValidationError,
|
||||
);
|
||||
expect(() => RaceStatus.create('' as unknown as RaceStatusValue)).toThrow(
|
||||
'Race status is required',
|
||||
);
|
||||
});
|
||||
|
||||
it('supports lifecycle guard helpers', () => {
|
||||
expect(RaceStatus.create('scheduled').canStart()).toBe(true);
|
||||
expect(RaceStatus.create('running').canStart()).toBe(false);
|
||||
|
||||
expect(RaceStatus.create('running').canComplete()).toBe(true);
|
||||
expect(RaceStatus.create('scheduled').canComplete()).toBe(false);
|
||||
|
||||
expect(RaceStatus.create('scheduled').canCancel()).toBe(true);
|
||||
expect(RaceStatus.create('running').canCancel()).toBe(true);
|
||||
expect(RaceStatus.create('completed').canCancel()).toBe(false);
|
||||
|
||||
expect(RaceStatus.create('completed').canReopen()).toBe(true);
|
||||
expect(RaceStatus.create('cancelled').canReopen()).toBe(true);
|
||||
expect(RaceStatus.create('running').canReopen()).toBe(false);
|
||||
|
||||
expect(RaceStatus.create('completed').isTerminal()).toBe(true);
|
||||
expect(RaceStatus.create('cancelled').isTerminal()).toBe(true);
|
||||
expect(RaceStatus.create('running').isTerminal()).toBe(false);
|
||||
|
||||
expect(RaceStatus.create('running').isRunning()).toBe(true);
|
||||
expect(RaceStatus.create('completed').isCompleted()).toBe(true);
|
||||
expect(RaceStatus.create('scheduled').isScheduled()).toBe(true);
|
||||
expect(RaceStatus.create('cancelled').isCancelled()).toBe(true);
|
||||
});
|
||||
|
||||
it('validates allowed transitions', () => {
|
||||
const scheduled = RaceStatus.create('scheduled');
|
||||
const running = RaceStatus.create('running');
|
||||
const completed = RaceStatus.create('completed');
|
||||
const cancelled = RaceStatus.create('cancelled');
|
||||
|
||||
expect(scheduled.canTransitionTo('running')).toEqual({ valid: true });
|
||||
expect(scheduled.canTransitionTo('cancelled')).toEqual({ valid: true });
|
||||
|
||||
expect(running.canTransitionTo('completed')).toEqual({ valid: true });
|
||||
expect(running.canTransitionTo('cancelled')).toEqual({ valid: true });
|
||||
|
||||
expect(completed.canTransitionTo('scheduled')).toEqual({ valid: true });
|
||||
expect(cancelled.canTransitionTo('scheduled')).toEqual({ valid: true });
|
||||
});
|
||||
|
||||
it('rejects disallowed transitions with a helpful error', () => {
|
||||
const scheduled = RaceStatus.create('scheduled');
|
||||
const result = scheduled.canTransitionTo('completed');
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.error).toBe('Cannot transition from scheduled to completed');
|
||||
});
|
||||
|
||||
it('equals compares by value', () => {
|
||||
const a = RaceStatus.create('scheduled');
|
||||
const b = RaceStatus.create('scheduled');
|
||||
const c = RaceStatus.create('running');
|
||||
expect(a.equals(b)).toBe(true);
|
||||
expect(a.equals(c)).toBe(false);
|
||||
});
|
||||
|
||||
it('only allowed status values compile (type-level)', () => {
|
||||
const allowed: RaceStatusValue[] = ['scheduled', 'running', 'completed', 'cancelled'];
|
||||
expect(allowed).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
@@ -100,8 +100,8 @@ export class RaceStatus implements IValueObject<RaceStatusProps> {
|
||||
const allowedTransitions: Record<RaceStatusValue, RaceStatusValue[]> = {
|
||||
scheduled: ['running', 'cancelled'],
|
||||
running: ['completed', 'cancelled'],
|
||||
completed: [],
|
||||
cancelled: [],
|
||||
completed: ['scheduled'],
|
||||
cancelled: ['scheduled'],
|
||||
};
|
||||
|
||||
if (!allowedTransitions[current].includes(target)) {
|
||||
|
||||
@@ -24,8 +24,11 @@ export class StrengthOfField implements IValueObject<StrengthOfFieldProps> {
|
||||
throw new RacingDomainValidationError('Strength of field must be an integer');
|
||||
}
|
||||
|
||||
if (value < 0 || value > 100) {
|
||||
throw new RacingDomainValidationError('Strength of field must be between 0 and 100');
|
||||
// SOF represents iRating-like values (commonly ~0-10k), not a 0-100 percentage.
|
||||
if (value < 0 || value > 10_000) {
|
||||
throw new RacingDomainValidationError(
|
||||
'Strength of field must be between 0 and 10000',
|
||||
);
|
||||
}
|
||||
|
||||
return new StrengthOfField(value);
|
||||
@@ -35,9 +38,9 @@ export class StrengthOfField implements IValueObject<StrengthOfFieldProps> {
|
||||
* Get the strength category
|
||||
*/
|
||||
getCategory(): 'beginner' | 'intermediate' | 'advanced' | 'expert' {
|
||||
if (this.value < 25) return 'beginner';
|
||||
if (this.value < 50) return 'intermediate';
|
||||
if (this.value < 75) return 'advanced';
|
||||
if (this.value < 1500) return 'beginner';
|
||||
if (this.value < 2500) return 'intermediate';
|
||||
if (this.value < 4000) return 'advanced';
|
||||
return 'expert';
|
||||
}
|
||||
|
||||
@@ -45,8 +48,8 @@ export class StrengthOfField implements IValueObject<StrengthOfFieldProps> {
|
||||
* Check if this SOF is suitable for the given participant count
|
||||
*/
|
||||
isSuitableForParticipants(count: number): boolean {
|
||||
// Higher SOF should generally have more participants
|
||||
const minExpected = Math.floor(this.value / 10);
|
||||
// Higher SOF should generally have more participants.
|
||||
const minExpected = Math.floor(this.value / 500);
|
||||
return count >= minExpected;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,30 +3,34 @@ import { TrackId } from './TrackId';
|
||||
|
||||
describe('TrackId', () => {
|
||||
it('should create track id', () => {
|
||||
const id = TrackId.create('track-123');
|
||||
expect(id.toString()).toBe('track-123');
|
||||
expect(id.props).toBe('track-123');
|
||||
const uuid = '550e8400-e29b-41d4-a716-446655440000';
|
||||
const id = TrackId.create(uuid);
|
||||
expect(id.toString()).toBe(uuid);
|
||||
expect(id.value).toBe(uuid);
|
||||
});
|
||||
|
||||
it('should trim whitespace', () => {
|
||||
const id = TrackId.create(' track-123 ');
|
||||
it('should allow fromString without validation', () => {
|
||||
const id = TrackId.fromString('track-123');
|
||||
expect(id.toString()).toBe('track-123');
|
||||
expect(id.value).toBe('track-123');
|
||||
});
|
||||
|
||||
it('should throw for empty id', () => {
|
||||
expect(() => TrackId.create('')).toThrow('Track ID cannot be empty');
|
||||
expect(() => TrackId.create(' ')).toThrow('Track ID cannot be empty');
|
||||
it('should throw for invalid uuid', () => {
|
||||
expect(() => TrackId.create('')).toThrow('TrackId must be a valid UUID');
|
||||
expect(() => TrackId.create(' ')).toThrow('TrackId must be a valid UUID');
|
||||
expect(() => TrackId.create('track-123')).toThrow('TrackId must be a valid UUID');
|
||||
});
|
||||
|
||||
it('should equal same ids', () => {
|
||||
const i1 = TrackId.create('track-123');
|
||||
const i2 = TrackId.create('track-123');
|
||||
const uuid = '550e8400-e29b-41d4-a716-446655440000';
|
||||
const i1 = TrackId.create(uuid);
|
||||
const i2 = TrackId.create(uuid);
|
||||
expect(i1.equals(i2)).toBe(true);
|
||||
});
|
||||
|
||||
it('should not equal different ids', () => {
|
||||
const i1 = TrackId.create('track-123');
|
||||
const i2 = TrackId.create('track-456');
|
||||
const i1 = TrackId.create('550e8400-e29b-41d4-a716-446655440000');
|
||||
const i2 = TrackId.create('550e8400-e29b-41d4-a716-446655440001');
|
||||
expect(i1.equals(i2)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ describe('TrackName', () => {
|
||||
it('should create track name', () => {
|
||||
const name = TrackName.create('Silverstone');
|
||||
expect(name.toString()).toBe('Silverstone');
|
||||
expect(name.props).toBe('Silverstone');
|
||||
expect(name.value).toBe('Silverstone');
|
||||
});
|
||||
|
||||
it('should trim whitespace', () => {
|
||||
@@ -14,8 +14,8 @@ describe('TrackName', () => {
|
||||
});
|
||||
|
||||
it('should throw for empty name', () => {
|
||||
expect(() => TrackName.create('')).toThrow('Track name is required');
|
||||
expect(() => TrackName.create(' ')).toThrow('Track name is required');
|
||||
expect(() => TrackName.create('')).toThrow('Track name cannot be empty');
|
||||
expect(() => TrackName.create(' ')).toThrow('Track name cannot be empty');
|
||||
});
|
||||
|
||||
it('should equal same names', () => {
|
||||
|
||||
Reference in New Issue
Block a user