diff --git a/src/commands/base-command.ts b/src/commands/base-command.ts index 8358ef06c2d..a4c467d7fd4 100644 --- a/src/commands/base-command.ts +++ b/src/commands/base-command.ts @@ -39,6 +39,7 @@ import { EXIT_CODES } from '../utils/exit-codes.js' import type { FeatureFlags } from '../utils/feature-flags.js' import { getFrameworksAPIPaths } from '../utils/frameworks-api.js' import { getSiteByName } from '../utils/get-site.js' +import { getLocalApiTestOpts } from '../utils/local-api-test-opts.js' import { buildAuthorizeUrl } from '../utils/login-url.js' import openBrowser from '../utils/open-browser.js' import { isInteractive } from '../utils/scripted-commands.js' @@ -845,6 +846,7 @@ export default class BaseCommand extends Command { host: host, pathPrefix: pathPrefix, scheme: scheme, + testOpts: getLocalApiTestOpts({ host, scheme }), offline: offline ?? flags.offline, siteFeatureFlagPrefix: 'cli', featureFlags: this.featureFlags, diff --git a/src/utils/local-api-test-opts.ts b/src/utils/local-api-test-opts.ts new file mode 100644 index 00000000000..2fa138fcb7a --- /dev/null +++ b/src/utils/local-api-test-opts.ts @@ -0,0 +1,20 @@ +const LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '[::1]']) + +export const getLocalApiTestOpts = ({ + host, + scheme, +}: { + host?: string + scheme?: string +}): { host: string; scheme?: string } | undefined => { + if (host === undefined) { + return undefined + } + + try { + const { hostname } = new URL(`http://${host}`) + return LOOPBACK_HOSTNAMES.has(hostname) ? { host, scheme } : undefined + } catch { + return undefined + } +} diff --git a/tests/integration/commands/dev/v2-api.test.ts b/tests/integration/commands/dev/v2-api.test.ts index f0257274442..6585f5561f8 100644 --- a/tests/integration/commands/dev/v2-api.test.ts +++ b/tests/integration/commands/dev/v2-api.test.ts @@ -67,9 +67,7 @@ describe.concurrent('v2 api', async () => { expect(context).toHaveProperty('ip', expect.stringMatching(/::1|127\.0\.0\.1/)) expect(context).toHaveProperty('geo.city', 'Mock City') expect(context).toHaveProperty('cookies', { foo: 'bar' }) - // TODO: re-enable once the extensions API is mocked. The real one rejects this test's fake token, - // so config resolution falls back to offline and the account is unknown. - // expect(context).toHaveProperty('account.id', 'mock-account-id') + expect(context).toHaveProperty('account.id', 'mock-account-id') expect(context).toHaveProperty('deploy.context', 'dev') expect(context).toHaveProperty('deploy.id', '0') expect(context).toHaveProperty('deploy.published', false) diff --git a/tests/integration/utils/mock-api-vitest.ts b/tests/integration/utils/mock-api-vitest.ts index b99049d25a2..055afe82efc 100644 --- a/tests/integration/utils/mock-api-vitest.ts +++ b/tests/integration/utils/mock-api-vitest.ts @@ -84,6 +84,18 @@ export const startMockApi = ({ routes, silent }: MockApiOptions): Promise { + test('returns host and scheme for localhost', () => { + expect(getLocalApiTestOpts({ host: 'localhost:1234', scheme: 'http' })).toEqual({ + host: 'localhost:1234', + scheme: 'http', + }) + }) + + test('returns host and scheme for 127.0.0.1', () => { + expect(getLocalApiTestOpts({ host: '127.0.0.1:1234', scheme: 'http' })).toEqual({ + host: '127.0.0.1:1234', + scheme: 'http', + }) + }) + + test('returns host and scheme for [::1]', () => { + expect(getLocalApiTestOpts({ host: '[::1]:1234', scheme: 'http' })).toEqual({ + host: '[::1]:1234', + scheme: 'http', + }) + }) + + test.each(['api.netlify.com', 'api-staging.netlify.com', 'example.com:8080', 'localhost.example.com'])( + 'returns undefined for non-loopback host %s', + (host) => { + expect(getLocalApiTestOpts({ host, scheme: 'https' })).toBeUndefined() + }, + ) + + test('returns undefined when host is undefined', () => { + expect(getLocalApiTestOpts({ host: undefined, scheme: 'http' })).toBeUndefined() + }) + + test('returns undefined when host cannot be parsed', () => { + expect(getLocalApiTestOpts({ host: 'not a host', scheme: 'http' })).toBeUndefined() + }) +})