321 lines
9.9 KiB
TypeScript
321 lines
9.9 KiB
TypeScript
/**
|
|
* Tests for EligibilityEvaluator
|
|
*/
|
|
|
|
import { EligibilityFilterDto } from '../types/Eligibility';
|
|
import { EligibilityEvaluator, RatingData } from './EligibilityEvaluator';
|
|
|
|
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);
|
|
});
|
|
});
|
|
});
|