multi-country.integration.test.js
1 /** 2 * Multi-Country Integration Tests 3 * Tests country-specific functionality: config, phone validation, localization, GDPR 4 * 5 * Run: npm run test:integration 6 */ 7 8 import { test, mock } from 'node:test'; 9 import assert from 'node:assert'; 10 import Database from 'better-sqlite3'; 11 import { createPgMock } from '../helpers/pg-mock.js'; 12 import { getCountryByCode, isMobileNumber } from '../../src/config/countries.js'; 13 import { getAllContacts } from '../../src/contacts/prioritize.js'; 14 15 // ── In-memory DB for database metadata test ────────────────────────────────── 16 17 const db = new Database(':memory:'); 18 19 mock.module('../../src/utils/db.js', { namedExports: createPgMock(db) }); 20 21 db.exec(` 22 CREATE TABLE IF NOT EXISTS sites ( 23 id INTEGER PRIMARY KEY AUTOINCREMENT, 24 domain TEXT, 25 landing_page_url TEXT, 26 status TEXT, 27 country_code TEXT, 28 google_domain TEXT, 29 language_code TEXT, 30 currency_code TEXT, 31 rescored_at DATETIME 32 ); 33 `); 34 35 // Test country configurations 36 test('Country config: validates all required fields for test countries', () => { 37 console.log('\n🧪 Testing country configurations...\n'); 38 39 const testCountries = ['US', 'UK', 'DE', 'JP', 'AU', 'FR']; 40 41 testCountries.forEach(code => { 42 console.log(`📍 Testing ${code} configuration...`); 43 44 const country = getCountryByCode(code); 45 46 // Required fields 47 assert.ok(country, `Country ${code} should exist`); 48 assert.strictEqual(country.code, code, `Country code should match ${code}`); 49 assert.ok(country.name, `${code} should have name`); 50 assert.ok(country.currency, `${code} should have currency`); 51 assert.ok(country.currencySymbol, `${code} should have currency symbol`); 52 assert.ok(country.googleDomain, `${code} should have google domain`); 53 assert.ok(country.language, `${code} should have language`); 54 assert.ok(country.dateFormat, `${code} should have date format`); 55 assert.ok(country.phoneFormat, `${code} should have phone format`); 56 assert.ok(country.timezone, `${code} should have timezone`); 57 58 // Mobile pattern validation 59 assert.ok(country.mobilePattern, `${code} should have mobile pattern`); 60 assert.ok(country.mobilePattern instanceof RegExp, `${code} mobile pattern should be RegExp`); 61 62 // GDPR flag 63 assert.ok(typeof country.requiresGDPRCheck === 'boolean', `${code} should have GDPR flag`); 64 65 console.log(` ✅ ${code} configuration valid`); 66 }); 67 68 console.log('\n✅ All country configurations valid!\n'); 69 }); 70 71 // Test mobile number validation 72 test('Mobile validation: correctly identifies mobile vs landline numbers', () => { 73 console.log('\n🧪 Testing mobile number validation...\n'); 74 75 const testCases = [ 76 // US - mobile must be 10 digits without 0 or 1 prefix 77 { phone: '+14155551234', country: 'US', expected: true, label: 'US mobile (E.164)' }, 78 { phone: '14155551234', country: 'US', expected: true, label: 'US mobile (no +)' }, 79 { phone: '4155551234', country: 'US', expected: true, label: 'US mobile (no country)' }, 80 { phone: '+12025551234', country: 'US', expected: true, label: 'US DC mobile' }, 81 { phone: '+10125551234', country: 'US', expected: false, label: 'US invalid (0 prefix)' }, 82 83 // UK - mobile must start with 7 84 { phone: '+447700900123', country: 'UK', expected: true, label: 'UK mobile (E.164)' }, 85 { phone: '07700900123', country: 'UK', expected: true, label: 'UK mobile (0 prefix)' }, 86 { phone: '+442071234567', country: 'UK', expected: false, label: 'UK landline (020)' }, 87 { phone: '+441632960123', country: 'UK', expected: false, label: 'UK landline (016)' }, 88 89 // Germany - mobile starts with 15, 16, 17 90 { phone: '+4915112345678', country: 'DE', expected: true, label: 'DE mobile (151)' }, 91 { phone: '+4916212345678', country: 'DE', expected: true, label: 'DE mobile (162)' }, 92 { phone: '+4917612345678', country: 'DE', expected: true, label: 'DE mobile (176)' }, 93 { phone: '015112345678', country: 'DE', expected: true, label: 'DE mobile (0 prefix)' }, 94 { phone: '+493012345678', country: 'DE', expected: false, label: 'DE landline (030)' }, 95 96 // Japan - mobile starts with 70, 80, 90 97 { phone: '+819012345678', country: 'JP', expected: true, label: 'JP mobile (90)' }, 98 { phone: '+818012345678', country: 'JP', expected: true, label: 'JP mobile (80)' }, 99 { phone: '+817012345678', country: 'JP', expected: true, label: 'JP mobile (70)' }, 100 { phone: '09012345678', country: 'JP', expected: true, label: 'JP mobile (0 prefix)' }, 101 { phone: '+81312345678', country: 'JP', expected: false, label: 'JP landline (03)' }, 102 103 // Australia - mobile starts with 4 104 { phone: '+61412345678', country: 'AU', expected: true, label: 'AU mobile' }, 105 { phone: '0412345678', country: 'AU', expected: true, label: 'AU mobile (0 prefix)' }, 106 { phone: '+61212345678', country: 'AU', expected: false, label: 'AU landline (02)' }, 107 { phone: '+61812345678', country: 'AU', expected: false, label: 'AU landline (08)' }, 108 109 // France - mobile starts with 6 or 7 110 { phone: '+33612345678', country: 'FR', expected: true, label: 'FR mobile (6)' }, 111 { phone: '+33712345678', country: 'FR', expected: true, label: 'FR mobile (7)' }, 112 { phone: '0612345678', country: 'FR', expected: true, label: 'FR mobile (0 prefix)' }, 113 { phone: '+33112345678', country: 'FR', expected: false, label: 'FR landline (01)' }, 114 ]; 115 116 testCases.forEach(({ phone, country, expected, label }) => { 117 const countryConfig = getCountryByCode(country); 118 const result = isMobileNumber(phone, countryConfig); 119 120 console.log( 121 ` ${result === expected ? '✅' : '❌'} ${label}: ${phone} → ${result ? 'mobile' : 'landline'}` 122 ); 123 124 assert.strictEqual( 125 result, 126 expected, 127 `${label}: ${phone} should be ${expected ? 'mobile' : 'landline'}` 128 ); 129 }); 130 131 console.log('\n✅ All mobile validation tests passed!\n'); 132 }); 133 134 // Test contact prioritization 135 test('Contact prioritization: prefers mobile over landline', async () => { 136 console.log('\n🧪 Testing contact prioritization...\n'); 137 138 // Test data with mixed mobile and landline numbers 139 // Note: getAllContacts expects a PARSED object, not a JSON string 140 const contactsJson = { 141 email_addresses: ['info@example.com'], 142 phone_numbers: [ 143 '+442071234567', // UK landline (020) 144 '+447700900123', // UK mobile (07) 145 '+441632960123', // UK landline (016) 146 '+447911123456', // UK mobile (07) 147 ], 148 }; 149 150 const contacts = getAllContacts(contactsJson, 'UK'); 151 152 console.log(` 📊 Found ${contacts.length} contacts`); 153 154 // Verify mobiles come first 155 const mobileContacts = contacts.filter(c => c.channel === 'sms'); 156 157 console.log(` 📱 Mobile contacts: ${mobileContacts.length}`); 158 console.log(` ☎️ Landline contacts: ${contacts.length - mobileContacts.length}`); 159 160 assert.ok(mobileContacts.length > 0, 'Should have mobile contacts'); 161 assert.ok(mobileContacts.length >= 2, 'Should have at least 2 mobile contacts (both 07 numbers)'); 162 163 // Verify first contact is email (always prioritized) 164 assert.strictEqual( 165 contacts[0].channel, 166 'email', 167 'First contact should be email (always prioritized)' 168 ); 169 assert.strictEqual(contacts[1].channel, 'sms', 'Second contact should be SMS'); 170 assert.ok(contacts[1].uri.startsWith('+4477'), 'First SMS should be mobile (+4477)'); 171 172 console.log(' ✅ Contacts prioritized correctly\n'); 173 }); 174 175 // Test database country metadata storage 176 test('Database: stores and retrieves country metadata', () => { 177 console.log('\n🧪 Testing database country metadata...\n'); 178 179 // Insert sample sites with country metadata using in-memory DB 180 db.prepare( 181 `INSERT INTO sites (domain, landing_page_url, status, country_code, google_domain, language_code, currency_code) 182 VALUES (?, ?, ?, ?, ?, ?, ?)` 183 ).run( 184 'example.com.au', 185 'https://example.com.au', 186 'enriched', 187 'AU', 188 'google.com.au', 189 'en-AU', 190 'AUD' 191 ); 192 db.prepare( 193 `INSERT INTO sites (domain, landing_page_url, status, country_code, google_domain, language_code, currency_code) 194 VALUES (?, ?, ?, ?, ?, ?, ?)` 195 ).run( 196 'example.co.uk', 197 'https://example.co.uk', 198 'enriched', 199 'UK', 200 'google.co.uk', 201 'en-GB', 202 'GBP' 203 ); 204 205 // Query sites with country metadata 206 const sitesByCountry = db 207 .prepare( 208 ` 209 SELECT country_code, COUNT(*) as count 210 FROM sites 211 WHERE country_code IS NOT NULL 212 GROUP BY country_code 213 ORDER BY count DESC 214 ` 215 ) 216 .all(); 217 218 console.log(' 📊 Sites by country:'); 219 sitesByCountry.forEach(row => { 220 console.log(` ${row.country_code}: ${row.count} sites`); 221 }); 222 223 assert.ok(sitesByCountry.length >= 2, 'Should have at least 2 country entries'); 224 225 // Verify country metadata fields are populated 226 const sampleSite = db 227 .prepare( 228 ` 229 SELECT country_code, google_domain, language_code, currency_code 230 FROM sites 231 WHERE country_code IS NOT NULL 232 LIMIT 1 233 ` 234 ) 235 .get(); 236 237 assert.ok(sampleSite, 'Should find a sample site'); 238 assert.ok(sampleSite.country_code, 'Should have country_code'); 239 assert.ok(sampleSite.google_domain, 'Should have google_domain'); 240 assert.ok(sampleSite.language_code, 'Should have language_code'); 241 assert.ok(sampleSite.currency_code, 'Should have currency_code'); 242 243 console.log('\n✅ Database country metadata test passed!\n'); 244 }); 245 246 // Test GDPR country identification 247 test('GDPR: correctly identifies EU/UK countries requiring checks', () => { 248 console.log('\n🧪 Testing GDPR country identification...\n'); 249 250 const gdprCountries = [ 251 'DE', 252 'FR', 253 'IT', 254 'ES', 255 'NL', 256 'BE', 257 'AT', 258 'SE', 259 'DK', 260 'NO', 261 'IE', 262 'UK', 263 'PL', 264 ]; 265 const nonGdprCountries = ['US', 'AU', 'JP', 'CA', 'NZ', 'SG', 'KR', 'CN', 'IN', 'MX', 'ID', 'CH']; 266 267 console.log(' 📋 Testing GDPR countries (should require checks):'); 268 gdprCountries.forEach(code => { 269 const country = getCountryByCode(code); 270 console.log(` ${country.requiresGDPRCheck ? '✅' : '❌'} ${code}: ${country.name}`); 271 assert.strictEqual(country.requiresGDPRCheck, true, `${code} should require GDPR checks`); 272 }); 273 274 console.log('\n 📋 Testing non-GDPR countries (should NOT require checks):'); 275 nonGdprCountries.forEach(code => { 276 const country = getCountryByCode(code); 277 console.log(` ${!country.requiresGDPRCheck ? '✅' : '❌'} ${code}: ${country.name}`); 278 assert.strictEqual(country.requiresGDPRCheck, false, `${code} should NOT require GDPR checks`); 279 }); 280 281 console.log('\n✅ GDPR identification test passed!\n'); 282 });