diff --git a/docs/commands/dev.md b/docs/commands/dev.md index 63be7e21482..e64191b051d 100644 --- a/docs/commands/dev.md +++ b/docs/commands/dev.md @@ -18,6 +18,7 @@ netlify dev **Flags** - `command` (*string*) - command to run +- `country` (*string*) - Two-letter country code (ISO 3166-1 alpha-2, https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements) to use as mock geolocation (enables --geo=mock autmatically) - `dir` (*string*) - dir with static files - `edgeInspect` (*string*) - enable the V8 Inspector Protocol for Edge Functions, with an optional address in the host:port format - `edgeInspectBrk` (*string*) - enable the V8 Inspector Protocol for Edge Functions and pause execution on the first line of code, with an optional address in the host:port format diff --git a/src/commands/dev/dev.js b/src/commands/dev/dev.js index 79f82dd722b..392a3717431 100644 --- a/src/commands/dev/dev.js +++ b/src/commands/dev/dev.js @@ -234,6 +234,7 @@ const FRAMEWORK_PORT_TIMEOUT = 6e5 * @param {InspectSettings} params.inspectSettings * @param {() => Promise} params.getUpdatedConfig * @param {string} params.geolocationMode + * @param {string} params.geoCountry * @param {*} params.settings * @param {boolean} params.offline * @param {*} params.site @@ -243,6 +244,7 @@ const FRAMEWORK_PORT_TIMEOUT = 6e5 const startProxyServer = async ({ addonsUrls, config, + geoCountry, geolocationMode, getUpdatedConfig, inspectSettings, @@ -256,6 +258,7 @@ const startProxyServer = async ({ config, configPath: site.configPath, geolocationMode, + geoCountry, getUpdatedConfig, inspectSettings, offline, @@ -386,6 +389,19 @@ const validateShortFlagArgs = (args) => { return args } +const validateGeoCountryCode = (arg) => { + // Validate that the arg passed is two letters only for country + // See https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes + if (!/^[a-z]{2}$/i.test(arg)) { + throw new Error( + `The geo country code must use a two letter abbreviation. + ${chalk.red(BANG)} Example: + netlify dev --geo=mock --country=FR`, + ) + } + return arg.toUpperCase() +} + /** * The dev command * @param {import('commander').OptionValues} options @@ -460,6 +476,7 @@ const dev = async (options, command) => { addonsUrls, config, geolocationMode: options.geo, + geoCountry: options.country, getUpdatedConfig, inspectSettings, offline: options.offline, @@ -599,6 +616,12 @@ const createDevCommand = (program) => { .choices(['cache', 'mock', 'update']) .default('cache'), ) + .addOption( + new Option( + '--country ', + 'Two-letter country code (ISO 3166-1 alpha-2, https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements) to use as mock geolocation (enables --geo=mock autmatically)', + ).argParser(validateGeoCountryCode), + ) .addOption( new Option('--staticServerPort ', 'port of the static app server used when no framework is detected') .argParser((value) => Number.parseInt(value)) diff --git a/src/lib/edge-functions/proxy.js b/src/lib/edge-functions/proxy.js index 3a4ecc2faed..202bb6dc44c 100644 --- a/src/lib/edge-functions/proxy.js +++ b/src/lib/edge-functions/proxy.js @@ -48,6 +48,7 @@ const handleProxyRequest = (req, proxyReq) => { const initializeProxy = async ({ config, configPath, + geoCountry, geolocationMode, getUpdatedConfig, inspectSettings, @@ -84,7 +85,7 @@ const initializeProxy = async ({ } const [geoLocation, registry] = await Promise.all([ - getGeoLocation({ mode: geolocationMode, offline, state }), + getGeoLocation({ mode: geolocationMode, geoCountry, offline, state }), server, ]) diff --git a/src/lib/geo-location.js b/src/lib/geo-location.js index 4d2cc25bc81..259443e0d3f 100644 --- a/src/lib/geo-location.js +++ b/src/lib/geo-location.js @@ -3,7 +3,6 @@ const fetch = require('node-fetch') const API_URL = 'https://netlifind.netlify.app' const STATE_GEO_PROPERTY = 'geolocation' - // 24 hours const CACHE_TTL = 8.64e7 @@ -17,12 +16,11 @@ const REQUEST_TIMEOUT = 1e4 * @property {object} country * @property {string} country.code * @property {string} country.name - * @property {object} country - * @property {string} country.code - * @property {string} country.name + * @property {object} subdivision + * @property {string} subdivision.code + * @property {string} subdivision.name */ -// The default location to be used if we're unable to talk to the API. const mockLocation = { city: 'San Francisco', country: { code: 'US', name: 'United States' }, @@ -34,13 +32,13 @@ const mockLocation = { * location, depending on the mode selected. * * @param {object} params - * @param {string} params.geolocationMode * @param {"cache"|"update"|"mock"} params.mode + * @param {string} params.geoCountry * @param {boolean} params.offline * @param {import('../utils/state-config').StateConfig} params.state * @returns {Promise} */ -const getGeoLocation = async ({ mode, offline, state }) => { +const getGeoLocation = async ({ geoCountry, mode, offline, state }) => { const cacheObject = state.get(STATE_GEO_PROPERTY) // If we have cached geolocation data and the `--geo` option is set to @@ -56,10 +54,18 @@ const getGeoLocation = async ({ mode, offline, state }) => { } } - // If the `--geo` option is set to `mock`, we use the mock location. Also, - // if the `--offline` option was used, we can't talk to the API, so let's - // also use the mock location. - if (mode === 'mock' || offline) { + // If the `--geo` option is set to `mock`, we use the default mock location. + // If the `--offline` option was used, we can't talk to the API, so let's + // also use the mock location. Otherwise, use the country code passed in by + // the user. + if (mode === 'mock' || offline || geoCountry) { + if (geoCountry) { + return { + city: 'Mock City', + country: { code: geoCountry, name: 'Mock Country' }, + subdivision: { code: 'SD', name: 'Mock Subdivision' }, + } + } return mockLocation } diff --git a/src/utils/proxy.js b/src/utils/proxy.js index d005013fcce..28b968b06bf 100644 --- a/src/utils/proxy.js +++ b/src/utils/proxy.js @@ -465,6 +465,7 @@ const startProxy = async function ({ addonsUrls, config, configPath, + geoCountry, geolocationMode, getUpdatedConfig, inspectSettings, @@ -478,6 +479,7 @@ const startProxy = async function ({ config, configPath, geolocationMode, + geoCountry, getUpdatedConfig, inspectSettings, offline, diff --git a/tests/integration/660.command.dev.geo.test.js b/tests/integration/660.command.dev.geo.test.js new file mode 100644 index 00000000000..62fb0384157 --- /dev/null +++ b/tests/integration/660.command.dev.geo.test.js @@ -0,0 +1,22 @@ +const process = require('process') + +const test = require('ava') + +const callCli = require('./utils/call-cli') +const { withSiteBuilder } = require('./utils/site-builder') + +test('should throw if invalid country arg is passed', async (t) => { + await withSiteBuilder('site-env', async (builder) => { + await builder.buildAsync() + + const options = { + cwd: builder.directory, + extendEnv: false, + PATH: process.env.PATH, + } + + await t.throwsAsync(() => callCli(['dev', '--geo=mock', '--country=a1'], options)) + await t.throwsAsync(() => callCli(['dev', '--geo=mock', '--country=NotARealCountryCode'], options)) + await t.throwsAsync(() => callCli(['dev', '--geo=mock', '--country='], options)) + }) +}) diff --git a/tests/unit/lib/geo-location.test.js b/tests/unit/lib/geo-location.test.js index 8b47cdce279..41fd46109a8 100644 --- a/tests/unit/lib/geo-location.test.js +++ b/tests/unit/lib/geo-location.test.js @@ -116,3 +116,37 @@ test('`getGeoLocation` returns mock geolocation data if `mode: "mock"`', async ( t.false(hasCalledStateSet) t.deepEqual(geo, mockLocation) }) + +test('`getGeoLocation` returns mock geolocation data if valid country code set', async (t) => { + const returnedLocation = { + city: 'Mock City', + country: { code: 'CA', name: 'Mock Country' }, + subdivision: { code: 'SD', name: 'Mock Subdivision' }, + } + + const mockState = { + get() {}, + set() {}, + } + + const geo = await getGeoLocation({ mode: 'mock', state: mockState, geoCountry: 'CA' }) + + t.deepEqual(geo, returnedLocation) +}) + +test('`getGeoLocation` mocks country code when not using mock flag', async (t) => { + const mockState = { + get() {}, + set() {}, + } + + const returnedLocation = { + city: 'Mock City', + country: { code: 'CA', name: 'Mock Country' }, + subdivision: { code: 'SD', name: 'Mock Subdivision' }, + } + + const geo = await getGeoLocation({ mode: 'update', offline: false, state: mockState, geoCountry: 'CA' }) + + t.deepEqual(geo, returnedLocation) +})