Files
gridpilot.gg/core/identity/domain/services/EligibilityEvaluator.test.ts
2025-12-29 22:27:33 +01:00

321 lines
9.9 KiB
TypeScript

/**
* Tests for EligibilityEvaluator
*/
import { EligibilityEvaluator, RatingData } from './EligibilityEvaluator';
import { EligibilityFilterDto } from '../../application/dtos/EligibilityFilterDto';
describe('EligibilityEvaluator', () => {
let evaluator: EligibilityEvaluator;
beforeEach(() => {
evaluator = new EligibilityEvaluator();
});
describe('DSL Parsing', () => {
it('should parse simple platform condition', () => {
const result = evaluator.parseDSL('platform.driving >= 55');
expect(result.logicalOperator).toBe('AND');
expect(result.conditions).toHaveLength(1);
expect(result.conditions[0]).toEqual({
target: 'platform',
dimension: 'driving',
operator: '>=',
expected: 55,
});
});
it('should parse simple external condition', () => {
const result = evaluator.parseDSL('external.iracing.iRating between 2000 2500');
expect(result.conditions).toHaveLength(1);
expect(result.conditions[0]).toEqual({
target: 'external',
game: 'iracing',
dimension: 'iRating',
operator: 'between',
expected: [2000, 2500],
});
});
it('should parse AND conditions', () => {
const result = evaluator.parseDSL('platform.driving >= 55 AND external.iracing.iRating >= 2000');
expect(result.logicalOperator).toBe('AND');
expect(result.conditions).toHaveLength(2);
});
it('should parse OR conditions', () => {
const result = evaluator.parseDSL('platform.driving >= 55 OR external.iracing.iRating between 2000 2500');
expect(result.logicalOperator).toBe('OR');
expect(result.conditions).toHaveLength(2);
});
it('should handle all comparison operators', () => {
const operators = ['>=', '<=', '>', '<', '=', '!='];
operators.forEach(op => {
const result = evaluator.parseDSL(`platform.driving ${op} 55`);
const condition = result.conditions[0];
expect(condition).toBeDefined();
if (condition) {
expect(condition.operator).toBe(op);
}
});
});
it('should throw on invalid format', () => {
expect(() => evaluator.parseDSL('invalid format')).toThrow();
});
it('should throw on mixed AND/OR', () => {
expect(() => evaluator.parseDSL('a >= 1 AND b >= 2 OR c >= 3')).toThrow();
});
});
describe('Evaluation', () => {
const ratingData: RatingData = {
platform: {
driving: 65,
admin: 70,
trust: 80,
},
external: {
iracing: {
iRating: 2200,
safetyRating: 4.5,
},
assetto: {
rating: 85,
},
},
};
it('should evaluate simple platform condition - pass', () => {
const filter: EligibilityFilterDto = {
dsl: 'platform.driving >= 55',
};
const result = evaluator.evaluate(filter, ratingData);
expect(result.eligible).toBe(true);
expect(result.reasons).toHaveLength(1);
expect(result.reasons[0].failed).toBe(false);
});
it('should evaluate simple platform condition - fail', () => {
const filter: EligibilityFilterDto = {
dsl: 'platform.driving >= 75',
};
const result = evaluator.evaluate(filter, ratingData);
expect(result.eligible).toBe(false);
expect(result.reasons[0].failed).toBe(true);
});
it('should evaluate external condition with between - pass', () => {
const filter: EligibilityFilterDto = {
dsl: 'external.iracing.iRating between 2000 2500',
};
const result = evaluator.evaluate(filter, ratingData);
expect(result.eligible).toBe(true);
expect(result.reasons[0].failed).toBe(false);
});
it('should evaluate external condition with between - fail', () => {
const filter: EligibilityFilterDto = {
dsl: 'external.iracing.iRating between 2500 3000',
};
const result = evaluator.evaluate(filter, ratingData);
expect(result.eligible).toBe(false);
expect(result.reasons[0].failed).toBe(true);
});
it('should evaluate AND conditions - all pass', () => {
const filter: EligibilityFilterDto = {
dsl: 'platform.driving >= 55 AND external.iracing.iRating >= 2000',
};
const result = evaluator.evaluate(filter, ratingData);
expect(result.eligible).toBe(true);
expect(result.reasons).toHaveLength(2);
expect(result.reasons.every(r => !r.failed)).toBe(true);
});
it('should evaluate AND conditions - one fails', () => {
const filter: EligibilityFilterDto = {
dsl: 'platform.driving >= 55 AND external.iracing.iRating >= 3000',
};
const result = evaluator.evaluate(filter, ratingData);
expect(result.eligible).toBe(false);
expect(result.reasons.some(r => r.failed)).toBe(true);
});
it('should evaluate OR conditions - at least one passes', () => {
const filter: EligibilityFilterDto = {
dsl: 'platform.driving >= 75 OR external.iracing.iRating >= 2000',
};
const result = evaluator.evaluate(filter, ratingData);
expect(result.eligible).toBe(true);
expect(result.reasons.filter(r => !r.failed).length).toBeGreaterThanOrEqual(1);
});
it('should evaluate OR conditions - all fail', () => {
const filter: EligibilityFilterDto = {
dsl: 'platform.driving >= 75 OR external.iracing.iRating >= 3000',
};
const result = evaluator.evaluate(filter, ratingData);
expect(result.eligible).toBe(false);
expect(result.reasons.every(r => r.failed)).toBe(true);
});
it('should handle missing data gracefully', () => {
const filter: EligibilityFilterDto = {
dsl: 'external.missing.value >= 100',
};
const result = evaluator.evaluate(filter, ratingData);
expect(result.eligible).toBe(false);
expect(result.reasons[0].failed).toBe(true);
expect(result.reasons[0].message).toContain('Missing data');
});
it('should include metadata with userId', () => {
const filter: EligibilityFilterDto = {
dsl: 'platform.driving >= 55',
context: { userId: 'user-123' },
};
const result = evaluator.evaluate(filter, ratingData);
expect(result.metadata?.userId).toBe('user-123');
expect(result.metadata?.filter).toBe('platform.driving >= 55');
});
it('should provide explainable reasons', () => {
const filter: EligibilityFilterDto = {
dsl: 'platform.driving >= 75',
};
const result = evaluator.evaluate(filter, ratingData);
expect(result.reasons[0]).toMatchObject({
target: 'platform.driving',
operator: '>=',
expected: 75,
actual: 65,
failed: true,
message: expect.stringContaining('Expected platform.driving >= 75, but got 65'),
});
});
it('should handle all operators correctly', () => {
const testCases = [
{ dsl: 'platform.driving >= 60', expected: true },
{ dsl: 'platform.driving > 65', expected: false },
{ dsl: 'platform.driving <= 70', expected: true },
{ dsl: 'platform.driving < 65', expected: false },
{ dsl: 'platform.driving = 65', expected: true },
{ dsl: 'platform.driving != 65', expected: false },
];
testCases.forEach(({ dsl, expected }) => {
const result = evaluator.evaluate({ dsl }, ratingData);
expect(result.eligible).toBe(expected);
});
});
it('should handle decimal values', () => {
const filter: EligibilityFilterDto = {
dsl: 'external.iracing.safetyRating >= 4.0',
};
const result = evaluator.evaluate(filter, ratingData);
expect(result.eligible).toBe(true);
expect(result.reasons[0].actual).toBe(4.5);
});
});
describe('Summary Generation', () => {
const ratingData: RatingData = {
platform: { driving: 65 },
external: { iracing: { iRating: 2200 } },
};
it('should generate summary for AND all pass', () => {
const result = evaluator.evaluate(
{ dsl: 'platform.driving >= 55 AND external.iracing.iRating >= 2000' },
ratingData
);
expect(result.summary).toBe('Eligible: All conditions met (2/2)');
});
it('should generate summary for OR at least one pass', () => {
const result = evaluator.evaluate(
{ dsl: 'platform.driving >= 75 OR external.iracing.iRating >= 2000' },
ratingData
);
expect(result.summary).toContain('Eligible: At least one condition met');
});
it('should generate summary for AND with failures', () => {
const result = evaluator.evaluate(
{ dsl: 'platform.driving >= 55 AND external.iracing.iRating >= 3000' },
ratingData
);
expect(result.summary).toContain('Not eligible: 1 condition(s) failed');
});
it('should generate summary for OR all fail', () => {
const result = evaluator.evaluate(
{ dsl: 'platform.driving >= 75 OR external.iracing.iRating >= 3000' },
ratingData
);
expect(result.summary).toContain('Not eligible: All conditions failed');
});
});
describe('Error Handling', () => {
it('should handle parsing errors gracefully', () => {
const result = evaluator.evaluate(
{ dsl: 'invalid syntax here' },
{ platform: {}, external: {} }
);
expect(result.eligible).toBe(false);
expect(result.reasons).toHaveLength(0);
expect(result.summary).toContain('Failed to evaluate filter');
expect(result.metadata?.error).toBeDefined();
});
it('should handle empty DSL', () => {
const result = evaluator.evaluate(
{ dsl: '' },
{ platform: {}, external: {} }
);
expect(result.eligible).toBe(false);
});
});
});