/** * 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); }); }); });