rating
This commit is contained in:
320
core/identity/domain/services/EligibilityEvaluator.test.ts
Normal file
320
core/identity/domain/services/EligibilityEvaluator.test.ts
Normal file
@@ -0,0 +1,320 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user