Skip to content
This repository was archived by the owner on Jun 9, 2025. It is now read-only.

Commit 071872f

Browse files
committed
Merge branch 'main' into feat/purge-cache-user-agent
2 parents b0152f1 + 95bf657 commit 071872f

File tree

10 files changed

+7166
-12695
lines changed

10 files changed

+7166
-12695
lines changed

.github/workflows/workflow.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ jobs:
1313
strategy:
1414
matrix:
1515
os: [ubuntu-latest, macOS-latest, windows-latest]
16-
node-version: [14.0.0, '*']
16+
node-version: [18.0.0, '*']
1717
exclude:
1818
- os: macOS-latest
19-
node-version: 14.0.0
19+
node-version: 18.0.0
2020
- os: windows-latest
21-
node-version: 14.0.0
21+
node-version: 18.0.0
2222
fail-fast: false
2323
steps:
2424
- name: Git checkout

package-lock.json

Lines changed: 6967 additions & 12472 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,21 +50,15 @@
5050
"format:fix:prettier": "cross-env-shell prettier --write $npm_package_config_prettier",
5151
"test:dev": "run-s build test:dev:*",
5252
"test:ci": "run-s test:ci:*",
53-
"test:dev:ava": "ava",
53+
"test:dev:vitest": "vitest",
5454
"test:dev:tsd": "tsd",
5555
"test:publish": "publint && attw --pack",
56-
"test:ci:ava": "nyc -r lcovonly -r text -r json ava"
56+
"test:ci:vitest": "vitest run --coverage"
5757
},
5858
"config": {
5959
"eslint": "--ignore-pattern README.md --ignore-path .gitignore --cache --format=codeframe --max-warnings=0 \"{src,scripts,.github,test}/**/*.{ts,js,md,html}\" \"*.{ts,js,md,html}\" \".*.{ts,js,md,html}\"",
6060
"prettier": "--ignore-path .gitignore --loglevel=warn \"{src,scripts,.github}/**/*.{ts,js,md,yml,json,html}\" \"*.{ts,js,yml,json,html}\" \".*.{ts,js,yml,json,html}\" \"!**/package-lock.json\" \"!package-lock.json\""
6161
},
62-
"ava": {
63-
"files": [
64-
"test/unit/*.js"
65-
],
66-
"verbose": true
67-
},
6862
"tsd": {
6963
"directory": "test/types/"
7064
},
@@ -86,17 +80,18 @@
8680
"@commitlint/cli": "^17.0.0",
8781
"@commitlint/config-conventional": "^17.0.0",
8882
"@netlify/eslint-config-node": "^7.0.1",
89-
"ava": "^2.4.0",
83+
"@types/semver": "^7.5.8",
84+
"@vitest/coverage-v8": "^2.1.8",
9085
"husky": "^7.0.4",
9186
"npm-run-all2": "^5.0.0",
92-
"nyc": "^15.0.0",
9387
"publint": "^0.2.7",
9488
"semver": "^7.5.4",
9589
"tsd": "^0.31.0",
9690
"tsup": "^8.0.2",
97-
"typescript": "^4.4.4"
91+
"typescript": "^4.4.4",
92+
"vitest": "^2.1.8"
9893
},
9994
"engines": {
100-
"node": ">=14.0.0"
95+
"node": ">=18.0.0"
10196
}
10297
}

test/unit/builder.js renamed to src/lib/builder.test.ts

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
1-
const test = require('ava')
1+
import { expect, test } from 'vitest'
22

3-
const { builder } = require('../../dist/lib/builder')
4-
const { invokeLambda } = require('../helpers/main')
3+
import { invokeLambda } from '../../test/helpers/main.mjs'
4+
import { BaseHandler } from '../function/handler.js'
5+
import { HandlerEvent } from '../main.js'
6+
7+
import { builder } from './builder.js'
58

69
const METADATA_OBJECT = { metadata: { version: 1, builder_function: true, ttl: 0 } }
710

8-
test('Injects the metadata object into an asynchronous handler', async (t) => {
11+
test('Injects the metadata object into an asynchronous handler', async () => {
12+
const ttl = 3600
913
const originalResponse = {
1014
body: ':thumbsup:',
1115
statusCode: 200,
12-
ttl: 3600,
16+
ttl,
1317
}
1418
const myHandler = async () => {
1519
const asyncTask = new Promise((resolve) => {
@@ -22,23 +26,25 @@ test('Injects the metadata object into an asynchronous handler', async (t) => {
2226
}
2327
const response = await invokeLambda(builder(myHandler))
2428

25-
t.deepEqual(response, { ...originalResponse, metadata: { version: 1, builder_function: true, ttl: 3600 } })
29+
expect(response).toStrictEqual({ ...originalResponse, metadata: { version: 1, builder_function: true, ttl } })
2630
})
2731

28-
test('Injects the metadata object into a synchronous handler', async (t) => {
32+
test('Injects the metadata object into a synchronous handler', async () => {
2933
const originalResponse = {
3034
body: ':thumbsup:',
3135
statusCode: 200,
3236
}
33-
const myHandler = (event, context, callback) => {
34-
callback(null, originalResponse)
37+
// eslint-disable-next-line promise/prefer-await-to-callbacks
38+
const myHandler: BaseHandler = (event, context, callback) => {
39+
// eslint-disable-next-line n/callback-return, promise/prefer-await-to-callbacks
40+
callback?.(null, originalResponse)
3541
}
3642
const response = await invokeLambda(builder(myHandler))
3743

38-
t.deepEqual(response, { ...originalResponse, ...METADATA_OBJECT })
44+
expect(response).toStrictEqual({ ...originalResponse, ...METADATA_OBJECT })
3945
})
4046

41-
test('Injects the metadata object for non-200 responses', async (t) => {
47+
test('Injects the metadata object for non-200 responses', async () => {
4248
const originalResponse = {
4349
body: ':thumbsdown:',
4450
statusCode: 404,
@@ -54,10 +60,10 @@ test('Injects the metadata object for non-200 responses', async (t) => {
5460
}
5561
const response = await invokeLambda(builder(myHandler))
5662

57-
t.deepEqual(response, { ...originalResponse, ...METADATA_OBJECT })
63+
expect(response).toStrictEqual({ ...originalResponse, ...METADATA_OBJECT })
5864
})
5965

60-
test('Returns a 405 error for requests using the POST method', async (t) => {
66+
test('Returns a 405 error for requests using the POST method', async () => {
6167
const originalResponse = {
6268
body: ':thumbsup:',
6369
statusCode: 200,
@@ -73,10 +79,10 @@ test('Returns a 405 error for requests using the POST method', async (t) => {
7379
}
7480
const response = await invokeLambda(builder(myHandler), { method: 'POST' })
7581

76-
t.deepEqual(response, { body: 'Method Not Allowed', statusCode: 405 })
82+
expect(response).toStrictEqual({ body: 'Method Not Allowed', statusCode: 405 })
7783
})
7884

79-
test('Returns a 405 error for requests using the PUT method', async (t) => {
85+
test('Returns a 405 error for requests using the PUT method', async () => {
8086
const originalResponse = {
8187
body: ':thumbsup:',
8288
statusCode: 200,
@@ -92,10 +98,10 @@ test('Returns a 405 error for requests using the PUT method', async (t) => {
9298
}
9399
const response = await invokeLambda(builder(myHandler), { method: 'PUT' })
94100

95-
t.deepEqual(response, { body: 'Method Not Allowed', statusCode: 405 })
101+
expect(response).toStrictEqual({ body: 'Method Not Allowed', statusCode: 405 })
96102
})
97103

98-
test('Returns a 405 error for requests using the DELETE method', async (t) => {
104+
test('Returns a 405 error for requests using the DELETE method', async () => {
99105
const originalResponse = {
100106
body: ':thumbsup:',
101107
statusCode: 200,
@@ -111,10 +117,10 @@ test('Returns a 405 error for requests using the DELETE method', async (t) => {
111117
}
112118
const response = await invokeLambda(builder(myHandler), { method: 'DELETE' })
113119

114-
t.deepEqual(response, { body: 'Method Not Allowed', statusCode: 405 })
120+
expect(response).toStrictEqual({ body: 'Method Not Allowed', statusCode: 405 })
115121
})
116122

117-
test('Returns a 405 error for requests using the PATCH method', async (t) => {
123+
test('Returns a 405 error for requests using the PATCH method', async () => {
118124
const originalResponse = {
119125
body: ':thumbsup:',
120126
statusCode: 200,
@@ -130,12 +136,13 @@ test('Returns a 405 error for requests using the PATCH method', async (t) => {
130136
}
131137
const response = await invokeLambda(builder(myHandler), { method: 'PATCH' })
132138

133-
t.deepEqual(response, { body: 'Method Not Allowed', statusCode: 405 })
139+
expect(response).toStrictEqual({ body: 'Method Not Allowed', statusCode: 405 })
134140
})
135141

136-
test('Preserves errors thrown inside the wrapped handler', async (t) => {
142+
test('Preserves errors thrown inside the wrapped handler', async () => {
137143
const error = new Error('Uh-oh!')
138144

145+
// @ts-expect-error There's no type for this custom property.
139146
error.someProperty = ':thumbsdown:'
140147

141148
const myHandler = async () => {
@@ -148,27 +155,32 @@ test('Preserves errors thrown inside the wrapped handler', async (t) => {
148155
throw error
149156
}
150157

151-
await t.throwsAsync(invokeLambda(builder(myHandler)), { is: error })
158+
try {
159+
await invokeLambda(builder(myHandler))
160+
161+
throw new Error('Invocation should have failed')
162+
} catch {}
152163
})
153164

154-
test('Does not pass query parameters to the wrapped handler', async (t) => {
165+
test('Does not pass query parameters to the wrapped handler', async () => {
155166
const originalResponse = {
156167
body: ':thumbsup:',
157168
statusCode: 200,
158169
}
159170
// eslint-disable-next-line require-await
160-
const myHandler = async (event) => {
161-
t.deepEqual(event.multiValueQueryStringParameters, {})
162-
t.deepEqual(event.queryStringParameters, {})
171+
const myHandler = async (event: HandlerEvent) => {
172+
expect(event.multiValueQueryStringParameters).toStrictEqual({})
173+
expect(event.queryStringParameters).toStrictEqual({})
163174

164175
return originalResponse
165176
}
166177
const multiValueQueryStringParameters = { foo: ['bar'], bar: ['baz'] }
167178
const queryStringParameters = { foo: 'bar', bar: 'baz' }
168179
const response = await invokeLambda(builder(myHandler), {
180+
// @ts-expect-error TODO: Fic types.
169181
multiValueQueryStringParameters,
170182
queryStringParameters,
171183
})
172184

173-
t.deepEqual(response, { ...originalResponse, ...METADATA_OBJECT })
185+
expect(response).toStrictEqual({ ...originalResponse, ...METADATA_OBJECT })
174186
})

src/lib/purge_cache.test.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import process from 'node:process'
2+
3+
import semver from 'semver'
4+
import { beforeEach, afterEach, expect, test } from 'vitest'
5+
6+
import { invokeLambda } from '../../test/helpers/main.mjs'
7+
import { MockFetch } from '../../test/helpers/mock_fetch.mjs'
8+
9+
import { purgeCache } from './purge_cache.js'
10+
11+
const globalFetch = globalThis.fetch
12+
const hasFetchAPI = semver.gte(process.version, '18.0.0')
13+
14+
beforeEach(() => {
15+
delete process.env.NETLIFY_PURGE_API_TOKEN
16+
delete process.env.SITE_ID
17+
delete process.env.NETLIFY_LOCAL
18+
})
19+
20+
afterEach(() => {
21+
globalThis.fetch = globalFetch
22+
})
23+
24+
test('Calls the purge API endpoint and returns `undefined` if the operation was successful', async () => {
25+
if (!hasFetchAPI) {
26+
console.warn('Skipping test requires the fetch API')
27+
28+
return
29+
}
30+
31+
const mockSiteID = '123456789'
32+
const mockToken = '1q2w3e4r5t6y7u8i9o0p'
33+
34+
process.env.NETLIFY_PURGE_API_TOKEN = mockToken
35+
process.env.SITE_ID = mockSiteID
36+
37+
const mockAPI = new MockFetch().post({
38+
body: (payload: string) => {
39+
const data = JSON.parse(payload)
40+
41+
expect(data.site_id).toBe(mockSiteID)
42+
},
43+
headers: { Authorization: `Bearer ${mockToken}` },
44+
method: 'post',
45+
response: new Response(null, { status: 202 }),
46+
url: `https://api.netlify.com/api/v1/purge`,
47+
})
48+
// eslint-disable-next-line unicorn/consistent-function-scoping
49+
const myFunction = async () => {
50+
await purgeCache()
51+
}
52+
53+
globalThis.fetch = mockAPI.fetcher
54+
55+
const response = await invokeLambda(myFunction)
56+
57+
expect(response).toBeUndefined()
58+
expect(mockAPI.fulfilled).toBeTruthy()
59+
})
60+
61+
test('Throws if the API response does not have a successful status code', async () => {
62+
if (!hasFetchAPI) {
63+
console.warn('Skipping test requires the fetch API')
64+
}
65+
66+
const mockSiteID = '123456789'
67+
const mockToken = '1q2w3e4r5t6y7u8i9o0p'
68+
69+
process.env.NETLIFY_PURGE_API_TOKEN = mockToken
70+
process.env.SITE_ID = mockSiteID
71+
72+
const mockAPI = new MockFetch().post({
73+
body: (payload: string) => {
74+
const data = JSON.parse(payload)
75+
76+
expect(data.site_id).toBe(mockSiteID)
77+
},
78+
headers: { Authorization: `Bearer ${mockToken}` },
79+
method: 'post',
80+
response: new Response(null, { status: 500 }),
81+
url: `https://api.netlify.com/api/v1/purge`,
82+
})
83+
// eslint-disable-next-line unicorn/consistent-function-scoping
84+
const myFunction = async () => {
85+
await purgeCache()
86+
}
87+
88+
globalThis.fetch = mockAPI.fetcher
89+
90+
try {
91+
await invokeLambda(myFunction)
92+
93+
throw new Error('Invocation should have failed')
94+
} catch (error) {
95+
expect((error as NodeJS.ErrnoException).message).toBe(
96+
'Cache purge API call returned an unexpected status code: 500',
97+
)
98+
}
99+
})
100+
101+
test('Ignores purgeCache if in local dev with no token or site', async () => {
102+
if (!hasFetchAPI) {
103+
console.warn('Skipping test requires the fetch API')
104+
105+
return
106+
}
107+
108+
process.env.NETLIFY_LOCAL = '1'
109+
110+
const mockAPI = new MockFetch().post({
111+
body: () => {
112+
throw new Error('Unexpected request')
113+
},
114+
})
115+
// eslint-disable-next-line unicorn/consistent-function-scoping
116+
const myFunction = async () => {
117+
await purgeCache()
118+
}
119+
120+
globalThis.fetch = mockAPI.fetcher
121+
122+
const response = await invokeLambda(myFunction)
123+
124+
expect(response).toBeUndefined()
125+
})

0 commit comments

Comments
 (0)