import { describe, it, before, after } from 'node:test'; import assert from 'node:assert/strict'; import crypto from 'node:crypto'; import fs from 'node:fs'; import path from 'node:path'; import { db } from '../db/database.js'; import { initializeDatabase } from '../db/schema.js'; import { hashPassword, comparePassword, generateApiKey, hashToken, generateAccessToken, verifyAccessToken, generateRefreshToken, } from '../utils/token.js'; import { OcrService } from '../services/ocrService.js'; import { MaintenanceService } from '../services/maintenanceService.js'; import { TcoService } from '../services/tcoService.js'; import { executeMcpTool } from '../mcp/tools.js'; import { ImportService } from '../services/importService.js'; import { RdwService } from '../services/rdwService.js'; import { AiReceiptService, getSystemSetting, setSystemSetting, maskApiKey, extractDutchAddress, filterVisionModels, formatStationName, formatAddress, } from '../services/aiReceiptService.js'; import { StationService } from '../services/stationService.js'; import { isAuthDisabled, getDefaultUser } from '../middleware/auth.js'; import { HaService } from '../services/haService.js'; import { NotificationService } from '../services/notificationService.js'; import { BackupService } from '../services/backupService.js'; import { TripExportService } from '../services/tripExportService.js'; import { FuelExportService } from '../services/fuelExportService.js'; import { getVehicleAccess } from '../utils/vehicleAccess.js'; import { ShowcaseService } from '../services/showcaseService.js'; import { ShowcaseZipService } from '../services/showcaseZipService.js'; import { AuthController } from '../controllers/authController.js'; import { FuelController } from '../controllers/fuelController.js'; import { MaintenanceController } from '../controllers/maintenanceController.js'; import { DocumentController } from '../controllers/documentController.js'; import { AnalyticsController } from '../controllers/analyticsController.js'; import { HaController } from '../controllers/haController.js'; import { CalendarService } from '../services/calendarService.js'; import { PdfService } from '../services/pdfService.js'; import { PDFDocument as PdfLibDocument } from 'pdf-lib'; import { DATA_PATHS } from '../config/env.js'; import { EmailService } from '../services/emailService.js'; describe('VehicleVault Comprehensive Test Suite', () => { let testUserId: string; let testVehicleId: string; before(() => { initializeDatabase(); testUserId = crypto.randomUUID(); testVehicleId = crypto.randomUUID(); // Create test user db.run( 'INSERT INTO users (id, email, password_hash, role, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)', [testUserId, 'driver@vault.local', 'fake_hash', 'user', new Date().toISOString(), new Date().toISOString()] ); // Create test vehicle db.run( `INSERT INTO vehicles ( id, user_id, name, type, license_plate, year, make, model, fuel_type, purchase_date, purchase_price, initial_odometer, status, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ testVehicleId, testUserId, 'Daily Driver Golf 7', 'car', '12-XYZ-3', 2018, 'Volkswagen', 'Golf', 'Petrol', '2023-01-15', 15000, 50000, 'active', new Date().toISOString(), new Date().toISOString(), ] ); }); after(() => { // Clean up test data db.run('DELETE FROM users WHERE id = ?', [testUserId]); db.run('DELETE FROM vehicles WHERE id = ?', [testVehicleId]); }); describe('Security & Auth Utilities', () => { it('should hash and verify passwords correctly with bcrypt', async () => { const password = 'SuperSecretPassword123!'; const hash = await hashPassword(password); assert.ok(hash !== password); assert.ok(await comparePassword(password, hash)); assert.ok(!(await comparePassword('WrongPassword', hash))); }); it('should generate vkv_ prefixed API keys with sha256 hash', () => { const { rawKey, keyHash, prefix } = generateApiKey('AI Assistant Key'); assert.ok(rawKey.startsWith('vkv_')); assert.ok(prefix.startsWith('vkv_')); assert.equal(hashToken(rawKey), keyHash); }); it('should support disabling authentication via system settings or environment variable', () => { // Default should be false db.run('DELETE FROM system_settings WHERE key = ?', ['disable_auth']); delete process.env.DISABLE_AUTH; assert.equal(isAuthDisabled(), false); // Enable via system_settings setSystemSetting('disable_auth', 'true'); assert.equal(isAuthDisabled(), true); // Verify default user lookup returns a valid user const defaultUser = getDefaultUser(); assert.ok(defaultUser.userId); assert.ok(defaultUser.email); assert.ok(['admin', 'user', 'driver'].includes(defaultUser.role)); // Clean up db.run('DELETE FROM system_settings WHERE key = ?', ['disable_auth']); assert.equal(isAuthDisabled(), false); }); }); describe('Tesseract OCR Regex Parser', () => { it('should correctly extract liters, price/L, and total from receipt text', () => { const mockReceiptText = ` SHELL STATION AMSTERDAM DATUM: 14-10-2024 16:34 BRANDSTOF: EURO 95 (E10) POMP 04 VOLUME: 42.50 L PRIJS/L: 1.949 EUR/L TOTAAL: 82.83 EUR BEDANKT VOOR UW BEZOEK! `; const parsed = OcrService.parseReceiptText(mockReceiptText, 92); assert.equal(parsed.date, '2024-10-14'); assert.equal(parsed.liters, 42.5); assert.equal(parsed.pricePerLiter, 1.949); assert.equal(parsed.totalCost, 82.83); }); it('should mathematically infer total cost if only liters and price are present', () => { const receiptWithoutTotal = ` TANKEN & GAAN Liters: 50.00 L Prijs: 2.000 €/L `; const parsed = OcrService.parseReceiptText(receiptWithoutTotal, 80); assert.equal(parsed.liters, 50.0); assert.equal(parsed.pricePerLiter, 2.0); assert.equal(parsed.totalCost, 100.0); }); it('should extract date and mathematically reconcile liters on Dutch Fieten Olie dot-matrix receipt', () => { const fietenReceipt = ` FIETEN OLIE Datum 19-10-2026 08:16:03 Pomp 4 95 Ongelood Volume 57 AI rijs € 2.159/ TOTAAL E80 4% * BTW 21.00 % € 14.02 Netto € 66.75 `; const parsed = OcrService.parseReceiptText(fietenReceipt, 85); assert.equal(parsed.date, '2026-10-19'); assert.equal(parsed.pricePerLiter, 2.159); assert.equal(parsed.totalCost, 80.77); assert.equal(parsed.liters, 37.41); }); }); describe('RDW Open Data Parser & APK Evaluation', () => { it('should normalize license plates correctly', () => { assert.equal(RdwService.normalizePlate('12-ab-34'), '12AB34'); assert.equal(RdwService.normalizePlate(' k-123-xx '), 'K123XX'); }); it('should format Dutch license plates with hyphens across official sidecodes', () => { // Sidecode 11: 3 letters, 2 digits, 1 letter assert.equal(RdwService.formatPlate('GBB01B'), 'GBB-01-B'); assert.equal(RdwService.formatPlate('gbb-01-b'), 'GBB-01-B'); // Sidecode 9 (e.g. Mercedes-Benz): 2 letters, 3 digits, 1 letter assert.equal(RdwService.formatPlate('NK956Z'), 'NK-956-Z'); assert.equal(RdwService.formatPlate('NK-956-Z'), 'NK-956-Z'); // Sidecode 10: 1 letter, 3 digits, 2 letters assert.equal(RdwService.formatPlate('G001BB'), 'G-001-BB'); // Sidecode 7: 2 digits, 3 letters, 1 digit assert.equal(RdwService.formatPlate('12TBB3'), '12-TBB-3'); // Sidecode 8: 1 digit, 3 letters, 2 digits assert.equal(RdwService.formatPlate('1TBB12'), '1-TBB-12'); // Sidecode 1 / 3: 2-2-2 patterns assert.equal(RdwService.formatPlate('12AB34'), '12-AB-34'); }); it('should evaluate APK dates within 30 days as due soon, and past dates as overdue', () => { const futureDate = new Date(); futureDate.setDate(futureDate.getDate() + 15); const soonStatus = RdwService.evaluateApkStatus(futureDate.toISOString().split('T')[0]); assert.equal(soonStatus.isDueSoon, true); assert.equal(soonStatus.isOverdue, false); const pastDate = new Date(); pastDate.setDate(pastDate.getDate() - 10); const overdueStatus = RdwService.evaluateApkStatus(pastDate.toISOString().split('T')[0]); assert.equal(overdueStatus.isOverdue, true); assert.equal(overdueStatus.isDueSoon, false); }); }); describe('Dual-Trigger Maintenance Engine', () => { it('should calculate dual-trigger interval (whichever comes first)', () => { // Create a unique category const catId = crypto.randomUUID(); const catName = `Oil Test ${catId.substring(0, 6)}`; db.run('INSERT INTO service_categories (id, name, is_preset) VALUES (?, ?, 0)', [catId, catName]); // Recent service date (10 days ago) so time percent is low (~3%) const recentDate = new Date(); recentDate.setDate(recentDate.getDate() - 10); // Interval: 10,000 km or 12 months const intervalId = crypto.randomUUID(); db.run( `INSERT INTO maintenance_intervals ( id, vehicle_id, category_id, custom_name, interval_km, interval_months, last_service_date, last_service_odometer, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [intervalId, testVehicleId, catId, 'Engine Oil Check', 10000, 12, recentDate.toISOString().split('T')[0], 50000, new Date().toISOString()] ); // Add a fuel log at 59,500 km (9,500 km driven out of 10,000 = 95% -> should be warning/amber) db.run( `INSERT INTO fuel_entries (id, vehicle_id, date, odometer, liters, price_per_liter, total_cost, is_full_tank, missed_previous, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, 1, 0, ?)`, [crypto.randomUUID(), testVehicleId, '2023-06-01', 59500, 45, 1.9, 85.5, new Date().toISOString()] ); const status = MaintenanceService.getVehicleMaintenanceStatus(testVehicleId); const oilItem = status.find((s) => s.id === intervalId); assert.ok(oilItem); assert.equal(oilItem.status, 'warning'); // >= 90% assert.equal(oilItem.kmRemaining, 500); assert.equal(oilItem.triggerReason, 'distance'); }); }); describe('Analytics & TCO Engine', () => { it('should compute operational TCO vs asset TCO with purchase price toggle', () => { // Clean any existing fuel entries for isolated test db.run('DELETE FROM fuel_entries WHERE vehicle_id = ?', [testVehicleId]); db.run('DELETE FROM service_records WHERE vehicle_id = ?', [testVehicleId]); // Add two fuel entries db.run( `INSERT INTO fuel_entries (id, vehicle_id, date, odometer, liters, price_per_liter, total_cost, is_full_tank, missed_previous, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, 1, 0, ?)`, [crypto.randomUUID(), testVehicleId, '2023-06-01', 59500, 45, 1.9, 85.5, new Date().toISOString()] ); db.run( `INSERT INTO fuel_entries (id, vehicle_id, date, odometer, liters, price_per_liter, total_cost, is_full_tank, missed_previous, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, 1, 0, ?)`, [crypto.randomUUID(), testVehicleId, '2023-07-01', 60000, 40, 2.0, 80.0, new Date().toISOString()] ); // Add service record db.run( `INSERT INTO service_records (id, vehicle_id, service_item, date, odometer, performing_party, cost, created_at) VALUES (?, ?, ?, ?, ?, 'garage', ?, ?)`, [crypto.randomUUID(), testVehicleId, 'New Brake Pads', '2023-07-05', 60000, 250, new Date().toISOString()] ); // Operational TCO (without purchase price) const opTco = TcoService.calculateTco({ vehicleId: testVehicleId, includePurchasePrice: false, }); assert.equal(opTco.includePurchasePrice, false); assert.equal(opTco.fuelTotal, 165.5); // 85.5 + 80.0 assert.equal(opTco.serviceTotal, 250.0); assert.equal(opTco.totalCost, 415.5); // Asset TCO (with purchase price of €15,000) const assetTco = TcoService.calculateTco({ vehicleId: testVehicleId, includePurchasePrice: true, }); assert.equal(assetTco.includePurchasePrice, true); assert.equal(assetTco.totalCost, 15415.5); }); it('should calculate fuel price statistics including average, min (cheapest), and max (most expensive) fuel prices', () => { const tco = TcoService.calculateTco({ vehicleId: testVehicleId, }); assert.ok(tco.priceStats); assert.equal(tco.priceStats.minPrice?.price, 1.9); assert.equal(tco.priceStats.minPrice?.date, '2023-06-01'); assert.equal(tco.priceStats.maxPrice?.price, 2.0); assert.equal(tco.priceStats.maxPrice?.date, '2023-07-01'); assert.equal(tco.priceStats.history.length, 2); // Weighted average: (85.5 + 80.0) / (45 + 40) = 165.5 / 85 = 1.947 assert.equal(tco.priceStats.avgPrice, 1.947); }); it('should exclude pre-ownership service records (before purchase_date or below initial_odometer) from TCO', () => { // testVehicleId has purchase_date = '2023-01-15' and initial_odometer = 50000 const preDateRecId = crypto.randomUUID(); const preOdoRecId = crypto.randomUUID(); // 1. Invoice dated 2021-05-10 (before purchase_date 2023-01-15) db.run( `INSERT INTO service_records (id, vehicle_id, service_item, date, odometer, performing_party, cost, created_at) VALUES (?, ?, ?, ?, ?, 'garage', ?, ?)`, [preDateRecId, testVehicleId, 'Grote Beurt Vorige Eigenaar', '2021-05-10', 38000, 680, new Date().toISOString()] ); // 2. Invoice dated 2023-02-01 but odometer 42000 (< initial_odometer 50000) db.run( `INSERT INTO service_records (id, vehicle_id, service_item, date, odometer, performing_party, cost, created_at) VALUES (?, ?, ?, ?, ?, 'garage', ?, ?)`, [preOdoRecId, testVehicleId, 'Import Inspectie Duitsland', '2023-02-01', 42000, 320, new Date().toISOString()] ); try { const tco = TcoService.calculateTco({ vehicleId: testVehicleId, includePurchasePrice: false, }); // Neither the €680 nor the €320 pre-ownership invoice should be added to the €250 own service total assert.equal(tco.serviceTotal, 250.0); assert.equal(tco.totalCost, 415.5); } finally { db.run('DELETE FROM service_records WHERE id IN (?, ?)', [preDateRecId, preOdoRecId]); } }); }); describe('CSV Import Engine', () => { it('should parse and preview Fuelio CSV structure', () => { const fuelioCsv = `## Data Date,Odometer,Fuel_amount,Price,Total_cost,Notes 2024-01-10,61000,45.2,1.95,88.14,Shell highway 2024-02-10,61600,43.8,1.98,86.72,Esso town `; const preview = ImportService.previewImport(fuelioCsv, testVehicleId); assert.equal(preview.detectedFormat, 'fuelio'); assert.equal(preview.validRows, 2); assert.equal(preview.records[0].odometer, 61000); assert.equal(preview.records[0].liters, 45.2); }); }); describe('MCP (Model Context Protocol) Tools', () => { it('should list active vehicles via list_vehicles', async () => { const res = await executeMcpTool('list_vehicles', { status: 'active' }); assert.ok(Array.isArray(res.vehicles)); const found = res.vehicles.find((v: any) => v.id === testVehicleId); assert.ok(found); }); it('should retrieve vehicle summary via get_vehicle_summary', async () => { const res = await executeMcpTool('get_vehicle_summary', { vehicle_id: testVehicleId }); assert.equal(res.vehicle.id, testVehicleId); assert.ok(res.tco); assert.ok(Array.isArray(res.upcomingMaintenanceAlerts)); }); it('should log a fuel entry via log_fuel_entry', async () => { const res = await executeMcpTool('log_fuel_entry', { vehicle_id: testVehicleId, odometer: 62200, liters: 44.0, price_per_liter: 1.95, is_full_tank: true, notes: 'MCP automated fill-up test', }); assert.equal(res.success, true); assert.ok(res.id); }); it('should log a service entry via log_service_entry', async () => { const res = await executeMcpTool('log_service_entry', { vehicle_id: testVehicleId, service_item: 'Cabin Air Filter', odometer: 62200, cost: 35.0, notes: 'MCP automated service log test', }); assert.equal(res.success, true); assert.ok(res.id); }); it('should check maintenance alerts via check_maintenance_alerts', async () => { const res = await executeMcpTool('check_maintenance_alerts', { vehicle_id: testVehicleId }); assert.ok(typeof res.count === 'number'); assert.ok(Array.isArray(res.alerts)); }); }); describe('AI Vision Receipt Scanner & System Settings', () => { it('should save and retrieve system settings correctly', () => { setSystemSetting('ai_provider', 'gemini'); setSystemSetting('gemini_api_key', 'AIzaSyTestKey1234567890'); setSystemSetting('gemini_model', 'gemini-3.8-flash'); assert.equal(getSystemSetting('ai_provider'), 'gemini'); assert.equal(getSystemSetting('gemini_api_key'), 'AIzaSyTestKey1234567890'); assert.equal(getSystemSetting('gemini_model'), 'gemini-3.8-flash'); }); it('should auto-migrate deprecated gemini models to gemini-3.8-flash', () => { setSystemSetting('gemini_model', 'gemini-2.0-flash'); const settings = AiReceiptService.getActiveSettings(); assert.equal(settings.geminiModel, 'gemini-3.8-flash'); assert.equal(getSystemSetting('gemini_model'), 'gemini-3.8-flash'); }); it('should mask API keys securely without exposing raw secrets', () => { const masked = maskApiKey('AIzaSyD-1234567890abcdef'); assert.equal(masked, 'AIzaSy...cdef'); const shortMasked = maskApiKey('short'); assert.equal(shortMasked, '••••••••'); assert.equal(maskApiKey(''), null); assert.equal(maskApiKey(null), null); }); it('should normalize and reconcile AI JSON extraction accurately', () => { const mockGeminiJson = JSON.stringify({ date: '2026-10-19', liters: 37.41, pricePerLiter: 2.159, totalCost: null, // should be reconciled station: 'FIETEN OLIE', // all caps from receipt header -> should be Title Cased stationAddress: 'Eeser Gaard 2, 8332 VN Steenwijk', fuelType: '95 Ongelood', }); const result = AiReceiptService.normalizeAiResult(mockGeminiJson, 'gemini', 'gemini-3.8-flash'); assert.equal(result.date, '2026-10-19'); assert.equal(result.liters, 37.41); assert.equal(result.pricePerLiter, 2.159); assert.equal(result.totalCost, 80.77); // 37.41 * 2.159 = 80.76819 -> 80.77 assert.equal(result.station, 'Fieten Olie'); assert.equal(result.stationAddress, 'Eeser Gaard 2, 8332 VN Steenwijk'); assert.equal(result.fuelType, '95 Ongelood'); assert.equal(result.engine, 'gemini'); }); it('should format all-caps station names and addresses correctly', () => { const mockOpenAiJson = JSON.stringify({ date: '2026-10-27', liters: 40.89, pricePerLiter: 2.079, totalCost: 85.01, station: 'FIETEN OLIE STEENWIJK', stationAddress: 'EESER GAARD 2, 8332 VN STEENWIJK', }); const result = AiReceiptService.normalizeAiResult(mockOpenAiJson, 'openai', 'gpt-4o-mini'); assert.equal(result.station, 'Fieten Olie Steenwijk'); assert.equal(result.stationAddress, 'Eeser Gaard 2, 8332 VN Steenwijk'); }); it('should handle markdown fenced JSON codeblocks from LLMs', () => { const fencedJson = '```json\n{"date":"27-10-2026","liters":"40.89","pricePerLiter":"2.079","totalCost":"85.01","station":"Fieten Olie","fuelType":"95 Ongelood"}\n```'; const result = AiReceiptService.normalizeAiResult(fencedJson, 'openai', 'gpt-4o-mini'); assert.equal(result.date, '2026-10-27'); assert.equal(result.liters, 40.89); assert.equal(result.pricePerLiter, 2.079); assert.equal(result.totalCost, 85.01); assert.equal(result.station, 'Fieten Olie'); assert.equal(result.fuelType, '95 Ongelood'); }); it('should test connection validation rejecting empty keys', async () => { const res = await AiReceiptService.testConnection('gemini', ''); assert.equal(res.success, false); assert.ok(res.message.includes('Geen API-sleutel ingevoerd')); }); it('should preserve existing API key when empty string or masked key is passed on save', () => { setSystemSetting('gemini_api_key', 'AQ.Ab8RealKey123456'); // Simulate SettingsController.updateAiSettings logic const emptyInput = ''; if (emptyInput.trim().length > 0 && !emptyInput.includes('••••') && !emptyInput.includes('...')) { setSystemSetting('gemini_api_key', emptyInput); } assert.equal(getSystemSetting('gemini_api_key'), 'AQ.Ab8RealKey123456'); const maskedInput = 'AQ.Ab8...3456'; if (maskedInput.trim().length > 0 && !maskedInput.includes('••••') && !maskedInput.includes('...')) { setSystemSetting('gemini_api_key', maskedInput); } assert.equal(getSystemSetting('gemini_api_key'), 'AQ.Ab8RealKey123456'); // Only explicit clear should clear it const clearGeminiKey = true; if (clearGeminiKey) { setSystemSetting('gemini_api_key', ''); } assert.equal(getSystemSetting('gemini_api_key'), ''); }); }); describe('Gas Station Location & GPS Service', () => { it('should calculate Haversine distance in meters accurately', () => { // Distance between two points in Steenwijk: ~1.8 km const meters = StationService.calculateDistanceMeters(52.788, 6.121, 52.8037052, 6.1287276); assert.ok(meters > 1700 && meters < 1950, `Expected ~1822m, got ${meters}`); }); it('should format distance cleanly in meters and kilometers', () => { assert.equal(StationService.formatDistance(120), '120 m'); assert.equal(StationService.formatDistance(999), '999 m'); assert.equal(StationService.formatDistance(1822), '1.8 km'); assert.equal(StationService.formatDistance(12400), '12.4 km'); }); it('should store and retrieve station location details on fuel entries', () => { const fuelId = crypto.randomUUID(); db.run( `INSERT INTO fuel_entries ( id, vehicle_id, date, odometer, liters, price_per_liter, total_cost, is_full_tank, missed_previous, notes, station_name, station_address, latitude, longitude, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ fuelId, testVehicleId, '2026-10-27', 65000, 40.89, 2.079, 85.01, 1, 0, 'Regular fill-up', 'Fieten Olie Steenwijk', 'Eeser Gaard 2, 8332 VN Steenwijk', 52.8037, 6.1287, new Date().toISOString(), ] ); const entry = db.get('SELECT * FROM fuel_entries WHERE id = ?', [fuelId]); assert.equal(entry.station_name, 'Fieten Olie Steenwijk'); assert.equal(entry.station_address, 'Eeser Gaard 2, 8332 VN Steenwijk'); assert.equal(entry.latitude, 52.8037); assert.equal(entry.longitude, 6.1287); // Test updating the fuel entry db.run( `UPDATE fuel_entries SET date = ?, odometer = ?, liters = ?, price_per_liter = ?, total_cost = ?, notes = ? WHERE id = ?`, ['2026-10-28', 65100, 42.00, 2.100, 88.20, 'Updated notes', fuelId] ); const updated = db.get('SELECT * FROM fuel_entries WHERE id = ?', [fuelId]); assert.equal(updated.date, '2026-10-28'); assert.equal(updated.odometer, 65100); assert.equal(updated.liters, 42.00); assert.equal(updated.price_per_liter, 2.100); assert.equal(updated.total_cost, 88.20); assert.equal(updated.notes, 'Updated notes'); db.run('DELETE FROM fuel_entries WHERE id = ?', [fuelId]); }); it('should extract Dutch station address and format brand names from receipt text', () => { // 1. Exact Fieten Olie receipt header test const receiptText = `FIETEN OLIE\nEeser Gaard 2\n8332 VN Steenwijk\nTelefoon:(0528) 34 12 93\nDatum 19-10-2026`; const extractedAddr = extractDutchAddress(receiptText); assert.equal(extractedAddr, 'Eeser Gaard 2, 8332 VN Steenwijk'); // 2. Format station brand name assert.equal(formatStationName('FIETEN OLIE'), 'Fieten Olie'); assert.equal(formatStationName('SHELL EXPRESS'), 'Shell Express'); assert.equal(formatStationName('BP'), 'BP'); assert.equal(formatStationName('ESSO'), 'ESSO'); // 3. Format address casing keeping postal code uppercase assert.equal(formatAddress('EESER GAARD 2, 8332 VN STEENWIJK'), 'Eeser Gaard 2, 8332 VN Steenwijk'); // 4. normalizeAiResult extracting address when combined in station or raw JSON const aiJson = JSON.stringify({ date: '2026-10-19', liters: 37.41, pricePerLiter: 2.159, totalCost: 80.77, station: 'FIETEN OLIE, Eeser Gaard 2, 8332 VN Steenwijk', fuelType: '95 Ongelood', }); const parsed = AiReceiptService.normalizeAiResult(aiJson, 'gemini', 'gemini-3.8-flash'); assert.equal(parsed.station, 'Fieten Olie'); assert.equal(parsed.stationAddress, 'Eeser Gaard 2, 8332 VN Steenwijk'); assert.equal(parsed.liters, 37.41); assert.equal(parsed.totalCost, 80.77); }); it('should filter available Gemini models to only multimodal text/vision models', () => { const rawApiModels = [ 'gemini-3.8-flash-lite-tts', 'gemini-3.8-flash-tts', 'gemini-3.8-flash', 'gemini-2.5-flash-image', 'gemini-2.5-flash-lite', 'gemini-2.5-flash', 'gemini-2.0-flash', 'gemini-1.5-flash', 'gemini-1.5-pro', 'gemini-3.1-flash-lite-preview', 'embedding-001', 'text-bison-001', ]; const filtered = filterVisionModels(rawApiModels); // Must include valid vision models assert.ok(filtered.includes('gemini-3.8-flash')); assert.ok(filtered.includes('gemini-2.5-flash')); assert.ok(filtered.includes('gemini-2.5-flash-lite')); assert.ok(filtered.includes('gemini-3.1-flash-lite-preview')); // Must exclude specialized TTS, image gen, embedding, and retired models assert.ok(!filtered.includes('gemini-3.8-flash-lite-tts')); assert.ok(!filtered.includes('gemini-3.8-flash-tts')); assert.ok(!filtered.includes('gemini-2.5-flash-image')); assert.ok(!filtered.includes('gemini-2.0-flash')); assert.ok(!filtered.includes('gemini-1.5-flash')); assert.ok(!filtered.includes('gemini-1.5-pro')); assert.ok(!filtered.includes('embedding-001')); assert.ok(!filtered.includes('text-bison-001')); // Preferred recommended model must be first assert.equal(filtered[0], 'gemini-3.8-flash'); }); it('should validate and handle receipt rescan requests via FuelController.rescanReceipt', async () => { // 1. Missing receipt returns 400 let statusCode = 200; let jsonPayload: any = null; const mockRes: any = { status: (code: number) => { statusCode = code; return mockRes; }, json: (data: any) => { jsonPayload = data; return mockRes; }, }; await FuelController.rescanReceipt({ params: {}, body: {} } as any, mockRes); assert.equal(statusCode, 400); assert.ok(jsonPayload.error); // 2. Non-existent receipt file returns 404 statusCode = 200; await FuelController.rescanReceipt({ params: {}, body: { receiptPath: 'nonexistent-receipt-9999.jpg' } } as any, mockRes); assert.equal(statusCode, 404); assert.ok(jsonPayload.error.includes('kon niet worden gevonden')); // 3. Existing receipt file is parsed safely const dummyFileName = `test-receipt-${Date.now()}.jpg`; const dummyFilePath = path.join(DATA_PATHS.receipts, dummyFileName); fs.writeFileSync(dummyFilePath, 'dummy test receipt content'); const origProcessReceipt = OcrService.processReceipt; OcrService.processReceipt = async () => ({ date: '2026-10-29', liters: 45.0, pricePerLiter: 1.959, totalCost: 88.16, station: 'Fieten Olie', rawText: 'Mock receipt', confidence: 95, engine: 'tesseract', }); try { statusCode = 200; jsonPayload = null; await FuelController.rescanReceipt({ params: {}, body: { receiptPath: dummyFileName } } as any, mockRes); assert.equal(statusCode, 200); assert.equal(jsonPayload.receiptPath, dummyFileName); assert.equal(jsonPayload.extracted.liters, 45.0); assert.equal(jsonPayload.extracted.station, 'Fieten Olie'); } finally { OcrService.processReceipt = origProcessReceipt; if (fs.existsSync(dummyFilePath)) { fs.unlinkSync(dummyFilePath); } } // 4. Test loading receipt_path from database fuel_entries via id param const fuelId = crypto.randomUUID(); const dbReceiptName = `db-receipt-${Date.now()}.jpg`; const dbReceiptPath = path.join(DATA_PATHS.receipts, dbReceiptName); fs.writeFileSync(dbReceiptPath, 'dummy'); db.run( `INSERT INTO fuel_entries (id, vehicle_id, date, odometer, liters, price_per_liter, total_cost, is_full_tank, missed_previous, receipt_path, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [fuelId, testVehicleId, '2026-10-29', 70000, 40, 2.0, 80, 1, 0, dbReceiptName, new Date().toISOString()] ); OcrService.processReceipt = async () => ({ date: '2026-10-29', liters: 40.0, pricePerLiter: 2.0, totalCost: 80.0, station: 'Shell', rawText: 'Mock', confidence: 98, engine: 'gemini', }); try { statusCode = 200; jsonPayload = null; await FuelController.rescanReceipt({ params: { id: fuelId }, body: {} } as any, mockRes); assert.equal(statusCode, 200); assert.equal(jsonPayload.receiptPath, dbReceiptName); assert.equal(jsonPayload.extracted.station, 'Shell'); } finally { OcrService.processReceipt = origProcessReceipt; db.run('DELETE FROM fuel_entries WHERE id = ?', [fuelId]); if (fs.existsSync(dbReceiptPath)) { fs.unlinkSync(dbReceiptPath); } } }); }); describe('Home Assistant REST Telemetry & Sensors Engine', () => { it('should generate sensor payload for active vehicles with accurate telemetry', () => { const payload = HaService.getSensorsPayload(); assert.equal(payload.status, 'ok'); assert.equal(payload.version, '2.0.0'); assert.ok(Array.isArray(payload.vehicles)); assert.ok(payload.total_vehicles >= 1); const target = payload.vehicles.find((v) => v.id === testVehicleId); assert.ok(target, 'Test vehicle must be present in HA sensor payload'); assert.equal(target.name, 'Daily Driver Golf 7'); assert.equal(target.status, 'active'); assert.ok(typeof target.odometer === 'number'); assert.ok(target.apk); assert.ok(target.fuel_stats); assert.ok(target.attributes); assert.equal(target.attributes.license_plate, '12-XYZ-3'); }); }); describe('Notification Engine & Dispatch Service', () => { it('should save and retrieve notification settings correctly', () => { NotificationService.saveSettings({ provider: 'ntfy', ntfyUrl: 'https://ntfy.sh/test-vault-channel', enabledApk: true, enabledMaintenance: false, }); const settings = NotificationService.getSettings(); assert.equal(settings.provider, 'ntfy'); assert.equal(settings.ntfyUrl, 'https://ntfy.sh/test-vault-channel'); assert.equal(settings.enabledApk, true); assert.equal(settings.enabledMaintenance, false); }); it('should handle test notification rejection when no provider is set gracefully', async () => { NotificationService.saveSettings({ provider: 'none' }); const res = await NotificationService.testNotification(); assert.equal(res.success, false); assert.ok(res.message.includes('uitgeschakeld')); }); it('should configure and persist SMTP settings including password masking', () => { EmailService.saveSmtpConfig({ host: 'smtp.example.com', port: 465, secure: true, user: 'vault@example.com', pass: 'supersecretpassword', from: 'VehicleVault ', to: 'admin@example.com', }); const smtp = EmailService.getSmtpConfig(); assert.equal(smtp.host, 'smtp.example.com'); assert.equal(smtp.port, 465); assert.equal(smtp.secure, true); assert.equal(smtp.user, 'vault@example.com'); assert.equal(smtp.pass, 'supersecretpassword'); assert.equal(smtp.to, 'admin@example.com'); // Saving masked pass must not overwrite real password EmailService.saveSmtpConfig({ pass: '••••••••', host: 'smtp.example.com', }); const smtpUpdated = EmailService.getSmtpConfig(); assert.equal(smtpUpdated.pass, 'supersecretpassword'); }); it('should handle email notification provider dispatch correctly', async () => { NotificationService.saveSettings({ provider: 'email', smtpHost: 'smtp.invalid.local', smtpTo: 'recipient@example.com', }); const settings = NotificationService.getSettings(); assert.equal(settings.provider, 'email'); assert.equal(settings.smtpHost, 'smtp.invalid.local'); assert.equal(settings.smtpTo, 'recipient@example.com'); }); it('should configure, persist, and validate Pushover notification settings', async () => { NotificationService.saveSettings({ provider: 'pushover', pushoverUserKey: 'uQiRzpo4DXghDmr9QzzfQu27cmVRsG', pushoverApiToken: 'azGDORePK8gMaC0QOYAMyEEuzJnyUi', pushoverDevice: 'iphone', }); const settings = NotificationService.getSettings(); assert.equal(settings.provider, 'pushover'); assert.equal(settings.pushoverUserKey, 'uQiRzpo4DXghDmr9QzzfQu27cmVRsG'); assert.equal(settings.pushoverApiToken, 'azGDORePK8gMaC0QOYAMyEEuzJnyUi'); assert.equal(settings.pushoverDevice, 'iphone'); // Validate missing keys rejection NotificationService.saveSettings({ provider: 'pushover', pushoverUserKey: '', pushoverApiToken: '', }); const res = await NotificationService.send('Test', 'Bericht'); assert.equal(res.success, false); assert.ok(res.message.includes('Pushover vereist')); }); }); describe('Password Reset & Forgotten Password Flow', () => { it('should create a valid reset token and verify it correctly', async () => { const resetUserEmail = `reset_${crypto.randomUUID().slice(0, 8)}@example.com`; const hash = await hashPassword('InitialPass123!'); const userId = crypto.randomUUID(); const now = new Date().toISOString(); try { db.run( 'INSERT INTO users (id, email, password_hash, role, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)', [userId, resetUserEmail, hash, 'user', now, now] ); // 1. Request forgot password let forgotResData: any = null; await AuthController.forgotPassword( { body: { email: resetUserEmail }, headers: { host: 'localhost:3084' }, protocol: 'http' } as any, { json: (d: any) => { forgotResData = d; } } as any ); assert.ok(forgotResData); assert.equal(forgotResData.success, true); assert.ok(forgotResData.resetLink); assert.ok(forgotResData.resetLink.includes('/reset-password?token=')); const tokenMatch = forgotResData.resetLink.match(/token=([a-f0-9]+)/); assert.ok(tokenMatch); const rawToken = tokenMatch[1]; // 2. Verify reset token let verifyResData: any = null; let verifyStatusCode = 200; await AuthController.verifyResetToken( { query: { token: rawToken } } as any, { json: (d: any) => { verifyResData = d; }, status: (code: number) => { verifyStatusCode = code; return { json: (d: any) => { verifyResData = d; } }; }, } as any ); assert.equal(verifyStatusCode, 200); assert.ok(verifyResData.valid); assert.equal(verifyResData.email, resetUserEmail); // 3. Reset password let resetResData: any = null; await AuthController.resetPassword( { body: { token: rawToken, newPassword: 'NewSecurePassword456!' } } as any, { json: (d: any) => { resetResData = d; }, status: (code: number) => { return { json: (d: any) => { resetResData = d; } }; }, } as any ); assert.ok(resetResData); assert.equal(resetResData.success, true); // 4. Verify user can now authenticate with the new password const updatedUser = db.get('SELECT password_hash FROM users WHERE id = ?', [userId]); const passwordMatches = await comparePassword('NewSecurePassword456!', updatedUser.password_hash); assert.equal(passwordMatches, true); // 5. Verify token cannot be reused let reuseResData: any = null; let reuseStatusCode = 200; await AuthController.verifyResetToken( { query: { token: rawToken } } as any, { json: (d: any) => { reuseResData = d; }, status: (code: number) => { reuseStatusCode = code; return { json: (d: any) => { reuseResData = d; } }; }, } as any ); assert.equal(reuseStatusCode, 400); assert.equal(reuseResData.valid, false); } finally { db.run('DELETE FROM users WHERE id = ?', [userId]); } }); it('should allow admin to generate password reset links for managed users', async () => { const adminTargetEmail = `admintarget_${crypto.randomUUID().slice(0, 8)}@example.com`; const hash = await hashPassword('InitialPass123!'); const userId = crypto.randomUUID(); const now = new Date().toISOString(); try { db.run( 'INSERT INTO users (id, email, password_hash, role, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)', [userId, adminTargetEmail, hash, 'driver', now, now] ); let resData: any = null; await AuthController.generateUserResetLink( { params: { id: userId }, body: { sendEmail: false }, headers: { host: 'localhost:3084' }, protocol: 'http' } as any, { json: (d: any) => { resData = d; } } as any ); assert.ok(resData); assert.equal(resData.success, true); assert.ok(resData.resetLink); assert.ok(resData.resetLink.includes('/reset-password?token=')); } finally { db.run('DELETE FROM users WHERE id = ?', [userId]); } }); }); describe('1-Click Database & File Backup/Restore Engine', () => { it('should generate valid zip archive containing vehiclevault.db and manifest.json', async () => { const zipBuffer = await BackupService.createBackupZip(); assert.ok(zipBuffer.length > 0); const stats = BackupService.getStorageStats(); assert.ok(typeof stats.databaseSizeFormatted === 'string'); assert.ok(typeof stats.totalUploadsFormatted === 'string'); assert.ok(typeof stats.dataDir === 'string'); }); }); describe('PHEV/EV Energy Logging & Multi-User Roles', () => { it('should record electric charging sessions with kWh and price per kWh', () => { const fuelId = crypto.randomUUID(); db.run( `INSERT INTO fuel_entries ( id, vehicle_id, date, odometer, liters, price_per_liter, total_cost, energy_type, kwh, price_per_kwh, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ fuelId, testVehicleId, '2026-10-29', 16500, 0, 0, 15.82, 'electric', 45.2, 0.35, new Date().toISOString(), ] ); const entry = db.get('SELECT * FROM fuel_entries WHERE id = ?', [fuelId]); assert.ok(entry); assert.equal(entry.energy_type, 'electric'); assert.equal(entry.kwh, 45.2); assert.equal(entry.price_per_kwh, 0.35); assert.equal(entry.total_cost, 15.82); }); it('should support driver and admin roles properly', () => { const driverUserId = crypto.randomUUID(); const uniqueEmail = `driver_${crypto.randomUUID()}@vehiclevault.local`; const now = new Date().toISOString(); db.run( 'INSERT INTO users (id, email, password_hash, role, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)', [driverUserId, uniqueEmail, 'fakehash', 'driver', now, now] ); const user = db.get('SELECT role FROM users WHERE id = ?', [driverUserId]); assert.equal(user.role, 'driver'); }); }); describe('Recurring Expenses & Fixed Costs TCO Engine', () => { it('should calculate monthly normalized recurring costs accurately', () => { const exp1Id = crypto.randomUUID(); const exp2Id = crypto.randomUUID(); const exp3Id = crypto.randomUUID(); const now = new Date().toISOString(); // Road tax: 65 / month db.run( `INSERT INTO recurring_expenses (id, vehicle_id, name, category, frequency, amount, start_date, is_active, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?)`, [exp1Id, testVehicleId, 'Motorrijtuigenbelasting (MRB)', 'tax', 'monthly', 65.0, '2026-01-01', now] ); // Insurance: 720 / year = 60 / month db.run( `INSERT INTO recurring_expenses (id, vehicle_id, name, category, frequency, amount, start_date, is_active, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?)`, [exp2Id, testVehicleId, 'All-risk Autoverzekering', 'insurance', 'yearly', 720.0, '2026-01-01', now] ); // Roadside assistance: 30 / quarter = 10 / month db.run( `INSERT INTO recurring_expenses (id, vehicle_id, name, category, frequency, amount, start_date, is_active, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?)`, [exp3Id, testVehicleId, 'ANWB Wegenwacht', 'roadside', 'quarterly', 30.0, '2026-01-01', now] ); const expenses = db.query('SELECT * FROM recurring_expenses WHERE vehicle_id = ? AND is_active = 1', [testVehicleId]); let monthlyTotal = 0; for (const e of expenses) { if (e.frequency === 'monthly') monthlyTotal += e.amount; else if (e.frequency === 'quarterly') monthlyTotal += e.amount / 3; else if (e.frequency === 'yearly') monthlyTotal += e.amount / 12; } assert.equal(monthlyTotal, 135.0); // 65 + 60 + 10 = 135 // TCO service calculation const tco = TcoService.calculateTco({ vehicleId: testVehicleId, includeFixedExpenses: true }); assert.equal(tco.monthlyFixedCost, 135.0); assert.ok(tco.fixedExpensesTotal >= 135.0); assert.ok(tco.totalCost >= tco.fixedExpensesTotal); // TCO service calculation without fixed expenses const tcoWithoutFixed = TcoService.calculateTco({ vehicleId: testVehicleId, includeFixedExpenses: false }); assert.equal(tcoWithoutFixed.includeFixedExpenses, false); assert.equal(tcoWithoutFixed.totalCost, Math.round((tco.totalCost - tco.fixedExpensesTotal) * 100) / 100); }); }); describe('Trip Logging & Belastingdienst CSV Export Engine', () => { it('should record trips, calculate distances and summarize business vs private km', () => { const trip1Id = crypto.randomUUID(); const trip2Id = crypto.randomUUID(); const now = new Date().toISOString(); // Trip 1: Business trip, 16500 to 16560 = 60km db.run( `INSERT INTO trips (id, vehicle_id, user_id, date, start_odometer, end_odometer, distance_km, from_location, to_location, purpose, description, is_return_trip, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`, [trip1Id, testVehicleId, testUserId, '2026-10-20', 16500, 16560, 60.0, 'Amsterdam', 'Rotterdam', 'business', 'Klantafspraak', now] ); // Trip 2: Private trip, 16560 to 16585 = 25km db.run( `INSERT INTO trips (id, vehicle_id, user_id, date, start_odometer, end_odometer, distance_km, from_location, to_location, purpose, description, is_return_trip, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`, [trip2Id, testVehicleId, testUserId, '2026-10-21', 16560, 16585, 25.0, 'Rotterdam', 'Breda', 'private', 'Familiebezoek', now] ); const trips = db.query('SELECT * FROM trips WHERE vehicle_id = ? ORDER BY date ASC', [testVehicleId]); assert.ok(trips.length >= 2); let totalDist = 0; let businessDist = 0; let privateDist = 0; for (const t of trips) { totalDist += Number(t.distance_km); if (t.purpose === 'business') businessDist += Number(t.distance_km); if (t.purpose === 'private') privateDist += Number(t.distance_km); } assert.ok(totalDist >= 85.0); assert.ok(businessDist >= 60.0); assert.ok(privateDist >= 25.0); }); it('should format Belastingdienst-compliant CSV export with correct headers and semicolon delimiter', () => { const trips = db.query('SELECT * FROM trips WHERE vehicle_id = ? ORDER BY date DESC', [testVehicleId]); assert.ok(trips.length > 0); const headers = [ 'Datum', 'Vertrekadres', 'Aankomstadres', 'Beginstand (km)', 'Eindstand (km)', 'Afstand (km)', 'Karakter rit', 'Toelichting/Omschrijving', 'Terugrit', ]; const csvLines = [headers.join(';')]; for (const t of trips) { csvLines.push( [ t.date, `"${(t.from_location || '').replace(/"/g, '""')}"`, `"${(t.to_location || '').replace(/"/g, '""')}"`, t.start_odometer, t.end_odometer, t.distance_km, t.purpose === 'business' ? 'Zakelijk' : t.purpose === 'commute' ? 'Woon-werk' : 'Prive', `"${(t.description || '').replace(/"/g, '""')}"`, t.is_return_trip ? 'Ja' : 'Nee', ].join(';') ); } const csvContent = '\uFEFF' + csvLines.join('\r\n'); assert.ok(csvContent.startsWith('\uFEFFDatum;Vertrekadres;Aankomstadres')); assert.ok(csvContent.includes('"Amsterdam";"Rotterdam"')); assert.ok(csvContent.includes('Zakelijk')); }); it('should generate an Excel (.xlsx) workbook for trips with summary metrics and formulas', async () => { const trips = db.query('SELECT * FROM trips WHERE vehicle_id = ? ORDER BY date DESC', [testVehicleId]); const vehicle = db.get('SELECT * FROM vehicles WHERE id = ?', [testVehicleId]); const buffer = await TripExportService.generateExcel(vehicle, trips, '2026'); assert.ok(Buffer.isBuffer(buffer)); assert.ok(buffer.length > 1000); assert.equal(buffer[0], 0x50); // 'P' assert.equal(buffer[1], 0x4b); // 'K' assert.equal(buffer[2], 0x03); assert.equal(buffer[3], 0x04); }); it('should generate a PDF fiscal dossier (landscape A4) for trips', async () => { const trips = db.query('SELECT * FROM trips WHERE vehicle_id = ? ORDER BY date DESC', [testVehicleId]); const vehicle = db.get('SELECT * FROM vehicles WHERE id = ?', [testVehicleId]); const buffer = await TripExportService.generatePdf(vehicle, trips, '2026'); assert.ok(Buffer.isBuffer(buffer)); assert.ok(buffer.length > 1000); const headerStr = buffer.subarray(0, 5).toString('utf-8'); assert.equal(headerStr, '%PDF-'); }); }); describe('Tire Set Management & Safety Limit Alerts Engine', () => { it('should detect tread depth warnings and legal minimum alerts correctly', () => { const summerSetId = crypto.randomUUID(); const winterSetId = crypto.randomUUID(); const wornSetId = crypto.randomUUID(); const now = new Date().toISOString(); // Good Summer Set (6.5mm) db.run( `INSERT INTO tire_sets (id, vehicle_id, name, season, brand_model, tire_size, status, tread_depth_fl, tread_depth_fr, tread_depth_rl, tread_depth_rr, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [summerSetId, testVehicleId, 'Zomerset 18 inch', 'summer', 'Michelin Pilot Sport 5', '225/40 R18', 'mounted', 6.5, 6.5, 6.8, 6.8, now] ); // Winter Set with 3.5mm tread (< 4.0mm warning for winter tires) db.run( `INSERT INTO tire_sets (id, vehicle_id, name, season, brand_model, tire_size, status, tread_depth_fl, tread_depth_fr, tread_depth_rl, tread_depth_rr, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [winterSetId, testVehicleId, 'Winterset 16 inch', 'winter', 'Continental WinterContact TS 870', '205/55 R16', 'stored', 3.5, 3.6, 3.8, 3.8, now] ); // Worn Set with 1.4mm tread (< 1.6mm legal rejection) db.run( `INSERT INTO tire_sets (id, vehicle_id, name, season, brand_model, tire_size, status, tread_depth_fl, tread_depth_fr, tread_depth_rl, tread_depth_rr, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [wornSetId, testVehicleId, 'Oude Bandenset', 'summer', 'Budget Tire', '205/55 R16', 'stored', 1.4, 1.5, 2.0, 2.1, now] ); function evaluateStatus(set: any): 'ok' | 'warning' | 'critical' { const depths = [set.tread_depth_fl, set.tread_depth_fr, set.tread_depth_rl, set.tread_depth_rr].filter((d: any) => d != null); const min = Math.min(...depths); if (min < 1.6) return 'critical'; if (set.season === 'winter' && min < 4.0) return 'warning'; if (min < 2.5) return 'warning'; return 'ok'; } const summer = db.get('SELECT * FROM tire_sets WHERE id = ?', [summerSetId]); const winter = db.get('SELECT * FROM tire_sets WHERE id = ?', [winterSetId]); const worn = db.get('SELECT * FROM tire_sets WHERE id = ?', [wornSetId]); assert.equal(evaluateStatus(summer), 'ok'); assert.equal(evaluateStatus(winter), 'warning'); assert.equal(evaluateStatus(worn), 'critical'); }); it('should calculate mileage delta when swapping tire sets', () => { const setAId = crypto.randomUUID(); const setBId = crypto.randomUUID(); const now = new Date().toISOString(); // Set A mounted at 10,000 km db.run( `INSERT INTO tire_sets (id, vehicle_id, name, season, status, mounted_odometer, total_km, created_at) VALUES (?, ?, ?, ?, 'mounted', 10000, 5000, ?)`, [setAId, testVehicleId, 'Set A', 'summer', now] ); // Set B currently stored with 3,000 km db.run( `INSERT INTO tire_sets (id, vehicle_id, name, season, status, mounted_odometer, total_km, created_at) VALUES (?, ?, ?, ?, 'stored', NULL, 3000, ?)`, [setBId, testVehicleId, 'Set B', 'winter', now] ); // Swap at odometer 18,000 km: Set A ran (18000 - 10000) = 8000 km. Set A total_km becomes 5000 + 8000 = 13000 km. const swapOdometer = 18000; const currentMounted = db.get("SELECT * FROM tire_sets WHERE id = ?", [setAId]); assert.ok(currentMounted); const delta = swapOdometer - currentMounted.mounted_odometer; const newTotal = currentMounted.total_km + delta; db.run( "UPDATE tire_sets SET status = 'stored', total_km = ?, mounted_odometer = NULL WHERE id = ?", [newTotal, currentMounted.id] ); db.run( "UPDATE tire_sets SET status = 'mounted', mounted_odometer = ? WHERE id = ?", [swapOdometer, setBId] ); const updatedA = db.get('SELECT * FROM tire_sets WHERE id = ?', [currentMounted.id]); const updatedB = db.get('SELECT * FROM tire_sets WHERE id = ?', [setBId]); assert.equal(updatedA.status, 'stored'); assert.equal(updatedA.total_km, 13000); assert.equal(updatedB.status, 'mounted'); assert.equal(updatedB.mounted_odometer, 18000); }); it('should support separate brand, model, rim size, and load/speed index (e.g. 92V)', () => { const setId = crypto.randomUUID(); const now = new Date().toISOString(); db.run( `INSERT INTO tire_sets ( id, vehicle_id, name, season, brand, model, brand_model, tire_size, rim_size, speed_index, status, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [setId, testVehicleId, 'Zomerset 17 inch', 'summer', 'Continental', 'PremiumContact 7', 'Continental PremiumContact 7', '225/45', '17"', '92V', 'mounted', now] ); const record = db.get('SELECT * FROM tire_sets WHERE id = ?', [setId]); assert.ok(record); assert.equal(record.brand, 'Continental'); assert.equal(record.model, 'PremiumContact 7'); assert.equal(record.rim_size, '17"'); assert.equal(record.speed_index, '92V'); assert.equal(record.brand_model, 'Continental PremiumContact 7'); }); it('should safely unmount previous mounted tire set when creating a new mounted set without SQL errors', () => { // Simulate TireController.create logic when status = 'mounted' db.run("UPDATE tire_sets SET status = 'stored' WHERE vehicle_id = ? AND status = 'mounted'", [testVehicleId]); const newMountedId = crypto.randomUUID(); const now = new Date().toISOString(); db.run( `INSERT INTO tire_sets ( id, vehicle_id, name, season, status, created_at ) VALUES (?, ?, ?, ?, 'mounted', ?)`, [newMountedId, testVehicleId, 'Nieuwe Set', 'all_season', now] ); const mounted = db.get("SELECT * FROM tire_sets WHERE vehicle_id = ? AND status = 'mounted'", [testVehicleId]); assert.ok(mounted); assert.equal(mounted.id, newMountedId); }); it('should allow editing total_km and mounted_odometer and compute dynamic live mileage', () => { const setId = crypto.randomUUID(); const now = new Date().toISOString(); // User creates or edits a tire set with 15,000 km historical mileage, mounted at 67,799 km db.run( `INSERT INTO tire_sets ( id, vehicle_id, name, season, brand, model, brand_model, status, mounted_odometer, total_km, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, 'mounted', 67799, 15000, ?)`, [setId, testVehicleId, 'Bridgestone Turanza', 'all_season', 'Bridgestone', 'Turanza 6', 'Bridgestone Turanza 6', now] ); // Verify direct edit update db.run( `UPDATE tire_sets SET total_km = ?, mounted_odometer = ? WHERE id = ?`, [18500, 67000, setId] ); const updated = db.get('SELECT * FROM tire_sets WHERE id = ?', [setId]); assert.equal(updated.total_km, 18500); assert.equal(updated.mounted_odometer, 67000); // Simulate live vehicle odometer at 68,799 km: delta is 68,799 - 67,000 = 1,799 km const vehicleCurrentOdo = 68799; const delta = Math.max(0, vehicleCurrentOdo - updated.mounted_odometer); const computedCurrentKm = updated.total_km + delta; assert.equal(delta, 1799); assert.equal(computedCurrentKm, 20299); }); }); describe('Fuel Fill-up Export & Module Toggle Configuration Engine', () => { it('should generate a formatted Excel (.xlsx) workbook for fuel entries', async () => { const vehicle = { name: 'Test Wagen', license_plate: 'AA-123-B', make: 'Volkswagen', model: 'Golf', fuel_type: 'Benzine', initial_odometer: 10000, }; const entries = [ { id: crypto.randomUUID(), date: '2026-05-01', odometer: 10500, liters: 45.2, price_per_liter: 1.959, total_cost: 88.55, is_full_tank: 1, missed_previous: 0, station_name: 'Shell Express', station_address: 'Rijksweg 1, Utrecht', energy_type: 'fuel', notes: 'Fijne tankbeurt', }, { id: crypto.randomUUID(), date: '2026-05-15', odometer: 11100, liters: 42.8, price_per_liter: 1.929, total_cost: 82.56, is_full_tank: 1, missed_previous: 0, station_name: 'BP Station', station_address: 'Dorpsstraat 10, Breukelen', energy_type: 'fuel', }, ]; const buffer = await FuelExportService.generateExcel(vehicle, entries); assert.ok(Buffer.isBuffer(buffer)); assert.ok(buffer.length > 1000); // Valid ZIP/Excel header PK\x03\x04 assert.equal(buffer[0], 0x50); assert.equal(buffer[1], 0x4b); assert.equal(buffer[2], 0x03); assert.equal(buffer[3], 0x04); }); it('should generate a UTF-8 BOM CSV with semicolon delimiter for fuel entries', () => { const vehicle = { name: 'Test Wagen', license_plate: 'AA-123-B', initial_odometer: 10000, }; const entries = [ { id: crypto.randomUUID(), date: '2026-05-01', odometer: 10500, liters: 45.2, price_per_liter: 1.959, total_cost: 88.55, is_full_tank: 1, missed_previous: 0, station_name: 'Shell Express', station_address: 'Rijksweg 1', energy_type: 'fuel', notes: 'Bon bewaard', }, ]; const buffer = FuelExportService.generateCsv(vehicle, entries); assert.ok(Buffer.isBuffer(buffer)); const text = buffer.toString('utf-8'); assert.ok(text.startsWith('\uFEFFDatum;Kilometerstand;Afstand (km);Liters/kWh;Type;Prijs per Eenheid;Totaalbedrag;Volle Tank;Tankstation;Adres;Notities')); assert.ok(text.includes('10500;500;45,20;Brandstof;1,959;88,55;Ja;"Shell Express";"Rijksweg 1";"Bon bewaard"')); }); it('should persist and retrieve module toggle settings correctly', () => { const defaultSetting = getSystemSetting('enabled_modules'); // If not yet set, should return null // Now save customized configuration const customConfig = { fuel: true, trips: false, maintenance: true, tires: false, fixedExpenses: true, documents: true, analytics: true, }; setSystemSetting('enabled_modules', JSON.stringify(customConfig)); const retrieved = JSON.parse(getSystemSetting('enabled_modules') || '{}'); assert.equal(retrieved.trips, false); assert.equal(retrieved.tires, false); assert.equal(retrieved.fuel, true); assert.equal(retrieved.fixedExpenses, true); // Clean up back to all enabled or original if (defaultSetting) { setSystemSetting('enabled_modules', defaultSetting); } else { setSystemSetting( 'enabled_modules', JSON.stringify({ fuel: true, trips: true, maintenance: true, tires: true, fixedExpenses: true, documents: true, analytics: true, }) ); } }); }); describe('Multi-User Vehicle Isolation & Vehicle Sharing Engine', () => { it('should isolate vehicles between distinct users and respect sharing permissions', () => { const userSon = crypto.randomUUID(); const userFather = crypto.randomUUID(); const userWife = crypto.randomUUID(); const now = new Date().toISOString(); db.run('INSERT INTO users (id, email, password_hash, role, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)', [ userSon, `son_${crypto.randomUUID()}@vault.local`, 'hash', 'admin', now, now, ]); db.run('INSERT INTO users (id, email, password_hash, role, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)', [ userFather, `father_${crypto.randomUUID()}@vault.local`, 'hash', 'user', now, now, ]); db.run('INSERT INTO users (id, email, password_hash, role, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)', [ userWife, `wife_${crypto.randomUUID()}@vault.local`, 'hash', 'user', now, now, ]); const sonCarId = crypto.randomUUID(); const fatherCarId = crypto.randomUUID(); // Son owns Hatchback db.run( `INSERT INTO vehicles (id, user_id, name, type, license_plate, created_at, updated_at) VALUES (?, ?, ?, 'car', 'GBB-01-B', ?, ?)`, [sonCarId, userSon, 'Volkswagen Golf', now, now] ); // Father owns Mercedes B180 db.run( `INSERT INTO vehicles (id, user_id, name, type, license_plate, created_at, updated_at) VALUES (?, ?, ?, 'car', 'NK-956-Z', ?, ?)`, [fatherCarId, userFather, 'Mercedes-Benz B 180', now, now] ); // 1. Initial access check: Son can access sonCar, Father can access fatherCar const sonAccessOwn = getVehicleAccess(sonCarId, userSon); assert.equal(sonAccessOwn.hasAccess, true); assert.equal(sonAccessOwn.isOwner, true); assert.equal(sonAccessOwn.role, 'owner'); const fatherAccessSonCar = getVehicleAccess(sonCarId, userFather); assert.equal(fatherAccessSonCar.hasAccess, false); const sonAccessFatherCar = getVehicleAccess(fatherCarId, userSon); assert.equal(sonAccessFatherCar.hasAccess, false); const wifeAccessSonCarInitial = getVehicleAccess(sonCarId, userWife); assert.equal(wifeAccessSonCarInitial.hasAccess, false); // 2. Son shares Kia Niro with his wife as editor const shareId = crypto.randomUUID(); db.run( `INSERT INTO vehicle_shares (id, vehicle_id, user_id, role, created_at) VALUES (?, ?, ?, 'editor', ?)`, [shareId, sonCarId, userWife, now] ); // Wife now has access as editor const wifeAccessSonCar = getVehicleAccess(sonCarId, userWife); assert.equal(wifeAccessSonCar.hasAccess, true); assert.equal(wifeAccessSonCar.isOwner, false); assert.equal(wifeAccessSonCar.role, 'editor'); // Wife still has NO access to father's Mercedes const wifeAccessFatherCar = getVehicleAccess(fatherCarId, userWife); assert.equal(wifeAccessFatherCar.hasAccess, false); // Father still has NO access to son's Kia Niro const fatherAccessSonCarStill = getVehicleAccess(sonCarId, userFather); assert.equal(fatherAccessSonCarStill.hasAccess, false); // 3. Revoke share from wife db.run('DELETE FROM vehicle_shares WHERE id = ?', [shareId]); const wifeAccessAfterRevoke = getVehicleAccess(sonCarId, userWife); assert.equal(wifeAccessAfterRevoke.hasAccess, false); }); it('should issue long-lived persistent access tokens for session retention', () => { const payload = { userId: 'u1', email: 'test@local', role: 'user' }; const token = generateAccessToken(payload); assert.ok(token); const verified = verifyAccessToken(token); assert.ok(verified); assert.equal(verified?.userId, 'u1'); assert.equal(verified?.email, 'test@local'); const refresh = generateRefreshToken(90); assert.ok(refresh.token); assert.ok(refresh.expiresAt); const expiresDate = new Date(refresh.expiresAt); const daysAhead = Math.round((expiresDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)); assert.ok(daysAhead >= 88 && daysAhead <= 91); }); it('should aggregate last login and last activity per user including dormant detection', async () => { const dummyId = crypto.randomUUID(); const testEmail = `audit_${dummyId.slice(0, 8)}@example.com`; const now = new Date().toISOString(); const loginTime = '2026-10-29T10:00:00.000Z'; const changeTime = '2026-10-29T11:30:00.000Z'; try { db.run( `INSERT INTO users (id, email, password_hash, role, last_login_at, last_active_at, created_at, updated_at) VALUES (?, ?, 'hash', 'user', ?, ?, ?, ?)`, [dummyId, testEmail, loginTime, changeTime, now, now] ); let responseData: any = null; const fakeRes: any = { json: (data: any) => { responseData = data; } }; await AuthController.listUsers({} as any, fakeRes); assert.ok(responseData); assert.ok(Array.isArray(responseData.users)); const found = responseData.users.find((u: any) => u.id === dummyId); assert.ok(found); assert.equal(found.email, testEmail); assert.equal(found.last_login_at, loginTime); assert.equal(found.last_active_at, changeTime); assert.equal(found.is_dormant, false); } finally { // Clean up db.run('DELETE FROM users WHERE id = ?', [dummyId]); } }); }); describe('Showcase & Digital Sales Dossier Engine (Digital Sales Dossier standard)', () => { it('should auto-populate a comprehensive showcase dossier with RDW specs and stats', async () => { const showcase = ShowcaseService.getOrInitShowcase(testVehicleId); assert.ok(showcase); assert.ok(showcase.public_slug); assert.equal(showcase.is_active, true); assert.ok(showcase.stats); assert.ok(showcase.specs); assert.ok(showcase.specs.basis.length > 0); assert.ok(showcase.highlights.length > 0); assert.ok(showcase.options_list.length > 0); assert.ok(showcase.transparency_notes); }); it('should support updating showcase pricing, contacts, highlights, and custom slug', () => { const updated = ShowcaseService.updateShowcase(testVehicleId, { asking_price: 12500, price_label: 'Vaste meeneemprijs', contact_whatsapp: '+31612345678', contact_location: 'Groningen', public_slug: 'golf-gti-special-edition', }); assert.ok(updated); assert.equal(updated.asking_price, 12500); assert.equal(updated.price_label, 'Vaste meeneemprijs'); assert.equal(updated.contact_whatsapp, '+31612345678'); assert.equal(updated.contact_location, 'Groningen'); assert.equal(updated.public_slug, 'golf-gti-special-edition'); // Verify public retrieval by custom slug const publicShowcase = ShowcaseService.getPublicShowcase('golf-gti-special-edition'); assert.ok(publicShowcase); assert.equal(publicShowcase.asking_price, 12500); // Support 'Geen prijs tonen' to omit price badge (ideal for dealer appraisal/trade-in) const noPriceShowcase = ShowcaseService.updateShowcase(testVehicleId, { asking_price: null, price_label: 'Geen prijs tonen', }); assert.ok(noPriceShowcase); assert.equal(noPriceShowcase.price_label, 'Geen prijs tonen'); assert.equal(noPriceShowcase.asking_price, null); }); it('should generate a standalone static website ZIP archive containing index.html, styles.css, app.js and images', async () => { const showcase = ShowcaseService.getOrInitShowcase(testVehicleId); assert.ok(showcase); const zipBuffer = await ShowcaseZipService.generateZip(showcase); assert.ok(zipBuffer); assert.ok(zipBuffer.length > 500); // Verify ZIP magic header PK\x03\x04 assert.equal(zipBuffer[0], 0x50); assert.equal(zipBuffer[1], 0x4b); assert.equal(zipBuffer[2], 0x03); assert.equal(zipBuffer[3], 0x04); }); it('should handle multi-photo gallery categorizing and primary photo synchronization', () => { const photoId1 = crypto.randomUUID(); const photoId2 = crypto.randomUUID(); const now = new Date().toISOString(); // Insert exterior photo db.run( `INSERT INTO vehicle_photos (id, vehicle_id, file_path, category, title, sort_order, is_primary, created_at) VALUES (?, ?, ?, 'exterieur', 'Voorkant', 1, 1, ?)`, [photoId1, testVehicleId, 'ext-front.jpg', now] ); // Insert damage / transparency photo db.run( `INSERT INTO vehicle_photos (id, vehicle_id, file_path, category, title, sort_order, is_primary, created_at) VALUES (?, ?, ?, 'gebruikssporen', 'Krasje achterbumper', 2, 0, ?)`, [photoId2, testVehicleId, 'damage-rear.jpg', now] ); const showcase = ShowcaseService.getOrInitShowcase(testVehicleId); assert.ok(showcase); assert.ok(showcase.photos.length >= 2); const ext = showcase.photos.find(p => p.id === photoId1); const sch = showcase.photos.find(p => p.id === photoId2); assert.equal(ext?.category, 'exterieur'); assert.equal(sch?.category, 'gebruikssporen'); assert.equal(ext?.is_primary, 1); }); }); describe('Multi-Country License Plate & Maintenance Invoices', () => { it('should support plate_country in vehicles schema and persist correctly', () => { const vId = crypto.randomUUID(); const now = new Date().toISOString(); db.run( `INSERT INTO vehicles (id, user_id, name, type, license_plate, plate_country, purchase_price, initial_odometer, status, created_at, updated_at) VALUES (?, ?, 'Belgian Daily', 'car', '1-ABC-123', 'BE', 12000, 45000, 'active', ?, ?)`, [vId, testUserId, now, now] ); const v = db.get('SELECT license_plate, plate_country FROM vehicles WHERE id = ?', [vId]); assert.ok(v); assert.equal(v.license_plate, '1-ABC-123'); assert.equal(v.plate_country, 'BE'); db.run('DELETE FROM vehicles WHERE id = ?', [vId]); }); it('should attach an invoice to an existing service record via MaintenanceController.attachInvoiceToRecord', async () => { const recId = crypto.randomUUID(); const now = new Date().toISOString(); db.run( `INSERT INTO service_records (id, vehicle_id, service_item, date, odometer, performing_party, cost, created_at) VALUES (?, ?, 'Grote Beurt & Remmen', '2026-10-25', 85000, 'garage', 450, ?)`, [recId, testVehicleId, now] ); const fakeFilename = `test_inv_${Date.now()}.pdf`; const fakeInvoicePath = path.join(DATA_PATHS.invoices, fakeFilename); fs.writeFileSync(fakeInvoicePath, '%PDF-1.4 test invoice content'); let statusCode = 200; let jsonPayload: any = null; const mockRes: any = { status: (c: number) => { statusCode = c; return mockRes; }, json: (p: any) => { jsonPayload = p; return mockRes; }, }; try { await MaintenanceController.attachInvoiceToRecord( { params: { id: recId }, file: { filename: fakeFilename, path: fakeInvoicePath } } as any, mockRes ); assert.equal(statusCode, 200); assert.equal(jsonPayload.success, true); assert.equal(jsonPayload.fileName, fakeFilename); const updated = db.get('SELECT invoice_path FROM service_records WHERE id = ?', [recId]); assert.equal(updated.invoice_path, fakeFilename); } finally { db.run('DELETE FROM service_records WHERE id = ?', [recId]); if (fs.existsSync(fakeInvoicePath)) { fs.unlinkSync(fakeInvoicePath); } } }); it('should ingest odometer reading from Home Assistant webhook via HaController.updateOdometer', async () => { let statusCode = 200; let jsonPayload: any = null; const mockRes: any = { status: (c: number) => { statusCode = c; return mockRes; }, json: (p: any) => { jsonPayload = p; return mockRes; }, }; await HaController.updateOdometer( { body: { vehicle_id: testVehicleId, odometer: 95400, source: 'home_assistant' } } as any, mockRes ); assert.equal(statusCode, 200); assert.equal(jsonPayload.success, true); assert.equal(jsonPayload.odometer, 95400); const log = db.get('SELECT odometer, source FROM odometer_logs WHERE vehicle_id = ? ORDER BY created_at DESC LIMIT 1', [testVehicleId]); assert.ok(log); assert.equal(log.odometer, 95400); assert.equal(log.source, 'home_assistant'); }); it('should generate RFC 5545 compliant .ics calendar export including APK and maintenance alarms', () => { const ics = CalendarService.generateIcs(testVehicleId); assert.ok(ics.startsWith('BEGIN:VCALENDAR')); assert.ok(ics.includes('PRODID:-//VehicleVault//Vehicle Management Platform//NL')); assert.ok(ics.includes('BEGIN:VEVENT')); assert.ok(ics.includes('VALARM')); assert.ok(ics.endsWith('END:VCALENDAR')); assert.ok(ics.includes('\r\n')); }); it('should alert on expiring documents within 30 days during notification check', async () => { const docId = crypto.randomUUID(); const inTwoWeeks = new Date(); inTwoWeeks.setDate(inTwoWeeks.getDate() + 14); const expDateStr = inTwoWeeks.toISOString().slice(0, 10); db.run( `INSERT INTO vehicle_documents (id, vehicle_id, user_id, name, category, file_path, file_size, expires_at, created_at) VALUES (?, ?, ?, 'Groene Kaart 2026', 'insurance', 'groenekaart.pdf', 1024, ?, ?)`, [docId, testVehicleId, testUserId, expDateStr, new Date().toISOString()] ); try { const doc = db.get('SELECT expires_at FROM vehicle_documents WHERE id = ?', [docId]); assert.equal(doc.expires_at, expDateStr); // Temporarily configure notification settings to mock dispatch NotificationService.saveSettings({ provider: 'webhook', webhookUrl: 'https://webhook.invalid.local/test', enabledApk: true, enabledMaintenance: true, }); // Run check (will attempt dispatch) const result = await NotificationService.checkAndDispatchAlerts(); assert.ok(result); assert.ok(Array.isArray(result.details)); } finally { db.run('DELETE FROM vehicle_documents WHERE id = ?', [docId]); NotificationService.saveSettings({ provider: 'none' }); } }); it('should normalize German AI invoice extraction with translation, garage address, and sync to vehicle_documents', () => { const sampleGermanAiJson = JSON.stringify({ garageName: 'Autohaus Müller GmbH', garageAddress: 'Industriestraße 12, 48599 Gronau (Westf.), Duitsland', date: '17.08.2026', odometer: '54.320 km', detectedLanguage: 'Duits', serviceTitle: 'Grote Onderhoudsbeurt & Olie verversen', totalCost: '357,00', partsCost: '150,00', laborCost: '150,00', translatedNotes: 'Inspektion nach Herstellervorgabe durchgeführt, Bremsflüssigkeit erneuert.', lineItems: [ { originalDescription: 'Inspektion mit Ölwechsel', translatedDescription: 'Inspectie met olieverversing', category: 'labor', quantity: '1x', totalPrice: 150.0, }, { originalDescription: 'Ölfilterpatrone & Motoröl 5W-30', translatedDescription: 'Oliefilterpatroon & Motorolie 5W-30', category: 'parts', quantity: '1x', totalPrice: 105.0, }, { originalDescription: 'Bremsflüssigkeit DOT4', translatedDescription: 'Remvloeistof DOT4', category: 'parts', quantity: '1x', totalPrice: 45.0, }, ], }); const parsed = AiReceiptService.normalizeInvoiceResult( sampleGermanAiJson, 'gemini', 'gemini-2.5-flash', 'nl' ); assert.equal(parsed.garageName, 'Autohaus Müller GmbH'); assert.equal(parsed.garageAddress, 'Industriestraße 12, 48599 Gronau (Westf.), Duitsland'); assert.equal(parsed.date, '2026-08-17'); assert.equal(parsed.odometer, 54320); assert.equal(parsed.detectedLanguage, 'Duits'); assert.equal(parsed.serviceTitle, 'Grote Onderhoudsbeurt & Olie verversen'); assert.equal(parsed.totalCost, 357); // Net 150 + 150 = 300, total 357 (19% German MwSt) -> reconciled to 178.5 + 178.5 = 357 assert.equal(Number(((parsed.partsCost || 0) + (parsed.laborCost || 0)).toFixed(2)), 357); assert.equal(parsed.lineItems.length, 3); assert.equal(parsed.lineItems[0].originalDescription, 'Inspektion mit Ölwechsel'); assert.equal(parsed.lineItems[0].translatedDescription, 'Inspectie met olieverversing'); assert.equal(parsed.lineItems[0].category, 'labor'); assert.equal(parsed.lineItems[1].category, 'parts'); // Also verify syncInvoiceToDocuments copies file to documents dir and creates vehicle_documents entry const fakeFilename = `german-invoice-${Date.now()}.pdf`; const fakeInvoicePath = path.join(DATA_PATHS.invoices, fakeFilename); const fakeDocPath = path.join(DATA_PATHS.documents, fakeFilename); fs.writeFileSync(fakeInvoicePath, '%PDF-1.4 german invoice test'); try { MaintenanceController.syncInvoiceToDocuments( testVehicleId, testUserId, fakeFilename, parsed.serviceTitle || 'Onderhoud', parsed.date || '2026-08-17', parsed.garageName, parsed.garageAddress ); assert.ok(fs.existsSync(fakeDocPath), 'Expected invoice file to be copied to documents directory'); const syncedDoc = db.get( 'SELECT name, category, file_path, notes FROM vehicle_documents WHERE vehicle_id = ? AND file_path = ?', [testVehicleId, fakeFilename] ); assert.ok(syncedDoc, 'Expected synced document record in vehicle_documents'); assert.equal(syncedDoc.category, 'invoice'); assert.ok(syncedDoc.name.includes('Autohaus Müller GmbH')); assert.ok(syncedDoc.name.includes('(17-08-2026)')); assert.ok(syncedDoc.notes.includes('Industriestraße 12')); } finally { db.run('DELETE FROM vehicle_documents WHERE vehicle_id = ? AND file_path = ?', [testVehicleId, fakeFilename]); if (fs.existsSync(fakeInvoicePath)) fs.unlinkSync(fakeInvoicePath); if (fs.existsSync(fakeDocPath)) fs.unlinkSync(fakeDocPath); } }); it('should reset multiple maintenance intervals simultaneously from a single invoice record', () => { const catId = db.get('SELECT id FROM service_categories LIMIT 1')?.id; assert.ok(catId); const int1 = crypto.randomUUID(); const int2 = crypto.randomUUID(); db.run( `INSERT INTO maintenance_intervals (id, vehicle_id, category_id, custom_name, interval_km, interval_months, last_service_date, last_service_odometer, created_at) VALUES (?, ?, ?, 'Motorolie & Oliefilter', 15000, 12, '2026-10-29', 67799, ?), (?, ?, ?, 'Remvloeistof', 40000, 24, '2026-10-29', 67799, ?)`, [int1, testVehicleId, catId, new Date().toISOString(), int2, testVehicleId, catId, new Date().toISOString()] ); try { MaintenanceController.resetMatchedIntervals(testVehicleId, [int1, int2], '2026-08-17', 66018); const updated1 = db.get('SELECT last_service_date, last_service_odometer FROM maintenance_intervals WHERE id = ?', [int1]); const updated2 = db.get('SELECT last_service_date, last_service_odometer FROM maintenance_intervals WHERE id = ?', [int2]); assert.equal(updated1.last_service_date, '2026-08-17'); assert.equal(updated1.last_service_odometer, 66018); assert.equal(updated2.last_service_date, '2026-08-17'); assert.equal(updated2.last_service_odometer, 66018); } finally { db.run('DELETE FROM maintenance_intervals WHERE id IN (?, ?)', [int1, int2]); } }); it('should include current tires and append PDF invoices as separate pages in PDF dossier export', async () => { const tireId = crypto.randomUUID(); const recId = crypto.randomUUID(); const sampleInvoiceName = `test-invoice-${Date.now()}.pdf`; const sampleInvoicePath = path.join(DATA_PATHS.invoices, sampleInvoiceName); // Create a valid 2-page PDF invoice with pdf-lib const samplePdf = await PdfLibDocument.create(); const p1 = samplePdf.addPage([595, 842]); p1.drawText('CAR Avenue Autohaus Invoice - Page 1', { x: 50, y: 750, size: 16 }); const p2 = samplePdf.addPage([595, 842]); p2.drawText('CAR Avenue Autohaus Invoice - Page 2', { x: 50, y: 750, size: 16 }); const samplePdfBytes = await samplePdf.save(); fs.writeFileSync(sampleInvoicePath, Buffer.from(samplePdfBytes)); db.run( `INSERT INTO tire_sets (id, vehicle_id, name, season, brand, model, tire_size, rim_size, speed_index, dot_code, status, total_km, tread_depth_fl, tread_depth_fr, tread_depth_rl, tread_depth_rr, created_at) VALUES (?, ?, 'Huidig', 'all_season', 'Bridgestone', 'Turanza 6', '205/60', '16', '92V', '2324', 'mounted', 1000, 7.5, 7.5, 7.5, 7.5, ?)`, [tireId, testVehicleId, new Date().toISOString()] ); db.run( `INSERT INTO service_records (id, vehicle_id, service_item, date, odometer, performing_party, cost, garage_name, invoice_path, created_at) VALUES (?, ?, 'Onderhoudsbeurt 90.000 km', '2026-08-17', 66018, 'garage', 636.25, 'CAR Avenue Autohaus GmbH', ?, ?)`, [recId, testVehicleId, sampleInvoiceName, new Date().toISOString()] ); try { const bufferWithoutReceipts = await PdfService.generateDossier({ vehicleId: testVehicleId, includeTires: true, includeFuel: true, includeReceipts: false, }); const docWithout = await PdfLibDocument.load(bufferWithoutReceipts); const basePageCount = docWithout.getPageCount(); const bufferWithReceipts = await PdfService.generateDossier({ vehicleId: testVehicleId, includeTires: true, includeFuel: true, includeReceipts: true, }); const docWith = await PdfLibDocument.load(bufferWithReceipts); // The 2-page PDF invoice must be appended as 2 separate pages at the end of the dossier assert.equal(docWith.getPageCount(), basePageCount + 2); } finally { db.run('DELETE FROM tire_sets WHERE id = ?', [tireId]); db.run('DELETE FROM service_records WHERE id = ?', [recId]); if (fs.existsSync(sampleInvoicePath)) fs.unlinkSync(sampleInvoicePath); } }); it('should allow shared users to access documents, maintenance intervals/records, and TCO on a shared vehicle', async () => { const fatherUserId = crypto.randomUUID(); const shareId = crypto.randomUUID(); const docId = crypto.randomUUID(); const now = new Date().toISOString(); db.run( `INSERT INTO users (id, email, password_hash, role, created_at, updated_at) VALUES (?, ?, 'hash', 'user', ?, ?)`, [fatherUserId, `father-${Date.now()}@vehiclevault.nl`, now, now] ); db.run( `INSERT INTO vehicle_shares (id, vehicle_id, user_id, role, created_at) VALUES (?, ?, ?, 'editor', ?)`, [shareId, testVehicleId, fatherUserId, now] ); db.run( `INSERT INTO vehicle_documents (id, vehicle_id, user_id, name, category, file_path, file_size, created_at) VALUES (?, ?, ?, 'Kentekenbewijs Kia', 'registration', 'kenteken.pdf', 1024, ?)`, [docId, testVehicleId, testUserId, now] ); try { // 1. DocumentController.list as father let docStatus = 200; let docBody: any = null; await DocumentController.list( { params: { vehicleId: testVehicleId }, query: {}, user: { userId: fatherUserId, email: 'father@test.nl', role: 'user' }, } as any, { status(code: number) { docStatus = code; return this; }, json(payload: any) { docBody = payload; }, } as any ); assert.equal(docStatus, 200); assert.ok(Array.isArray(docBody?.documents)); assert.ok(docBody.documents.some((d: any) => d.id === docId)); // 2. MaintenanceController.listIntervals as father let intStatus = 200; let intBody: any = null; await MaintenanceController.listIntervals( { params: { vehicleId: testVehicleId }, user: { userId: fatherUserId, email: 'father@test.nl', role: 'user' }, } as any, { status(code: number) { intStatus = code; return this; }, json(payload: any) { intBody = payload; }, } as any ); assert.equal(intStatus, 200); assert.ok(Array.isArray(intBody?.intervals)); // 3. AnalyticsController.getTco as father let tcoStatus = 200; let tcoBody: any = null; await AnalyticsController.getTco( { params: { vehicleId: testVehicleId }, query: {}, user: { userId: fatherUserId, email: 'father@test.nl', role: 'user' }, } as any, { status(code: number) { tcoStatus = code; return this; }, json(payload: any) { tcoBody = payload; }, } as any ); assert.equal(tcoStatus, 200); assert.equal(tcoBody?.vehicleId, testVehicleId); } finally { db.run('DELETE FROM vehicle_documents WHERE id = ?', [docId]); db.run('DELETE FROM vehicle_shares WHERE id = ?', [shareId]); db.run('DELETE FROM users WHERE id = ?', [fatherUserId]); } }); }); });