/ tests / pipeline / multi-country.integration.test.js
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  });