diff --git a/.eslintrc.js b/.eslintrc.js index a7cad672..74ce087b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,6 +6,7 @@ module.exports = { extends: '@netlify/eslint-config-node', rules: { 'max-statements': 'off', + 'max-lines': 'off', }, overrides: [ ...overrides, diff --git a/src/lib/purge_cache.test.ts b/src/lib/purge_cache.test.ts index 04871c56..4ace1b01 100644 --- a/src/lib/purge_cache.test.ts +++ b/src/lib/purge_cache.test.ts @@ -58,9 +58,11 @@ test('Calls the purge API endpoint and returns `undefined` if the operation was expect(mockAPI.fulfilled).toBeTruthy() }) -test('Throws if the API response does not have a successful status code', async () => { +test('Throws an error if the API response does not have a successful status code, using the response body as part of the error message', async () => { if (!hasFetchAPI) { console.warn('Skipping test requires the fetch API') + + return } const mockSiteID = '123456789' @@ -77,7 +79,7 @@ test('Throws if the API response does not have a successful status code', async }, headers: { Authorization: `Bearer ${mockToken}` }, method: 'post', - response: new Response(null, { status: 500 }), + response: new Response('site not found', { status: 404 }), url: `https://api.netlify.com/api/v1/purge`, }) // eslint-disable-next-line unicorn/consistent-function-scoping @@ -90,14 +92,54 @@ test('Throws if the API response does not have a successful status code', async try { await invokeLambda(myFunction) - throw new Error('Invocation should have failed') + expect.fail('Invocation should have failed') } catch (error) { expect((error as NodeJS.ErrnoException).message).toBe( - 'Cache purge API call returned an unexpected status code: 500', + 'Cache purge API call was unsuccessful.\nStatus: 404\nBody: site not found', ) } }) +test('Throws if the API response does not have a successful status code, does not include the response body if it is not text', async () => { + if (!hasFetchAPI) { + console.warn('Skipping test requires the fetch API') + + return + } + + const mockSiteID = '123456789' + const mockToken = '1q2w3e4r5t6y7u8i9o0p' + + process.env.NETLIFY_PURGE_API_TOKEN = mockToken + process.env.SITE_ID = mockSiteID + + const mockAPI = new MockFetch().post({ + body: (payload: string) => { + const data = JSON.parse(payload) + + expect(data.site_id).toBe(mockSiteID) + }, + headers: { Authorization: `Bearer ${mockToken}` }, + method: 'post', + response: new Response(null, { status: 500 }), + url: `https://api.netlify.com/api/v1/purge`, + }) + // eslint-disable-next-line unicorn/consistent-function-scoping + const myFunction = async () => { + await purgeCache() + } + + globalThis.fetch = mockAPI.fetcher + + try { + await invokeLambda(myFunction) + + throw new Error('Invocation should have failed') + } catch (error) { + expect((error as NodeJS.ErrnoException).message).toBe('Cache purge API call was unsuccessful.\nStatus: 500') + } +}) + test('Ignores purgeCache if in local dev with no token or site', async () => { if (!hasFetchAPI) { console.warn('Skipping test requires the fetch API') diff --git a/src/lib/purge_cache.ts b/src/lib/purge_cache.ts index f88c905a..e781eb06 100644 --- a/src/lib/purge_cache.ts +++ b/src/lib/purge_cache.ts @@ -38,6 +38,14 @@ export const purgeCache = async (options: PurgeCacheOptions = {}) => { ) } + const { siteID } = options as PurgeCacheOptionsWithSiteID + const { siteSlug } = options as PurgeCacheOptionsWithSiteSlug + const { domain } = options as PurgeCacheOptionsWithDomain + + if ((siteID && siteSlug) || (siteID && domain) || (siteSlug && domain)) { + throw new Error('Can only pass one of either "siteID", "siteSlug", or "domain"') + } + const payload: PurgeAPIPayload = { cache_tags: options.tags, deploy_alias: options.deployAlias, @@ -50,22 +58,20 @@ export const purgeCache = async (options: PurgeCacheOptions = {}) => { return } - if ('siteSlug' in options) { - payload.site_slug = options.siteSlug - } else if ('domain' in options) { - payload.domain = options.domain + if (siteSlug) { + payload.site_slug = siteSlug + } else if (domain) { + payload.domain = domain } else { // The `siteID` from `options` takes precedence over the one from the // environment. - const siteID = options.siteID || env.SITE_ID + payload.site_id = siteID || env.SITE_ID - if (!siteID) { + if (!payload.site_id) { throw new Error( 'The Netlify site ID was not found in the execution environment. Please supply it manually using the `siteID` property.', ) } - - payload.site_id = siteID } if (!token) { @@ -91,6 +97,13 @@ export const purgeCache = async (options: PurgeCacheOptions = {}) => { }) if (!response.ok) { - throw new Error(`Cache purge API call returned an unexpected status code: ${response.status}`) + let text + try { + text = await response.text() + } catch {} + if (text) { + throw new Error(`Cache purge API call was unsuccessful.\nStatus: ${response.status}\nBody: ${text}`) + } + throw new Error(`Cache purge API call was unsuccessful.\nStatus: ${response.status}`) } }