contract-tests.test.ts•11 kB
/**
* Contract Tests (Fase 3)
*
* Validates that tools conform to their published contracts/schemas.
* Ensures backward compatibility and proper error responses.
*/
import { describe, it, expect } from '@jest/globals';
import { ValidationError, ErrorCode } from '../../src/types/index';
// ==============================================================================
// SCHEMA VALIDATION TESTS
// ==============================================================================
describe('Tool Contract Tests', () => {
describe('bereken_hypotheek_starter', () => {
it('should accept minimal valid input', () => {
const input = {
inkomen_aanvrager: 50000,
geboortedatum_aanvrager: '1990-05-15',
heeft_partner: false,
verplichtingen_pm: 0
};
// This should not throw
expect(() => validateInput(input)).not.toThrow();
});
it('should accept input with partner', () => {
const input = {
inkomen_aanvrager: 50000,
geboortedatum_aanvrager: '1990-05-15',
heeft_partner: true,
inkomen_partner: 40000,
geboortedatum_partner: '1992-08-20',
verplichtingen_pm: 250
};
expect(() => validateInput(input)).not.toThrow();
});
it('should accept optional session_id', () => {
const input = {
inkomen_aanvrager: 50000,
geboortedatum_aanvrager: '1990-05-15',
heeft_partner: false,
verplichtingen_pm: 0,
session_id: '550e8400-e29b-41d4-a716-446655440000'
};
expect(() => validateInput(input)).not.toThrow();
});
it('should reject invalid inkomen_aanvrager type', () => {
const input = {
inkomen_aanvrager: '50000', // String instead of number
geboortedatum_aanvrager: '1990-05-15',
heeft_partner: false
};
expect(() => validateInput(input)).toThrow();
});
it('should reject missing required fields', () => {
const input = {
inkomen_aanvrager: 50000
// Missing: geboortedatum_aanvrager, heeft_partner
};
expect(() => validateInput(input)).toThrow();
});
});
describe('bereken_hypotheek_doorstromer', () => {
it('should accept valid doorstromer input', () => {
const input = {
inkomen_aanvrager: 65000,
geboortedatum_aanvrager: '1985-03-15',
heeft_partner: false,
verplichtingen_pm: 0,
waarde_huidige_woning: 350000,
bestaande_hypotheek: {
leningdelen: [{
huidige_schuld: 180000,
huidige_rente: 0.032,
resterende_looptijd_in_maanden: 240,
rentevasteperiode_maanden: 120,
hypotheekvorm: 'annuiteit'
}]
}
};
expect(() => validateDoorstromerInput(input)).not.toThrow();
});
it('should accept multiple leningdelen', () => {
const input = {
inkomen_aanvrager: 70000,
geboortedatum_aanvrager: '1980-01-01',
heeft_partner: false,
verplichtingen_pm: 0,
waarde_huidige_woning: 400000,
bestaande_hypotheek: {
leningdelen: [
{
huidige_schuld: 150000,
huidige_rente: 0.025,
resterende_looptijd_in_maanden: 180,
rentevasteperiode_maanden: 60,
hypotheekvorm: 'annuiteit'
},
{
huidige_schuld: 50000,
huidige_rente: 0.04,
resterende_looptijd_in_maanden: 120,
rentevasteperiode_maanden: 0,
hypotheekvorm: 'aflossingsvrij'
}
]
}
};
expect(() => validateDoorstromerInput(input)).not.toThrow();
});
it('should reject missing waarde_huidige_woning', () => {
const input = {
inkomen_aanvrager: 65000,
geboortedatum_aanvrager: '1985-03-15',
heeft_partner: false,
verplichtingen_pm: 0,
// Missing: waarde_huidige_woning
bestaande_hypotheek: {
leningdelen: [{
huidige_schuld: 180000,
huidige_rente: 0.032,
resterende_looptijd_in_maanden: 240,
rentevasteperiode_maanden: 120,
hypotheekvorm: 'annuiteit'
}]
}
};
expect(() => validateDoorstromerInput(input)).toThrow();
});
it('should reject invalid hypotheekvorm', () => {
const input = {
inkomen_aanvrager: 65000,
geboortedatum_aanvrager: '1985-03-15',
heeft_partner: false,
verplichtingen_pm: 0,
waarde_huidige_woning: 350000,
bestaande_hypotheek: {
leningdelen: [{
huidige_schuld: 180000,
huidige_rente: 0.032,
resterende_looptijd_in_maanden: 240,
rentevasteperiode_maanden: 120,
hypotheekvorm: 'invalid_type' // Invalid!
}]
}
};
expect(() => validateDoorstromerInput(input)).toThrow();
});
});
});
// ==============================================================================
// ERROR RESPONSE TESTS
// ==============================================================================
describe('Error Response Contracts', () => {
it('should return structured error for validation failure', () => {
const error = new ValidationError(
ErrorCode.AGE_OUT_OF_RANGE,
'Leeftijd moet tussen 18 en 75 zijn',
'geboortedatum_aanvrager',
'2010-01-01'
);
const structured = error.toStructured('test-session-id');
expect(structured).toHaveProperty('code');
expect(structured).toHaveProperty('message');
expect(structured).toHaveProperty('field');
expect(structured).toHaveProperty('correlation_id');
expect(structured.code).toBe(ErrorCode.AGE_OUT_OF_RANGE);
});
it('should include retry_after_ms for rate limit errors', () => {
// This would come from actual tool call error
const errorResponse = {
code: ErrorCode.API_RATE_LIMIT,
message: 'Rate limit exceeded',
retry_after_ms: 30000
};
expect(errorResponse.retry_after_ms).toBeGreaterThan(0);
});
it('should include field information for validation errors', () => {
const error = new ValidationError(
ErrorCode.INCOME_OUT_OF_RANGE,
'Inkomen te hoog',
'inkomen_aanvrager',
1500000
);
const structured = error.toStructured();
expect(structured.field).toBe('inkomen_aanvrager');
expect(structured.details).toHaveProperty('value');
});
});
// ==============================================================================
// BACKWARD COMPATIBILITY TESTS
// ==============================================================================
describe('Backward Compatibility', () => {
it('should still accept hypotheekvorm as string (not just enum)', () => {
// v3.0 accepted strings, v4.0 should still accept for compatibility
const input = {
huidige_schuld: 180000,
huidige_rente: 0.032,
resterende_looptijd_in_maanden: 240,
rentevasteperiode_maanden: 120,
hypotheekvorm: 'annuiteit' // String, not Hypotheekvorm enum
};
// Should not throw
expect(() => validateLeningdeel(input)).not.toThrow();
});
it('should accept both "annuiteit" and "annuïteit" spellings', () => {
// Common user mistake - we should handle gracefully
const input1 = { hypotheekvorm: 'annuiteit' };
const input2 = { hypotheekvorm: 'annuïteit' };
// Both should work (second gets normalized)
expect(() => normalizeHypotheekvorm(input1.hypotheekvorm)).not.toThrow();
// Note: annuïteit should throw in v4 strict mode, but we document the breaking change
});
});
// ==============================================================================
// RESPONSE SCHEMA TESTS
// ==============================================================================
describe('Response Schema Validation', () => {
it('should return consistent structure for maximaal berekening', () => {
const mockResponse = {
resultaat: [
{
maximaal_bedrag: 220000,
bruto_maandlasten_nieuwe_lening: 1015,
resultaat_omschrijving: 'Met NHG',
gebruikte_hypotheekgegevens: {
nhg_toegepast: true,
energielabel: 'B',
opzet_nieuwe_hypotheek: []
}
}
]
};
// Validate structure
expect(mockResponse).toHaveProperty('resultaat');
expect(Array.isArray(mockResponse.resultaat)).toBe(true);
expect(mockResponse.resultaat[0]).toHaveProperty('maximaal_bedrag');
expect(mockResponse.resultaat[0]).toHaveProperty('bruto_maandlasten_nieuwe_lening');
});
it('should return consistent structure for opzet berekening', () => {
const mockResponse = {
resultaat: {
Benodigd_bedrag: {
Woning_koopsom: 300000,
Kosten: 15000
},
Financiering: {
Hypotheek: 300000,
Eigen_geld: 15000
},
bruto_maandlasten_nieuwe_lening: 1385
}
};
// Validate structure
expect(mockResponse).toHaveProperty('resultaat');
expect(mockResponse.resultaat).toHaveProperty('Benodigd_bedrag');
expect(mockResponse.resultaat).toHaveProperty('Financiering');
});
});
// ==============================================================================
// HELPER FUNCTIONS (would import from actual code)
// ==============================================================================
function validateInput(input: any): void {
// Simplified validation - actual code in validation/schemas.ts
if (typeof input.inkomen_aanvrager !== 'number') {
throw new Error('inkomen_aanvrager must be number');
}
if (typeof input.geboortedatum_aanvrager !== 'string') {
throw new Error('geboortedatum_aanvrager must be string');
}
if (typeof input.heeft_partner !== 'boolean') {
throw new Error('heeft_partner must be boolean');
}
}
function validateDoorstromerInput(input: any): void {
validateInput(input);
if (typeof input.waarde_huidige_woning !== 'number') {
throw new Error('waarde_huidige_woning required');
}
if (!input.bestaande_hypotheek || !input.bestaande_hypotheek.leningdelen) {
throw new Error('bestaande_hypotheek.leningdelen required');
}
// Validate each leningdeel using the same logic as validateLeningdeel
input.bestaande_hypotheek.leningdelen.forEach((ld: any) => validateLeningdeel(ld));
}
function validateLeningdeel(input: any): void {
const validTypes = ['annuiteit', 'lineair', 'aflossingsvrij'];
if (!validTypes.includes(input.hypotheekvorm)) {
throw new Error('Invalid hypotheekvorm');
}
}
function normalizeHypotheekvorm(value: string): string {
// Would call actual normalization function
return value.toLowerCase().replace('ï', 'i');
}