/ tests / utils / timezone-detector.test.js
timezone-detector.test.js
  1  /**
  2   * Timezone Detector Tests
  3   *
  4   * Tests for detectTimezone() and getSiteTimezone().
  5   * Uses mock DB for getSiteTimezone to avoid SQLite I/O.
  6   */
  7  
  8  import { test, describe } from 'node:test';
  9  import assert from 'node:assert/strict';
 10  
 11  import { detectTimezone, getSiteTimezone } from '../../src/utils/timezone-detector.js';
 12  
 13  // ─── detectTimezone ────────────────────────────────────────────────────────────
 14  
 15  describe('detectTimezone', () => {
 16    test('defaults to America/New_York when no location data', () => {
 17      assert.equal(detectTimezone(null, null), 'America/New_York');
 18      assert.equal(detectTimezone(undefined, undefined), 'America/New_York');
 19    });
 20  
 21    test('exact city match for US cities', () => {
 22      assert.equal(detectTimezone('new york', 'US'), 'America/New_York');
 23      assert.equal(detectTimezone('chicago', 'US'), 'America/Chicago');
 24      assert.equal(detectTimezone('denver', 'US'), 'America/Denver');
 25      assert.equal(detectTimezone('los angeles', 'US'), 'America/Los_Angeles');
 26      assert.equal(detectTimezone('phoenix', 'US'), 'America/Phoenix');
 27      assert.equal(detectTimezone('honolulu', 'US'), 'Pacific/Honolulu');
 28      assert.equal(detectTimezone('anchorage', 'US'), 'America/Anchorage');
 29    });
 30  
 31    test('case-insensitive city lookup', () => {
 32      assert.equal(detectTimezone('New York', 'US'), 'America/New_York');
 33      assert.equal(detectTimezone('LOS ANGELES', 'US'), 'America/Los_Angeles');
 34      assert.equal(detectTimezone('Chicago', 'US'), 'America/Chicago');
 35    });
 36  
 37    test('case-insensitive country code', () => {
 38      assert.equal(detectTimezone('sydney', 'au'), 'Australia/Sydney');
 39      assert.equal(detectTimezone('toronto', 'ca'), 'America/Toronto');
 40    });
 41  
 42    test('city lookup for Australian cities', () => {
 43      assert.equal(detectTimezone('sydney', 'AU'), 'Australia/Sydney');
 44      assert.equal(detectTimezone('melbourne', 'AU'), 'Australia/Melbourne');
 45      assert.equal(detectTimezone('perth', 'AU'), 'Australia/Perth');
 46      assert.equal(detectTimezone('adelaide', 'AU'), 'Australia/Adelaide');
 47      assert.equal(detectTimezone('darwin', 'AU'), 'Australia/Darwin');
 48      assert.equal(detectTimezone('brisbane', 'AU'), 'Australia/Brisbane');
 49    });
 50  
 51    test('city lookup for Canadian cities', () => {
 52      assert.equal(detectTimezone('toronto', 'CA'), 'America/Toronto');
 53      assert.equal(detectTimezone('vancouver', 'CA'), 'America/Vancouver');
 54      assert.equal(detectTimezone('calgary', 'CA'), 'America/Edmonton');
 55      assert.equal(detectTimezone("st. john's", 'CA'), 'America/St_Johns');
 56    });
 57  
 58    test('partial city name match', () => {
 59      // "San Francisco Bay Area" should match "san francisco"
 60      assert.equal(detectTimezone('San Francisco Bay Area', 'US'), 'America/Los_Angeles');
 61    });
 62  
 63    test('country fallback when city not in CITY_TIMEZONES', () => {
 64      // Obscure US city → country fallback
 65      assert.equal(detectTimezone('Springfield', 'US'), 'America/New_York'); // US country default
 66    });
 67  
 68    test('country-level timezone for single-timezone countries', () => {
 69      assert.equal(detectTimezone(null, 'AU'), 'Australia/Sydney');
 70      assert.equal(detectTimezone(null, 'UK'), 'Europe/London'); // UK alias for GB (migration added to COUNTRY_TIMEZONES)
 71      assert.equal(detectTimezone(null, 'GB'), 'Europe/London');
 72      assert.equal(detectTimezone(null, 'DE'), 'Europe/Berlin');
 73      assert.equal(detectTimezone(null, 'JP'), 'Asia/Tokyo');
 74      assert.equal(detectTimezone(null, 'IN'), 'Asia/Kolkata');
 75      assert.equal(detectTimezone(null, 'NZ'), 'Pacific/Auckland');
 76      assert.equal(detectTimezone(null, 'ZA'), 'Africa/Johannesburg');
 77    });
 78  
 79    test('defaults to America/New_York when country not in map', () => {
 80      assert.equal(detectTimezone(null, 'UNKNOWN'), 'America/New_York');
 81      assert.equal(detectTimezone('SomeCity', 'XX'), 'America/New_York');
 82    });
 83  
 84    test('Mexico city-specific timezones', () => {
 85      assert.equal(detectTimezone('cancun', 'MX'), 'America/Cancun');
 86      assert.equal(detectTimezone('tijuana', 'MX'), 'America/Tijuana');
 87      assert.equal(detectTimezone('mexico city', 'MX'), 'America/Mexico_City');
 88    });
 89  });
 90  
 91  // ─── getSiteTimezone ───────────────────────────────────────────────────────────
 92  
 93  describe('getSiteTimezone', () => {
 94    test('returns timezone from site city and country_code', () => {
 95      const db = {
 96        prepare: () => ({
 97          get: () => ({ city: 'sydney', country_code: 'AU' }),
 98        }),
 99      };
100      const result = getSiteTimezone(1, db);
101      assert.equal(result, 'Australia/Sydney');
102    });
103  
104    test('returns America/New_York when site not found', () => {
105      const db = {
106        prepare: () => ({
107          get: () => undefined,
108        }),
109      };
110      const result = getSiteTimezone(999, db);
111      assert.equal(result, 'America/New_York');
112    });
113  
114    test('handles site with no city (country fallback)', () => {
115      const db = {
116        prepare: () => ({
117          get: () => ({ city: null, country_code: 'DE' }),
118        }),
119      };
120      const result = getSiteTimezone(1, db);
121      assert.equal(result, 'Europe/Berlin');
122    });
123  
124    test('passes siteId to SQL query', () => {
125      let capturedArgs = null;
126      const db = {
127        prepare: () => ({
128          get: (...args) => {
129            capturedArgs = args;
130            return { city: 'paris', country_code: 'FR' };
131          },
132        }),
133      };
134      getSiteTimezone(42, db);
135      assert.equal(capturedArgs[0], 42);
136    });
137  });