From 88624138943108c8b99970af12a63aab62ffcb4d Mon Sep 17 00:00:00 2001 From: CalebBarnes Date: Wed, 4 Jun 2025 18:17:01 -0700 Subject: [PATCH 1/5] fix: only auto-install extensions when required packages are present Previously, extensions were being auto-installed on all sites with the feature flag enabled, regardless of whether the required packages were present in package.json. This caused mass installations of extensions like Neon on accounts that didn't need them. Now properly filters extensions to only install when: 1. Extension is not already installed, AND 2. At least one required package exists in project dependencies Fixes the logic in handleAutoInstallExtensions to check package.json dependencies before attempting installation. --- .../utils/extensions/auto-install-extensions.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/config/src/utils/extensions/auto-install-extensions.ts b/packages/config/src/utils/extensions/auto-install-extensions.ts index 751e62d1a9..eefc04c6b9 100644 --- a/packages/config/src/utils/extensions/auto-install-extensions.ts +++ b/packages/config/src/utils/extensions/auto-install-extensions.ts @@ -102,9 +102,19 @@ export async function handleAutoInstallExtensions({ } const autoInstallableExtensions = await fetchAutoInstallableExtensionsMeta() - const extensionsToInstall = autoInstallableExtensions.filter((ext) => { - return !integrations?.some((integration) => integration.slug === ext.slug) + const enabledExtensionSlugs = new Set((integrations ?? []).map(({ slug }) => slug)) + const extensionsToInstallCandidates = autoInstallableExtensions.filter( + ({ slug }) => !enabledExtensionSlugs.has(slug), + ) + const extensionsToInstall = extensionsToInstallCandidates.filter(({ packages }) => { + for (const pkg of packages) { + if (Object.hasOwn(packageJson.dependencies, pkg)) { + return true + } + } + return false }) + if (extensionsToInstall.length === 0) { return integrations } From d76d8399c2cfd7502fcb4e9ddad12224013e04c8 Mon Sep 17 00:00:00 2001 From: CalebBarnes Date: Wed, 4 Jun 2025 18:39:32 -0700 Subject: [PATCH 2/5] fix: use buildDir for package.json detection in auto-install-extensions Previously, auto-install extensions used `cwd` to look for package.json, but this isn't always the correct location. The config system calculates `buildDir` which represents the proper base directory where package.json should be found (either `build.base` or `repositoryRoot`). Changes: - Pass `buildDir` instead of `cwd` to handleAutoInstallExtensions - Update parameter and error logging to use buildDir - Gracefully fail if no package.json found in buildDir - Maintain existing package detection logic without new dependencies --- packages/config/src/main.ts | 2 +- .../extensions/auto-install-extensions.ts | 31 +++++++++++-------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/packages/config/src/main.ts b/packages/config/src/main.ts index e24125e34b..e016bbc719 100644 --- a/packages/config/src/main.ts +++ b/packages/config/src/main.ts @@ -176,7 +176,7 @@ export const resolveConfig = async function (opts): Promise { siteId, accountId, token, - cwd, + buildDir, extensionApiBaseUrl, testOpts, offline, diff --git a/packages/config/src/utils/extensions/auto-install-extensions.ts b/packages/config/src/utils/extensions/auto-install-extensions.ts index eefc04c6b9..a8cbb71ce8 100644 --- a/packages/config/src/utils/extensions/auto-install-extensions.ts +++ b/packages/config/src/utils/extensions/auto-install-extensions.ts @@ -8,8 +8,13 @@ import { type ModeOption } from '../../types/options.js' import { fetchAutoInstallableExtensionsMeta, installExtension } from './utils.js' function getPackageJSON(directory: string) { - const require = createRequire(join(directory, 'package.json')) - return require('./package.json') + try { + const require = createRequire(join(directory, 'package.json')) + return require('./package.json') + } catch { + // Gracefully fail if no package.json found in buildDir + return {} + } } interface AutoInstallOptions { @@ -17,7 +22,7 @@ interface AutoInstallOptions { siteId: string accountId: string token: string - cwd: string + buildDir: string integrations: IntegrationResponse[] offline: boolean testOpts: any @@ -30,7 +35,7 @@ export async function handleAutoInstallExtensions({ siteId, accountId, token, - cwd, + buildDir, integrations, offline, testOpts = {}, @@ -44,7 +49,7 @@ export async function handleAutoInstallExtensions({ console.error("Failed to auto install extension(s): Missing 'accountId'", { accountId, siteId, - cwd, + buildDir, offline, mode, }) @@ -54,7 +59,7 @@ export async function handleAutoInstallExtensions({ console.error("Failed to auto install extension(s): Missing 'siteId'", { accountId, siteId, - cwd, + buildDir, offline, mode, }) @@ -64,17 +69,17 @@ export async function handleAutoInstallExtensions({ console.error("Failed to auto install extension(s): Missing 'token'", { accountId, siteId, - cwd, + buildDir, offline, mode, }) return integrations } - if (!cwd) { - console.error("Failed to auto install extension(s): Missing 'cwd'", { + if (!buildDir) { + console.error("Failed to auto install extension(s): Missing 'buildDir'", { accountId, siteId, - cwd, + buildDir, offline, mode, }) @@ -84,7 +89,7 @@ export async function handleAutoInstallExtensions({ console.error("Failed to auto install extension(s): Running as 'offline'", { accountId, siteId, - cwd, + buildDir, offline, mode, }) @@ -92,7 +97,7 @@ export async function handleAutoInstallExtensions({ } try { - const packageJson = getPackageJSON(cwd) + const packageJson = getPackageJSON(buildDir) if ( !packageJson?.dependencies || typeof packageJson?.dependencies !== 'object' || @@ -108,7 +113,7 @@ export async function handleAutoInstallExtensions({ ) const extensionsToInstall = extensionsToInstallCandidates.filter(({ packages }) => { for (const pkg of packages) { - if (Object.hasOwn(packageJson.dependencies, pkg)) { + if (packageJson?.dependencies && Object.hasOwn(packageJson.dependencies, pkg)) { return true } } From 67ee7e32eeb60d5d1711b44340ff63a46f0a1d45 Mon Sep 17 00:00:00 2001 From: CalebBarnes Date: Wed, 4 Jun 2025 19:14:13 -0700 Subject: [PATCH 3/5] fix: use buildDir for package.json detection and add comprehensive tests - Use buildDir instead of cwd when reading package.json in auto-install extensions - Add extensionApiBaseUrl parameter to fetchAutoInstallableExtensionsMeta for test mocking - Update main config to use testOpts.host for extension API calls in test environment - Add comprehensive test suite covering: - Feature flag disabled/enabled scenarios - Missing package.json graceful handling - Package detection logic (with/without required packages) - buildDir resolution with and without netlify.toml - Mock auto-installable extensions API for deterministic testing Fixes mass installation bug where extensions were installed on all sites regardless of whether required packages were present in dependencies. --- packages/config/src/main.ts | 10 +- .../extensions/auto-install-extensions.ts | 2 +- packages/config/src/utils/extensions/utils.ts | 7 +- .../no_netlify_toml_with_neon/package.json | 8 + .../fixtures/no_package_json/netlify.toml | 3 + .../fixtures/with_neon_package/netlify.toml | 3 + .../fixtures/with_neon_package/package.json | 7 + .../fixtures/without_packages/netlify.toml | 3 + .../fixtures/without_packages/package.json | 8 + packages/config/tests/extensions/tests.js | 178 ++++++++++++++++++ 10 files changed, 223 insertions(+), 6 deletions(-) create mode 100644 packages/config/tests/extensions/fixtures/no_netlify_toml_with_neon/package.json create mode 100644 packages/config/tests/extensions/fixtures/no_package_json/netlify.toml create mode 100644 packages/config/tests/extensions/fixtures/with_neon_package/netlify.toml create mode 100644 packages/config/tests/extensions/fixtures/with_neon_package/package.json create mode 100644 packages/config/tests/extensions/fixtures/without_packages/netlify.toml create mode 100644 packages/config/tests/extensions/fixtures/without_packages/package.json create mode 100644 packages/config/tests/extensions/tests.js diff --git a/packages/config/src/main.ts b/packages/config/src/main.ts index e016bbc719..cc9e95d6f1 100644 --- a/packages/config/src/main.ts +++ b/packages/config/src/main.ts @@ -74,9 +74,13 @@ export const resolveConfig = async function (opts): Promise { } // TODO(kh): remove this mapping and get the extensionApiHost from the opts - const extensionApiBaseUrl = host?.includes(NETLIFY_API_STAGING_BASE_URL) - ? EXTENSION_API_STAGING_BASE_URL - : EXTENSION_API_BASE_URL + let extensionApiBaseUrl = EXTENSION_API_BASE_URL + if (host?.includes(NETLIFY_API_STAGING_BASE_URL)) { + extensionApiBaseUrl = EXTENSION_API_STAGING_BASE_URL + } else if (testOpts?.host) { + // For test servers - use testOpts.host for extension API calls + extensionApiBaseUrl = `http://${testOpts.host}` + } const { config: configOpt, diff --git a/packages/config/src/utils/extensions/auto-install-extensions.ts b/packages/config/src/utils/extensions/auto-install-extensions.ts index a8cbb71ce8..738535c0d1 100644 --- a/packages/config/src/utils/extensions/auto-install-extensions.ts +++ b/packages/config/src/utils/extensions/auto-install-extensions.ts @@ -106,7 +106,7 @@ export async function handleAutoInstallExtensions({ return integrations } - const autoInstallableExtensions = await fetchAutoInstallableExtensionsMeta() + const autoInstallableExtensions = await fetchAutoInstallableExtensionsMeta(extensionApiBaseUrl) const enabledExtensionSlugs = new Set((integrations ?? []).map(({ slug }) => slug)) const extensionsToInstallCandidates = autoInstallableExtensions.filter( ({ slug }) => !enabledExtensionSlugs.has(slug), diff --git a/packages/config/src/utils/extensions/utils.ts b/packages/config/src/utils/extensions/utils.ts index 59d155f3f4..dd050b76b6 100644 --- a/packages/config/src/utils/extensions/utils.ts +++ b/packages/config/src/utils/extensions/utils.ts @@ -64,8 +64,11 @@ type AutoInstallableExtensionMeta = { * * @returns Array of extensions with their associated packages */ -export async function fetchAutoInstallableExtensionsMeta(): Promise { - const url = new URL(`/meta/auto-installable`, process.env.EXTENSION_API_BASE_URL ?? EXTENSION_API_BASE_URL) +export async function fetchAutoInstallableExtensionsMeta( + extensionApiBaseUrl?: string, +): Promise { + const baseUrl = extensionApiBaseUrl ?? process.env.EXTENSION_API_BASE_URL ?? EXTENSION_API_BASE_URL + const url = new URL(`/meta/auto-installable`, baseUrl) const response = await fetch(url.toString()) if (!response.ok) { throw new Error(`Failed to fetch extensions meta`) diff --git a/packages/config/tests/extensions/fixtures/no_netlify_toml_with_neon/package.json b/packages/config/tests/extensions/fixtures/no_netlify_toml_with_neon/package.json new file mode 100644 index 0000000000..93e182e00e --- /dev/null +++ b/packages/config/tests/extensions/fixtures/no_netlify_toml_with_neon/package.json @@ -0,0 +1,8 @@ +{ + "name": "test-project-no-netlify-toml-with-neon", + "version": "1.0.0", + "dependencies": { + "@netlify/neon": "^1.0.0", + "react": "^18.0.0" + } +} diff --git a/packages/config/tests/extensions/fixtures/no_package_json/netlify.toml b/packages/config/tests/extensions/fixtures/no_package_json/netlify.toml new file mode 100644 index 0000000000..e1d3d0aaea --- /dev/null +++ b/packages/config/tests/extensions/fixtures/no_package_json/netlify.toml @@ -0,0 +1,3 @@ +[build] + command = "echo 'no package.json here'" + publish = "dist" diff --git a/packages/config/tests/extensions/fixtures/with_neon_package/netlify.toml b/packages/config/tests/extensions/fixtures/with_neon_package/netlify.toml new file mode 100644 index 0000000000..a1680b2fff --- /dev/null +++ b/packages/config/tests/extensions/fixtures/with_neon_package/netlify.toml @@ -0,0 +1,3 @@ +[build] + command = "npm run build" + publish = "dist" diff --git a/packages/config/tests/extensions/fixtures/with_neon_package/package.json b/packages/config/tests/extensions/fixtures/with_neon_package/package.json new file mode 100644 index 0000000000..2e498df9dc --- /dev/null +++ b/packages/config/tests/extensions/fixtures/with_neon_package/package.json @@ -0,0 +1,7 @@ +{ + "name": "test-project-with-neon", + "version": "1.0.0", + "dependencies": { + "@netlify/neon": "^0.1.0" + } +} diff --git a/packages/config/tests/extensions/fixtures/without_packages/netlify.toml b/packages/config/tests/extensions/fixtures/without_packages/netlify.toml new file mode 100644 index 0000000000..a1680b2fff --- /dev/null +++ b/packages/config/tests/extensions/fixtures/without_packages/netlify.toml @@ -0,0 +1,3 @@ +[build] + command = "npm run build" + publish = "dist" diff --git a/packages/config/tests/extensions/fixtures/without_packages/package.json b/packages/config/tests/extensions/fixtures/without_packages/package.json new file mode 100644 index 0000000000..7428d704ac --- /dev/null +++ b/packages/config/tests/extensions/fixtures/without_packages/package.json @@ -0,0 +1,8 @@ +{ + "name": "test-project-without-extension-packages", + "version": "1.0.0", + "dependencies": { + "react": "^18.0.0", + "lodash": "^4.17.21" + } +} diff --git a/packages/config/tests/extensions/tests.js b/packages/config/tests/extensions/tests.js new file mode 100644 index 0000000000..39c53e6e9b --- /dev/null +++ b/packages/config/tests/extensions/tests.js @@ -0,0 +1,178 @@ +import { Fixture } from '@netlify/testing' +import test from 'ava' + +const SITE_INFO_DATA = { + path: '/api/v1/sites/test', + response: { id: 'test', name: 'test' }, +} + +const TEAM_INSTALLATIONS_META_RESPONSE = { + path: '/team/account1/integrations/installations/meta/test', + response: [], +} + +const FETCH_INTEGRATIONS_EMPTY_RESPONSE = { + path: '/integrations', + response: [], +} + +// Mock response for auto-installable extensions API +const AUTO_INSTALLABLE_EXTENSIONS_RESPONSE = { + path: '/meta/auto-installable', + response: [ + { + slug: 'neon', + hostSiteUrl: 'https://neon-extension.netlify.app', + packages: ['@netlify/neon'], + }, + ], +} + +test('Auto-install extensions: feature flag disabled returns integrations unchanged', async (t) => { + const { output } = await new Fixture('./fixtures/with_neon_package') + .withFlags({ + siteId: 'test', + accountId: 'account1', + token: 'test', + mode: 'dev', + featureFlags: { + auto_install_required_extensions: false, + }, + }) + .runConfigServer([ + SITE_INFO_DATA, + TEAM_INSTALLATIONS_META_RESPONSE, + FETCH_INTEGRATIONS_EMPTY_RESPONSE, + AUTO_INSTALLABLE_EXTENSIONS_RESPONSE, + ]) + + const config = JSON.parse(output) + + // Should not have attempted to install any extensions + t.false(output.includes('Installing extension')) + t.assert(config.integrations) + t.is(config.integrations.length, 0) +}) + +test('Auto-install extensions: gracefully handles missing package.json', async (t) => { + const { output } = await new Fixture('./fixtures/no_package_json') + .withFlags({ + siteId: 'test', + accountId: 'account1', + token: 'test', + mode: 'dev', + featureFlags: { + auto_install_required_extensions: true, + }, + }) + .runConfigServer([ + SITE_INFO_DATA, + TEAM_INSTALLATIONS_META_RESPONSE, + FETCH_INTEGRATIONS_EMPTY_RESPONSE, + AUTO_INSTALLABLE_EXTENSIONS_RESPONSE, + ]) + + const config = JSON.parse(output) + + // Should not have attempted to install any extensions + t.false(output.includes('Installing extension')) + t.assert(config.integrations) + t.is(config.integrations.length, 0) +}) + +test('Auto-install extensions: correctly reads package.json from buildDir', async (t) => { + // This test verifies that the function correctly reads package.json from buildDir + const { output, requests } = await new Fixture('./fixtures/with_neon_package') + .withFlags({ + siteId: 'test', + accountId: 'account1', + token: 'test', + mode: 'dev', + featureFlags: { + auto_install_required_extensions: true, + }, + }) + .runConfigServer([ + SITE_INFO_DATA, + TEAM_INSTALLATIONS_META_RESPONSE, + FETCH_INTEGRATIONS_EMPTY_RESPONSE, + AUTO_INSTALLABLE_EXTENSIONS_RESPONSE, + ]) + + const config = JSON.parse(output) + + // Should have found package.json in buildDir + t.assert(config.integrations) + t.assert(config.buildDir) + t.true(config.buildDir.includes('with_neon_package')) + + // Should have made a request to fetch auto-installable extensions + const autoInstallRequest = requests.find((request) => request.url.includes('/meta/auto-installable')) + t.assert(autoInstallRequest, 'Should have fetched auto-installable extensions') +}) + +test('Auto-install extensions: does not install when required packages are missing', async (t) => { + // This test uses a fixture that has dependencies but not the extension packages + const { output, requests } = await new Fixture('./fixtures/without_packages') + .withFlags({ + siteId: 'test', + accountId: 'account1', + token: 'test', + mode: 'dev', + featureFlags: { + auto_install_required_extensions: true, + }, + }) + .runConfigServer([ + SITE_INFO_DATA, + TEAM_INSTALLATIONS_META_RESPONSE, + FETCH_INTEGRATIONS_EMPTY_RESPONSE, + AUTO_INSTALLABLE_EXTENSIONS_RESPONSE, + ]) + + const config = JSON.parse(output) + + // Should not attempt to install extensions since required packages are missing + t.false(output.includes('Installing extension')) + t.assert(config.integrations) + t.is(config.integrations.length, 0) + + // Should have made a request to fetch auto-installable extensions + const autoInstallRequest = requests.find((request) => request.url.includes('/meta/auto-installable')) + t.assert(autoInstallRequest, 'Should have fetched auto-installable extensions') +}) + +test('Auto-install extensions: correctly reads package.json when no netlify.toml exists', async (t) => { + // This test verifies buildDir resolution works correctly when there's no netlify.toml + // but package.json exists with extension packages + const { output, requests } = await new Fixture('./fixtures/no_netlify_toml_with_neon') + .withFlags({ + siteId: 'test', + accountId: 'account1', + token: 'test', + mode: 'dev', + featureFlags: { + auto_install_required_extensions: true, + }, + }) + .runConfigServer([ + SITE_INFO_DATA, + TEAM_INSTALLATIONS_META_RESPONSE, + FETCH_INTEGRATIONS_EMPTY_RESPONSE, + AUTO_INSTALLABLE_EXTENSIONS_RESPONSE, + ]) + + const config = JSON.parse(output) + + // Should have found package.json in buildDir even without netlify.toml + t.assert(config.integrations) + t.assert(config.buildDir) + t.true(config.buildDir.includes('no_netlify_toml_with_neon')) + + // buildDir should be the repository root since there's no build.base config + t.true(config.buildDir.endsWith('no_netlify_toml_with_neon')) + + // Should have made a request to fetch auto-installable extensions + const autoInstallRequest = requests.find((request) => request.url.includes('/meta/auto-installable')) + t.assert(autoInstallRequest, 'Should have fetched auto-installable extensions') +}) From 71b85ff06bcf47a9544653a15d9c518eac00a2b0 Mon Sep 17 00:00:00 2001 From: CalebBarnes Date: Wed, 4 Jun 2025 19:33:52 -0700 Subject: [PATCH 4/5] test: add comprehensive HTTP mocking for extension installation endpoints - Add global fetch mock to intercept external extension installation requests - Mock installation endpoint responses without modifying implementation code - Track installation requests for comprehensive test assertions - Verify correct HTTP method, URL, headers, and request body - Add unique mock response identifiers for verification - Maintain clean separation between test and production code This approach properly mocks external HTTP requests using test-only code, ensuring no real network calls are made during testing while preserving the original implementation behavior. --- packages/config/tests/extensions/tests.js | 56 ++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/packages/config/tests/extensions/tests.js b/packages/config/tests/extensions/tests.js index 39c53e6e9b..2b28351629 100644 --- a/packages/config/tests/extensions/tests.js +++ b/packages/config/tests/extensions/tests.js @@ -1,6 +1,40 @@ import { Fixture } from '@netlify/testing' import test from 'ava' +// Mock fetch for external extension installation requests +const originalFetch = globalThis.fetch +const mockInstallationResponse = { success: true, mocked: true, testId: 'MOCK_RESPONSE_12345' } + +// Track installation requests for testing +let installationRequests = [] + +const mockFetch = async (url, options) => { + // Convert URL object to string if needed + const urlString = url.toString() + + // If it's an installation request to an external extension URL + if (urlString.includes('/.netlify/functions/handler/on-install')) { + installationRequests.push({ url: urlString, options }) + return { + ok: true, + status: 200, + json: async () => mockInstallationResponse, + text: async () => JSON.stringify(mockInstallationResponse), + } + } + + // For all other requests, use the original fetch + return originalFetch(url, options) +} + +// Set up global fetch mock +globalThis.fetch = mockFetch + +// Reset installation requests before each test +test.beforeEach(() => { + installationRequests = [] +}) + const SITE_INFO_DATA = { path: '/api/v1/sites/test', response: { id: 'test', name: 'test' }, @@ -22,7 +56,7 @@ const AUTO_INSTALLABLE_EXTENSIONS_RESPONSE = { response: [ { slug: 'neon', - hostSiteUrl: 'https://neon-extension.netlify.app', + hostSiteUrl: 'https://neon-extension.netlify.app', // Mocked by fetch mock packages: ['@netlify/neon'], }, ], @@ -109,6 +143,19 @@ test('Auto-install extensions: correctly reads package.json from buildDir', asyn // Should have made a request to fetch auto-installable extensions const autoInstallRequest = requests.find((request) => request.url.includes('/meta/auto-installable')) t.assert(autoInstallRequest, 'Should have fetched auto-installable extensions') + + // Should have attempted to install the extension (mocked) + t.assert(installationRequests.length > 0, 'Should have attempted to install extension') + t.assert( + installationRequests[0].url.includes('/.netlify/functions/handler/on-install'), + 'Should have called installation endpoint', + ) + t.assert( + installationRequests[0].url.includes('neon-extension.netlify.app'), + 'Should have called correct external URL', + ) + t.assert(installationRequests[0].options.method === 'POST', 'Should use POST method') + t.assert(installationRequests[0].options.body.includes('account1'), 'Should include team ID in request body') }) test('Auto-install extensions: does not install when required packages are missing', async (t) => { @@ -175,4 +222,11 @@ test('Auto-install extensions: correctly reads package.json when no netlify.toml // Should have made a request to fetch auto-installable extensions const autoInstallRequest = requests.find((request) => request.url.includes('/meta/auto-installable')) t.assert(autoInstallRequest, 'Should have fetched auto-installable extensions') + + // Should have attempted to install the extension + t.assert(installationRequests.length > 0, 'Should have attempted to install extension') + t.assert( + installationRequests[0].url.includes('/.netlify/functions/handler/on-install'), + 'Should have called installation endpoint', + ) }) From 70c790688c601bdd7af09a583038d543b1e65b43 Mon Sep 17 00:00:00 2001 From: CalebBarnes Date: Wed, 4 Jun 2025 19:44:52 -0700 Subject: [PATCH 5/5] refactor: use pure test-only HTTP mocking without implementation changes - Revert implementation changes to fetchAutoInstallableExtensionsMeta and main.ts - Extend global fetch mock to handle both installation and extension API endpoints - Remove server-side mocks in favor of pure fetch interception - Maintain clean separation between test and production code This approach provides complete HTTP mocking without polluting the implementation with test-specific logic, following proper testing best practices. --- packages/config/src/main.ts | 10 +-- .../extensions/auto-install-extensions.ts | 2 +- packages/config/src/utils/extensions/utils.ts | 7 +- packages/config/tests/extensions/tests.js | 66 +++++++------------ 4 files changed, 30 insertions(+), 55 deletions(-) diff --git a/packages/config/src/main.ts b/packages/config/src/main.ts index cc9e95d6f1..e016bbc719 100644 --- a/packages/config/src/main.ts +++ b/packages/config/src/main.ts @@ -74,13 +74,9 @@ export const resolveConfig = async function (opts): Promise { } // TODO(kh): remove this mapping and get the extensionApiHost from the opts - let extensionApiBaseUrl = EXTENSION_API_BASE_URL - if (host?.includes(NETLIFY_API_STAGING_BASE_URL)) { - extensionApiBaseUrl = EXTENSION_API_STAGING_BASE_URL - } else if (testOpts?.host) { - // For test servers - use testOpts.host for extension API calls - extensionApiBaseUrl = `http://${testOpts.host}` - } + const extensionApiBaseUrl = host?.includes(NETLIFY_API_STAGING_BASE_URL) + ? EXTENSION_API_STAGING_BASE_URL + : EXTENSION_API_BASE_URL const { config: configOpt, diff --git a/packages/config/src/utils/extensions/auto-install-extensions.ts b/packages/config/src/utils/extensions/auto-install-extensions.ts index 738535c0d1..a8cbb71ce8 100644 --- a/packages/config/src/utils/extensions/auto-install-extensions.ts +++ b/packages/config/src/utils/extensions/auto-install-extensions.ts @@ -106,7 +106,7 @@ export async function handleAutoInstallExtensions({ return integrations } - const autoInstallableExtensions = await fetchAutoInstallableExtensionsMeta(extensionApiBaseUrl) + const autoInstallableExtensions = await fetchAutoInstallableExtensionsMeta() const enabledExtensionSlugs = new Set((integrations ?? []).map(({ slug }) => slug)) const extensionsToInstallCandidates = autoInstallableExtensions.filter( ({ slug }) => !enabledExtensionSlugs.has(slug), diff --git a/packages/config/src/utils/extensions/utils.ts b/packages/config/src/utils/extensions/utils.ts index dd050b76b6..59d155f3f4 100644 --- a/packages/config/src/utils/extensions/utils.ts +++ b/packages/config/src/utils/extensions/utils.ts @@ -64,11 +64,8 @@ type AutoInstallableExtensionMeta = { * * @returns Array of extensions with their associated packages */ -export async function fetchAutoInstallableExtensionsMeta( - extensionApiBaseUrl?: string, -): Promise { - const baseUrl = extensionApiBaseUrl ?? process.env.EXTENSION_API_BASE_URL ?? EXTENSION_API_BASE_URL - const url = new URL(`/meta/auto-installable`, baseUrl) +export async function fetchAutoInstallableExtensionsMeta(): Promise { + const url = new URL(`/meta/auto-installable`, process.env.EXTENSION_API_BASE_URL ?? EXTENSION_API_BASE_URL) const response = await fetch(url.toString()) if (!response.ok) { throw new Error(`Failed to fetch extensions meta`) diff --git a/packages/config/tests/extensions/tests.js b/packages/config/tests/extensions/tests.js index 2b28351629..abdf019d3d 100644 --- a/packages/config/tests/extensions/tests.js +++ b/packages/config/tests/extensions/tests.js @@ -23,6 +23,16 @@ const mockFetch = async (url, options) => { } } + // If it's a request to the extension API for auto-installable extensions + if (urlString.includes('api.netlifysdk.com/meta/auto-installable')) { + return { + ok: true, + status: 200, + json: async () => AUTO_INSTALLABLE_EXTENSIONS_RESPONSE.response, + text: async () => JSON.stringify(AUTO_INSTALLABLE_EXTENSIONS_RESPONSE.response), + } + } + // For all other requests, use the original fetch return originalFetch(url, options) } @@ -73,12 +83,7 @@ test('Auto-install extensions: feature flag disabled returns integrations unchan auto_install_required_extensions: false, }, }) - .runConfigServer([ - SITE_INFO_DATA, - TEAM_INSTALLATIONS_META_RESPONSE, - FETCH_INTEGRATIONS_EMPTY_RESPONSE, - AUTO_INSTALLABLE_EXTENSIONS_RESPONSE, - ]) + .runConfigServer([SITE_INFO_DATA, TEAM_INSTALLATIONS_META_RESPONSE, FETCH_INTEGRATIONS_EMPTY_RESPONSE]) const config = JSON.parse(output) @@ -99,12 +104,7 @@ test('Auto-install extensions: gracefully handles missing package.json', async ( auto_install_required_extensions: true, }, }) - .runConfigServer([ - SITE_INFO_DATA, - TEAM_INSTALLATIONS_META_RESPONSE, - FETCH_INTEGRATIONS_EMPTY_RESPONSE, - AUTO_INSTALLABLE_EXTENSIONS_RESPONSE, - ]) + .runConfigServer([SITE_INFO_DATA, TEAM_INSTALLATIONS_META_RESPONSE, FETCH_INTEGRATIONS_EMPTY_RESPONSE]) const config = JSON.parse(output) @@ -116,7 +116,7 @@ test('Auto-install extensions: gracefully handles missing package.json', async ( test('Auto-install extensions: correctly reads package.json from buildDir', async (t) => { // This test verifies that the function correctly reads package.json from buildDir - const { output, requests } = await new Fixture('./fixtures/with_neon_package') + const { output } = await new Fixture('./fixtures/with_neon_package') .withFlags({ siteId: 'test', accountId: 'account1', @@ -126,12 +126,7 @@ test('Auto-install extensions: correctly reads package.json from buildDir', asyn auto_install_required_extensions: true, }, }) - .runConfigServer([ - SITE_INFO_DATA, - TEAM_INSTALLATIONS_META_RESPONSE, - FETCH_INTEGRATIONS_EMPTY_RESPONSE, - AUTO_INSTALLABLE_EXTENSIONS_RESPONSE, - ]) + .runConfigServer([SITE_INFO_DATA, TEAM_INSTALLATIONS_META_RESPONSE, FETCH_INTEGRATIONS_EMPTY_RESPONSE]) const config = JSON.parse(output) @@ -140,9 +135,8 @@ test('Auto-install extensions: correctly reads package.json from buildDir', asyn t.assert(config.buildDir) t.true(config.buildDir.includes('with_neon_package')) - // Should have made a request to fetch auto-installable extensions - const autoInstallRequest = requests.find((request) => request.url.includes('/meta/auto-installable')) - t.assert(autoInstallRequest, 'Should have fetched auto-installable extensions') + // Auto-installable extensions API call is mocked by global fetch mock + // (not visible in requests array since it's intercepted before reaching test server) // Should have attempted to install the extension (mocked) t.assert(installationRequests.length > 0, 'Should have attempted to install extension') @@ -160,7 +154,7 @@ test('Auto-install extensions: correctly reads package.json from buildDir', asyn test('Auto-install extensions: does not install when required packages are missing', async (t) => { // This test uses a fixture that has dependencies but not the extension packages - const { output, requests } = await new Fixture('./fixtures/without_packages') + const { output } = await new Fixture('./fixtures/without_packages') .withFlags({ siteId: 'test', accountId: 'account1', @@ -170,12 +164,7 @@ test('Auto-install extensions: does not install when required packages are missi auto_install_required_extensions: true, }, }) - .runConfigServer([ - SITE_INFO_DATA, - TEAM_INSTALLATIONS_META_RESPONSE, - FETCH_INTEGRATIONS_EMPTY_RESPONSE, - AUTO_INSTALLABLE_EXTENSIONS_RESPONSE, - ]) + .runConfigServer([SITE_INFO_DATA, TEAM_INSTALLATIONS_META_RESPONSE, FETCH_INTEGRATIONS_EMPTY_RESPONSE]) const config = JSON.parse(output) @@ -184,15 +173,14 @@ test('Auto-install extensions: does not install when required packages are missi t.assert(config.integrations) t.is(config.integrations.length, 0) - // Should have made a request to fetch auto-installable extensions - const autoInstallRequest = requests.find((request) => request.url.includes('/meta/auto-installable')) - t.assert(autoInstallRequest, 'Should have fetched auto-installable extensions') + // Auto-installable extensions API call is mocked by global fetch mock + // (not visible in requests array since it's intercepted before reaching test server) }) test('Auto-install extensions: correctly reads package.json when no netlify.toml exists', async (t) => { // This test verifies buildDir resolution works correctly when there's no netlify.toml // but package.json exists with extension packages - const { output, requests } = await new Fixture('./fixtures/no_netlify_toml_with_neon') + const { output } = await new Fixture('./fixtures/no_netlify_toml_with_neon') .withFlags({ siteId: 'test', accountId: 'account1', @@ -202,12 +190,7 @@ test('Auto-install extensions: correctly reads package.json when no netlify.toml auto_install_required_extensions: true, }, }) - .runConfigServer([ - SITE_INFO_DATA, - TEAM_INSTALLATIONS_META_RESPONSE, - FETCH_INTEGRATIONS_EMPTY_RESPONSE, - AUTO_INSTALLABLE_EXTENSIONS_RESPONSE, - ]) + .runConfigServer([SITE_INFO_DATA, TEAM_INSTALLATIONS_META_RESPONSE, FETCH_INTEGRATIONS_EMPTY_RESPONSE]) const config = JSON.parse(output) @@ -219,9 +202,8 @@ test('Auto-install extensions: correctly reads package.json when no netlify.toml // buildDir should be the repository root since there's no build.base config t.true(config.buildDir.endsWith('no_netlify_toml_with_neon')) - // Should have made a request to fetch auto-installable extensions - const autoInstallRequest = requests.find((request) => request.url.includes('/meta/auto-installable')) - t.assert(autoInstallRequest, 'Should have fetched auto-installable extensions') + // Auto-installable extensions API call is mocked by global fetch mock + // (not visible in requests array since it's intercepted before reaching test server) // Should have attempted to install the extension t.assert(installationRequests.length > 0, 'Should have attempted to install extension')