diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fd9947c..7ec0fa7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,7 +7,21 @@ on: branches: [master] jobs: - build: + static-checks: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v7.0.0 + - name: Use Node.js 24.x + uses: actions/setup-node@v6.4.0 + with: + node-version: 24.x + cache: npm + - run: npm ci + - run: npm run format:check + - run: npm run lint + + test: runs-on: ubuntu-latest strategy: @@ -20,8 +34,7 @@ jobs: uses: actions/setup-node@v6.4.0 with: node-version: ${{ matrix.node-version }} + cache: npm - run: npm ci - - run: npm run format:check - - run: npm run lint - run: npm run build --if-present - run: npm test diff --git a/package.json b/package.json index 3e5b881..7eadcbc 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ }, "homepage": "https://github.com/parcelLab/regionIdentifier", "scripts": { - "test": "node --test test/tests.js", + "test": "node --test", "lint": "eslint .", "format": "prettier --write .", "format:check": "prettier --check ." diff --git a/test/fixtures/countries-postal-codes.json b/test/fixtures/countries-postal-codes.json new file mode 100644 index 0000000..d809caa --- /dev/null +++ b/test/fixtures/countries-postal-codes.json @@ -0,0 +1,544 @@ +{ + "Austria": [ + { + "name": "AUT", + "zip": "1999", + "result": "AT-9", + "usingGoogle": false + }, + { + "name": "Austria", + "usingGoogle": false, + "zip": "1999", + "result": "AT-9" + } + ], + "Deutschland": [ + { + "name": "DEU", + "zip": "6578", + "result": "DE-TH", + "usingGoogle": false + } + ], + "Belgium": [ + { + "name": "BEL", + "zip": "1701", + "result": "BE-VLG", + "usingGoogle": false + } + ], + "Switzerland": [ + { + "name": "CHE", + "zip": "2333", + "result": "CH-BE", + "usingGoogle": false + }, + { + "name": "CHE", + "zip": "9488", + "result": "CH-SG", + "usingGoogle": false + } + ], + "Spain": [ + { + "name": "ESP", + "zip": "04004", + "result": "ES-AN", + "usingGoogle": false + } + ], + "France": [ + { + "name": "FRA", + "zip": "25090", + "result": "FR-I", + "usingGoogle": false + }, + { + "name": "FRA", + "zip": "92440", + "result": "FR-J", + "usingGoogle": false + }, + { + "name": "FRA", + "zip": "75116", + "result": "FR-J", + "usingGoogle": false + }, + { + "name": "FRA", + "zip": "51000", + "result": "FR-G", + "usingGoogle": false + }, + { + "name": "FRA", + "zip": "51100", + "result": "FR-G", + "usingGoogle": false + }, + { + "name": "FRA", + "zip": "75020", + "result": "FR-J", + "usingGoogle": false + }, + { + "name": "FR", + "zip": "25090", + "result": "FR-I", + "usingGoogle": false + }, + { + "name": "FR", + "zip": "87200", + "result": "FR-L", + "usingGoogle": false + }, + { + "name": "FR", + "zip": "87200 CEDEX 1", + "result": "FR-L", + "usingGoogle": false + }, + { + "name": "FR", + "zip": "61100", + "result": "FR-P", + "usingGoogle": false + }, + { + "name": "FR", + "zip": "81000", + "result": "FR-N", + "usingGoogle": false + } + ], + "United Kingdom": [ + { + "name": "GBR", + "zip": "PA13 4PS", + "result": "GB-SCT", + "usingGoogle": false + }, + { + "name": "GBR", + "zip": "EC2N 1AR", + "result": "GB-ENG", + "usingGoogle": false + }, + { + "name": "GBR", + "zip": "FY83QN", + "result": "GB-ENG", + "usingGoogle": false + }, + { + "name": "GBR", + "zip": "CF466RT", + "result": "GB-WLS", + "usingGoogle": false + }, + { + "name": "GBR", + "zip": "AL10 9BL", + "result": "GB-ENG", + "usingGoogle": false + }, + { + "name": "GBR", + "zip": "m14 5pq", + "result": "GB-ENG", + "usingGoogle": false + }, + { + "name": "GBR", + "zip": "Po10 8Eg", + "result": "GB-ENG", + "usingGoogle": false + }, + { + "name": "GBR", + "zip": "L5 3QN", + "result": "GB-ENG", + "usingGoogle": false + } + ], + "Italy": [ + { + "name": "ITA", + "zip": "67033", + "result": "IT-65", + "usingGoogle": false + }, + { + "name": "ITA", + "zip": "41121", + "result": "IT-45", + "usingGoogle": false + } + ], + "India": [ + { + "name": "IND", + "zip": "110001", + "result": "IN-DL", + "usingGoogle": false + } + ], + "Ireland": [ + { + "name": "IRL", + "zip": "D02 X285", + "result": "IE-L", + "usingGoogle": false + } + ], + "Romania": [ + { + "name": "ROU", + "zip": "010011", + "result": "RO-B", + "usingGoogle": false + } + ], + "Hungary": [ + { + "name": "HUN", + "zip": "1011", + "result": "HU-BU", + "usingGoogle": false + } + ], + "Japan": [ + { + "name": "JPN", + "zip": "100-0001", + "result": "JP-13", + "usingGoogle": false + }, + { + "name": "JPN", + "zip": "530-0001", + "result": "JP-27", + "usingGoogle": false + } + ], + "Brazil": [ + { + "name": "BRA", + "zip": "69900-000", + "result": "BR-AC", + "usingGoogle": false + } + ], + "South Korea": [ + { + "name": "KOR", + "zip": "01000", + "result": "KR-11", + "usingGoogle": false + } + ], + "Lithuania": [ + { + "name": "LTU", + "zip": "62001", + "result": "LT-03", + "usingGoogle": false + } + ], + "Luxembourg": [ + { + "name": "LUX", + "zip": "L-4795", + "result": "LU-CA", + "usingGoogle": false + } + ], + "Portugal": [ + { + "name": "PRT", + "zip": "3020-740", + "result": "PT-01", + "usingGoogle": false + } + ], + "Slovakia": [ + { + "name": "SVK", + "zip": "049 01", + "result": "SK-BC", + "usingGoogle": false + } + ], + "Turkey": [ + { + "name": "TUR", + "zip": "01010", + "result": "TR-01", + "usingGoogle": false + } + ], + "Netherlands": [ + { + "name": "NLD", + "zip": "3541", + "result": "NL-UT", + "usingGoogle": false + }, + { + "name": "NLD", + "zip": "6869 VC", + "result": "NL-GE", + "usingGoogle": false + }, + { + "name": "NLD", + "zip": "2294 HC", + "result": "NL-ZH", + "usingGoogle": false + }, + { + "name": "NLD", + "zip": "2294HC", + "result": "NL-ZH", + "usingGoogle": false + } + ], + "Canada": [ + { + "name": "CAN", + "zip": "X1A 0E2", + "result": "CA-NT", + "usingGoogle": false + }, + { + "name": "CAN", + "zip": "X1A 0E2", + "result": "CA-NT", + "usingGoogle": false + }, + { + "name": "CAN", + "zip": "X1A0E2", + "result": "CA-NT", + "usingGoogle": false + }, + { + "name": "CAN", + "zip": "G1P 2J1", + "result": "CA-QC", + "usingGoogle": false + }, + { + "name": "CAN", + "zip": "S0J 0G0", + "result": "CA-SK", + "usingGoogle": false + } + ], + "Mexico": [ + { + "name": "MEX", + "zip": "06000", + "result": "MX-CMX", + "usingGoogle": false + } + ], + "Serbia": [ + { + "name": "SRB", + "zip": "11000", + "result": "RS-00", + "usingGoogle": false + }, + { + "name": "SRB", + "zip": "11320", + "result": "RS-10", + "usingGoogle": false + }, + { + "name": "SRB", + "zip": "18230", + "result": "RS-15", + "usingGoogle": false + }, + { + "name": "SRB", + "zip": "24430", + "result": "RS-03", + "usingGoogle": false + }, + { + "name": "SRB", + "zip": "21000", + "result": "RS-VO", + "usingGoogle": false + }, + { + "name": "SRB", + "zip": "38000", + "result": "RS-KM", + "usingGoogle": false + } + ], + "United States": [ + { + "name": "USA", + "zip": "972022239", + "result": "US-OR", + "usingGoogle": false + }, + { + "name": "USA", + "zip": "06103", + "result": "US-CT", + "usingGoogle": false + }, + { + "name": "USA", + "zip": "00646", + "result": "US-PR", + "usingGoogle": false + }, + { + "name": "USA", + "zip": "96799", + "result": "US-AS", + "usingGoogle": false + }, + { + "name": "USA", + "zip": "96921", + "result": "US-GU", + "usingGoogle": false + }, + { + "name": "USA", + "zip": "96951", + "result": "US-MP", + "usingGoogle": false + }, + { + "name": "USA", + "zip": "00831", + "result": "US-VI", + "usingGoogle": false + }, + { + "name": "USA", + "zip": "10038", + "result": "US-NY", + "usingGoogle": false + } + ], + "Qatar": [ + { + "name": "QAT", + "zip": "06000", + "result": null, + "usingGoogle": false + } + ], + "Sweden": [ + { + "name": "SWE", + "zip": "11337", + "result": "SE-AB", + "usingGoogle": false + }, + { + "name": "SWE", + "zip": "95435", + "result": "SE-BD", + "usingGoogle": false + }, + { + "name": "SWE", + "zip": "23531", + "result": "SE-M", + "usingGoogle": false + }, + { + "name": "SWE", + "zip": "10000", + "result": "SE-AB", + "usingGoogle": false + }, + { + "name": "SWE", + "zip": "99999", + "result": "SE-BD", + "usingGoogle": false + }, + { + "name": "SWE", + "zip": "81999", + "result": "SE-C", + "usingGoogle": false + } + ], + "Finland": [ + { + "name": "FIN", + "zip": "00940", + "result": "FI-18", + "usingGoogle": false + }, + { + "name": "FIN", + "zip": "70840", + "result": "FI-15", + "usingGoogle": false + }, + { + "name": "FIN", + "zip": "96900", + "result": "FI-10", + "usingGoogle": false + }, + { + "name": "FIN", + "zip": "00100", + "result": "FI-18", + "usingGoogle": false + }, + { + "name": "FIN", + "zip": "99990", + "result": "FI-10", + "usingGoogle": false + }, + { + "name": "FIN", + "zip": "20200", + "result": "FI-19", + "usingGoogle": false + }, + { + "name": "FIN", + "zip": "22100", + "result": "FI-01", + "usingGoogle": false + }, + { + "name": "AUS", + "zip": "0841", + "result": "AU-NT", + "usingGoogle": false + }, + { + "name": "AUS", + "zip": "2111", + "result": "AU-NSW", + "usingGoogle": false + } + ] +} diff --git a/test/tests.js b/test/tests.js index 310a7a9..96d6b1e 100644 --- a/test/tests.js +++ b/test/tests.js @@ -1,554 +1,40 @@ -const assert = require('node:assert/strict'); +const { default: axios } = require('axios'); +const { equal, rejects, match } = require('node:assert/strict'); const { describe, it } = require('node:test'); +const { GoogleMapsAPIError } = require('../lib/errors'); const RegionIdentifier = require('../lib/region'); +const countriesPostalCodes = require('./fixtures/countries-postal-codes.json'); const identifier = new RegionIdentifier(''); -const countriesPostalCodes = { - 'Austria': [ - { - name: 'AUT', - zip: '1999', - result: 'AT-9', - usingGoogle: false, - }, - { - name: 'Austria', - usingGoogle: false, - zip: '1999', - result: 'AT-9', - }, - ], - 'Deutschland': [ - { - name: 'DEU', - zip: '6578', - result: 'DE-TH', - usingGoogle: false, - }, - ], - 'Belgium': [ - { - name: 'BEL', - zip: '1701', - result: 'BE-VLG', - usingGoogle: false, - }, - ], - 'Switzerland': [ - { - name: 'CHE', - zip: '2333', - result: 'CH-BE', - usingGoogle: false, - }, - { - name: 'CHE', - zip: '9488', - result: 'CH-SG', - usingGoogle: false, - }, - ], - 'Spain': [ - { - name: 'ESP', - zip: '04004', - result: 'ES-AN', - usingGoogle: false, - }, - ], - 'France': [ - { - name: 'FRA', - zip: '25090', - result: 'FR-I', - usingGoogle: false, - }, - { - name: 'FRA', - zip: '92440', - result: 'FR-J', - usingGoogle: false, - }, - { - name: 'FRA', - zip: '75116', - result: 'FR-J', - usingGoogle: false, - }, - { - name: 'FRA', - zip: '51000', - result: 'FR-G', - usingGoogle: false, - }, - { - name: 'FRA', - zip: '51100', - result: 'FR-G', - usingGoogle: false, - }, - { - name: 'FRA', - zip: '75020', - result: 'FR-J', - usingGoogle: false, - }, - { - name: 'FR', - zip: '25090', - result: 'FR-I', - usingGoogle: false, - }, - { - name: 'FR', - zip: '87200', - result: 'FR-L', - usingGoogle: false, - }, - { - name: 'FR', - zip: '87200 CEDEX 1', - result: 'FR-L', - usingGoogle: false, - }, - { - name: 'FR', - zip: '61100', - result: 'FR-P', - usingGoogle: false, - }, - { - name: 'FR', - zip: '81000', - result: 'FR-N', - usingGoogle: false, - }, - ], - 'United Kingdom': [ - { - name: 'GBR', - zip: 'PA13 4PS', - result: 'GB-SCT', - usingGoogle: false, - }, - { - name: 'GBR', - zip: 'EC2N 1AR', - result: 'GB-ENG', - usingGoogle: false, - }, - { - name: 'GBR', - zip: 'FY83QN', - result: 'GB-ENG', - usingGoogle: false, - }, - { - name: 'GBR', - zip: 'CF466RT', - result: 'GB-WLS', - usingGoogle: false, - }, - { - name: 'GBR', - zip: 'AL10 9BL', - result: 'GB-ENG', - usingGoogle: false, - }, - { - name: 'GBR', - zip: 'm14 5pq', - result: 'GB-ENG', - usingGoogle: false, - }, - { - name: 'GBR', - zip: 'Po10 8Eg', - result: 'GB-ENG', - usingGoogle: false, - }, - { - name: 'GBR', - zip: 'L5 3QN', - result: 'GB-ENG', - usingGoogle: false, - }, - ], - 'Italy': [ - { - name: 'ITA', - zip: '67033', - result: 'IT-65', - usingGoogle: false, - }, - { - name: 'ITA', - zip: '41121', - result: 'IT-45', - usingGoogle: false, - }, - ], - 'India': [ - { - name: 'IND', - zip: '110001', - result: 'IN-DL', - usingGoogle: false, - }, - ], - 'Ireland': [ - { - name: 'IRL', - zip: 'D02 X285', - result: 'IE-L', - usingGoogle: false, - }, - ], - 'Romania': [ - { - name: 'ROU', - zip: '010011', - result: 'RO-B', - usingGoogle: false, - }, - ], - 'Hungary': [ - { - name: 'HUN', - zip: '1011', - result: 'HU-BU', - usingGoogle: false, - }, - ], - 'Japan': [ - { - name: 'JPN', - zip: '100-0001', - result: 'JP-13', - usingGoogle: false, - }, - { - name: 'JPN', - zip: '530-0001', - result: 'JP-27', - usingGoogle: false, - }, - ], - 'Brazil': [ - { - name: 'BRA', - zip: '69900-000', - result: 'BR-AC', - usingGoogle: false, - }, - ], - 'South Korea': [ - { - name: 'KOR', - zip: '01000', - result: 'KR-11', - usingGoogle: false, - }, - ], - 'Lithuania': [ - { - name: 'LTU', - zip: '62001', - result: 'LT-03', - usingGoogle: false, - }, - ], - 'Luxembourg': [ - { - name: 'LUX', - zip: 'L-4795', - result: 'LU-CA', - usingGoogle: false, - }, - ], - 'Portugal': [ - { - name: 'PRT', - zip: '3020-740', - result: 'PT-01', - usingGoogle: false, - }, - ], - 'Slovakia': [ - { - name: 'SVK', - zip: '049 01', - result: 'SK-BC', - usingGoogle: false, - }, - ], - 'Turkey': [ - { - name: 'TUR', - zip: '01010', - result: 'TR-01', - usingGoogle: false, - }, - ], - 'Netherlands': [ - { - name: 'NLD', - zip: '3541', - result: 'NL-UT', - usingGoogle: false, - }, - { - name: 'NLD', - zip: '6869 VC', - result: 'NL-GE', - usingGoogle: false, - }, - { - name: 'NLD', - zip: '2294 HC', - result: 'NL-ZH', - usingGoogle: false, - }, - { - name: 'NLD', - zip: '2294HC', - result: 'NL-ZH', - usingGoogle: false, - }, - ], - 'Canada': [ - { - name: 'CAN', - zip: 'X1A 0E2', - result: 'CA-NT', - usingGoogle: false, - }, - { - name: 'CAN', - zip: 'X1A 0E2', - result: 'CA-NT', - usingGoogle: false, - }, - { - name: 'CAN', - zip: 'X1A0E2', - result: 'CA-NT', - usingGoogle: false, - }, - { - name: 'CAN', - zip: 'G1P 2J1', - result: 'CA-QC', - usingGoogle: false, - }, - { - name: 'CAN', - zip: 'S0J 0G0', - result: 'CA-SK', - usingGoogle: false, - }, - ], - 'Mexico': [ - { - name: 'MEX', - zip: '06000', - result: 'MX-CMX', - usingGoogle: false, - }, - ], - 'Serbia': [ - { - name: 'SRB', - zip: '11000', - result: 'RS-00', - usingGoogle: false, - }, - { - name: 'SRB', - zip: '11320', - result: 'RS-10', - usingGoogle: false, - }, - { - name: 'SRB', - zip: '18230', - result: 'RS-15', - usingGoogle: false, - }, - { - name: 'SRB', - zip: '24430', - result: 'RS-03', - usingGoogle: false, - }, - { - name: 'SRB', - zip: '21000', - result: 'RS-VO', - usingGoogle: false, - }, - { - name: 'SRB', - zip: '38000', - result: 'RS-KM', - usingGoogle: false, - }, - ], - 'United States': [ - { - name: 'USA', - zip: '972022239', - result: 'US-OR', - usingGoogle: false, - }, - { - name: 'USA', - zip: '06103', - result: 'US-CT', - usingGoogle: false, - }, - { - name: 'USA', - zip: '00646', - result: 'US-PR', - usingGoogle: false, - }, - { - name: 'USA', - zip: '96799', - result: 'US-AS', - usingGoogle: false, - }, - { - name: 'USA', - zip: '96921', - result: 'US-GU', - usingGoogle: false, - }, - { - name: 'USA', - zip: '96951', - result: 'US-MP', - usingGoogle: false, - }, - { - name: 'USA', - zip: '00831', - result: 'US-VI', - usingGoogle: false, - }, - { - name: 'USA', - zip: '10038', - result: 'US-NY', - usingGoogle: false, - }, - ], - 'Qatar': [ - { - name: 'QAT', - zip: '06000', - result: null, - usingGoogle: false, - }, - ], - 'Sweden': [ - { - name: 'SWE', - zip: '11337', - result: 'SE-AB', - usingGoogle: false, - }, - { - name: 'SWE', - zip: '95435', - result: 'SE-BD', - usingGoogle: false, - }, - { - name: 'SWE', - zip: '23531', - result: 'SE-M', - usingGoogle: false, - }, - { - name: 'SWE', - zip: '10000', - result: 'SE-AB', - usingGoogle: false, - }, - { - name: 'SWE', - zip: '99999', - result: 'SE-BD', - usingGoogle: false, - }, - { - name: 'SWE', - zip: '81999', - result: 'SE-C', - usingGoogle: false, - }, - ], - 'Finland': [ - { - name: 'FIN', - zip: '00940', - result: 'FI-18', - usingGoogle: false, - }, - { - name: 'FIN', - zip: '70840', - result: 'FI-15', - usingGoogle: false, - }, - { - name: 'FIN', - zip: '96900', - result: 'FI-10', - usingGoogle: false, - }, - { - name: 'FIN', - zip: '00100', - result: 'FI-18', - usingGoogle: false, - }, - { - name: 'FIN', - zip: '99990', - result: 'FI-10', - usingGoogle: false, - }, - { - name: 'FIN', - zip: '20200', - result: 'FI-19', - usingGoogle: false, - }, - { - name: 'FIN', - zip: '22100', - result: 'FI-01', - usingGoogle: false, - }, - { - name: 'AUS', - zip: '0841', - result: 'AU-NT', - usingGoogle: false, - }, - { - name: 'AUS', - zip: '2111', - result: 'AU-NSW', - usingGoogle: false, - }, - ], -}; +function googleMapsResponse(regionShortName) { + return { + data: { + results: [ + { + address_components: [ + { + short_name: regionShortName, + types: ['administrative_area_level_1'], + }, + ], + }, + ], + }, + }; +} + +async function withMockedGoogleGet(mockGet, callback) { + const originalGet = axios.get; + axios.get = mockGet; + + try { + return await callback(); + } finally { + axios.get = originalGet; + } +} describe('REGION IDENTIFIER', () => { Object.entries(countriesPostalCodes).forEach(([countryName, countryPostalCodes]) => { @@ -557,10 +43,86 @@ describe('REGION IDENTIFIER', () => { it(`result for: ${test.name} with zip code: ${test.zip}`, async () => { const [region, googleUsed] = await identifier.get(test.name, test.zip); - assert.equal(region, test.result); - assert.equal(googleUsed, test.usingGoogle); + equal(region, test.result); + equal(googleUsed, test.usingGoogle); }); }); }); }); }); + +describe('Fallback and error handling', () => { + it('returns null without Google Maps when static data has no matching zip code', async () => { + const [region, googleUsed] = await withMockedGoogleGet( + async () => { + throw new Error('Google Maps should not be called for static misses'); + }, + () => identifier.get('DEU', '00000'), + ); + + equal(region, null); + equal(googleUsed, false); + }); + + it('throws when country or zip code is missing', async () => { + await rejects(() => identifier.get('', '12345')); + await rejects(() => identifier.get('DEU', '')); + }); + + it('detects regions with Google Maps when called directly', async () => { + const region = await withMockedGoogleGet( + async (url) => { + match(url, /maps\.googleapis\.com/); + match(url, /country%3DQAT%20postal_code%3D06000/); + return googleMapsResponse('DA'); + }, + () => identifier.detectWithGoogle({ alpha2: 'QA', alpha3: 'QAT' }, '06000'), + ); + + equal(region, 'QA-DA'); + }); + + it('throws when country cannot be resolved', async () => { + await rejects(() => identifier.get('Atlantis', '12345')); + }); + + it('wraps Google Maps request failures', async () => { + await withMockedGoogleGet( + async () => { + throw new Error('network unavailable'); + }, + async () => { + await rejects( + () => identifier.detectWithGoogle({ alpha2: 'QA', alpha3: 'QAT' }, '06000'), + (err) => err instanceof GoogleMapsAPIError, + ); + }, + ); + }); + + it('throws when Google Maps response has no administrative region', async () => { + await withMockedGoogleGet( + async () => ({ + data: { + results: [ + { + address_components: [{ short_name: 'QA', types: ['country'] }], + }, + ], + }, + }), + async () => { + await rejects( + () => identifier.detectWithGoogle({ alpha2: 'QA', alpha3: 'QAT' }, '06000'), + (err) => err instanceof GoogleMapsAPIError, + ); + }, + ); + }); + + it('returns region display names', () => { + equal(identifier.getNameFromCountryAndRegion('DEU', 'DE-TH'), 'Thüringen'); + equal(identifier.getNameFromCountryAndRegion('DEU', 'UNKNOWN'), null); + equal(identifier.getNameFromCountryAndRegion('Atlantis', 'DE-TH'), null); + }); +});