Skip to content

Commit

Permalink
feat!: replace ava with vitest (#554)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Requires Node.js >=18.0.0
  • Loading branch information
eduardoboucas authored Dec 18, 2024
1 parent 53b7bd0 commit 95bf657
Show file tree
Hide file tree
Showing 9 changed files with 7,076 additions and 12,568 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
node-version: [14.0.0, '*']
node-version: [18.0.0, '*']
exclude:
- os: macOS-latest
node-version: 14.0.0
node-version: 18.0.0
- os: windows-latest
node-version: 14.0.0
node-version: 18.0.0
fail-fast: false
steps:
- name: Git checkout
Expand Down
19,439 changes: 6,967 additions & 12,472 deletions package-lock.json

Large diffs are not rendered by default.

19 changes: 7 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,21 +50,15 @@
"format:fix:prettier": "cross-env-shell prettier --write $npm_package_config_prettier",
"test:dev": "run-s build test:dev:*",
"test:ci": "run-s test:ci:*",
"test:dev:ava": "ava",
"test:dev:vitest": "vitest",
"test:dev:tsd": "tsd",
"test:publish": "publint && attw --pack",
"test:ci:ava": "nyc -r lcovonly -r text -r json ava"
"test:ci:vitest": "vitest run --coverage"
},
"config": {
"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}\"",
"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\""
},
"ava": {
"files": [
"test/unit/*.js"
],
"verbose": true
},
"tsd": {
"directory": "test/types/"
},
Expand All @@ -86,17 +80,18 @@
"@commitlint/cli": "^17.0.0",
"@commitlint/config-conventional": "^17.0.0",
"@netlify/eslint-config-node": "^7.0.1",
"ava": "^2.4.0",
"@types/semver": "^7.5.8",
"@vitest/coverage-v8": "^2.1.8",
"husky": "^7.0.4",
"npm-run-all2": "^5.0.0",
"nyc": "^15.0.0",
"publint": "^0.2.7",
"semver": "^7.5.4",
"tsd": "^0.31.0",
"tsup": "^8.0.2",
"typescript": "^4.4.4"
"typescript": "^4.4.4",
"vitest": "^2.1.8"
},
"engines": {
"node": ">=14.0.0"
"node": ">=18.0.0"
}
}
66 changes: 39 additions & 27 deletions test/unit/builder.js → src/lib/builder.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
const test = require('ava')
import { expect, test } from 'vitest'

const { builder } = require('../../dist/lib/builder')
const { invokeLambda } = require('../helpers/main')
import { invokeLambda } from '../../test/helpers/main.mjs'
import { BaseHandler } from '../function/handler.js'
import { HandlerEvent } from '../main.js'

import { builder } from './builder.js'

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

test('Injects the metadata object into an asynchronous handler', async (t) => {
test('Injects the metadata object into an asynchronous handler', async () => {
const ttl = 3600
const originalResponse = {
body: ':thumbsup:',
statusCode: 200,
ttl: 3600,
ttl,
}
const myHandler = async () => {
const asyncTask = new Promise((resolve) => {
Expand All @@ -22,23 +26,25 @@ test('Injects the metadata object into an asynchronous handler', async (t) => {
}
const response = await invokeLambda(builder(myHandler))

t.deepEqual(response, { ...originalResponse, metadata: { version: 1, builder_function: true, ttl: 3600 } })
expect(response).toStrictEqual({ ...originalResponse, metadata: { version: 1, builder_function: true, ttl } })
})

test('Injects the metadata object into a synchronous handler', async (t) => {
test('Injects the metadata object into a synchronous handler', async () => {
const originalResponse = {
body: ':thumbsup:',
statusCode: 200,
}
const myHandler = (event, context, callback) => {
callback(null, originalResponse)
// eslint-disable-next-line promise/prefer-await-to-callbacks
const myHandler: BaseHandler = (event, context, callback) => {
// eslint-disable-next-line n/callback-return, promise/prefer-await-to-callbacks
callback?.(null, originalResponse)
}
const response = await invokeLambda(builder(myHandler))

t.deepEqual(response, { ...originalResponse, ...METADATA_OBJECT })
expect(response).toStrictEqual({ ...originalResponse, ...METADATA_OBJECT })
})

test('Injects the metadata object for non-200 responses', async (t) => {
test('Injects the metadata object for non-200 responses', async () => {
const originalResponse = {
body: ':thumbsdown:',
statusCode: 404,
Expand All @@ -54,10 +60,10 @@ test('Injects the metadata object for non-200 responses', async (t) => {
}
const response = await invokeLambda(builder(myHandler))

t.deepEqual(response, { ...originalResponse, ...METADATA_OBJECT })
expect(response).toStrictEqual({ ...originalResponse, ...METADATA_OBJECT })
})

test('Returns a 405 error for requests using the POST method', async (t) => {
test('Returns a 405 error for requests using the POST method', async () => {
const originalResponse = {
body: ':thumbsup:',
statusCode: 200,
Expand All @@ -73,10 +79,10 @@ test('Returns a 405 error for requests using the POST method', async (t) => {
}
const response = await invokeLambda(builder(myHandler), { method: 'POST' })

t.deepEqual(response, { body: 'Method Not Allowed', statusCode: 405 })
expect(response).toStrictEqual({ body: 'Method Not Allowed', statusCode: 405 })
})

test('Returns a 405 error for requests using the PUT method', async (t) => {
test('Returns a 405 error for requests using the PUT method', async () => {
const originalResponse = {
body: ':thumbsup:',
statusCode: 200,
Expand All @@ -92,10 +98,10 @@ test('Returns a 405 error for requests using the PUT method', async (t) => {
}
const response = await invokeLambda(builder(myHandler), { method: 'PUT' })

t.deepEqual(response, { body: 'Method Not Allowed', statusCode: 405 })
expect(response).toStrictEqual({ body: 'Method Not Allowed', statusCode: 405 })
})

test('Returns a 405 error for requests using the DELETE method', async (t) => {
test('Returns a 405 error for requests using the DELETE method', async () => {
const originalResponse = {
body: ':thumbsup:',
statusCode: 200,
Expand All @@ -111,10 +117,10 @@ test('Returns a 405 error for requests using the DELETE method', async (t) => {
}
const response = await invokeLambda(builder(myHandler), { method: 'DELETE' })

t.deepEqual(response, { body: 'Method Not Allowed', statusCode: 405 })
expect(response).toStrictEqual({ body: 'Method Not Allowed', statusCode: 405 })
})

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

t.deepEqual(response, { body: 'Method Not Allowed', statusCode: 405 })
expect(response).toStrictEqual({ body: 'Method Not Allowed', statusCode: 405 })
})

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

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

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

await t.throwsAsync(invokeLambda(builder(myHandler)), { is: error })
try {
await invokeLambda(builder(myHandler))

throw new Error('Invocation should have failed')
} catch {}
})

test('Does not pass query parameters to the wrapped handler', async (t) => {
test('Does not pass query parameters to the wrapped handler', async () => {
const originalResponse = {
body: ':thumbsup:',
statusCode: 200,
}
// eslint-disable-next-line require-await
const myHandler = async (event) => {
t.deepEqual(event.multiValueQueryStringParameters, {})
t.deepEqual(event.queryStringParameters, {})
const myHandler = async (event: HandlerEvent) => {
expect(event.multiValueQueryStringParameters).toStrictEqual({})
expect(event.queryStringParameters).toStrictEqual({})

return originalResponse
}
const multiValueQueryStringParameters = { foo: ['bar'], bar: ['baz'] }
const queryStringParameters = { foo: 'bar', bar: 'baz' }
const response = await invokeLambda(builder(myHandler), {
// @ts-expect-error TODO: Fic types.
multiValueQueryStringParameters,
queryStringParameters,
})

t.deepEqual(response, { ...originalResponse, ...METADATA_OBJECT })
expect(response).toStrictEqual({ ...originalResponse, ...METADATA_OBJECT })
})
63 changes: 35 additions & 28 deletions test/unit/purge_cache.js → src/lib/purge_cache.test.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,31 @@
const process = require('process')
import process from 'node:process'

const test = require('ava')
const semver = require('semver')
import semver from 'semver'
import { beforeEach, afterEach, expect, test } from 'vitest'

const { purgeCache } = require('../../dist/lib/purge_cache')
const { invokeLambda } = require('../helpers/main')
const MockFetch = require('../helpers/mock_fetch')
import { invokeLambda } from '../../test/helpers/main.mjs'
import { MockFetch } from '../../test/helpers/mock_fetch.mjs'

import { purgeCache } from './purge_cache.js'

const globalFetch = globalThis.fetch
const hasFetchAPI = semver.gte(process.version, '18.0.0')

test.beforeEach(() => {
beforeEach(() => {
delete process.env.NETLIFY_PURGE_API_TOKEN
delete process.env.SITE_ID
delete process.env.NETLIFY_LOCAL
})

test.afterEach(() => {
afterEach(() => {
globalThis.fetch = globalFetch
})

test.serial('Calls the purge API endpoint and returns `undefined` if the operation was successful', async (t) => {
test('Calls the purge API endpoint and returns `undefined` if the operation was successful', async () => {
if (!hasFetchAPI) {
console.warn('Skipping test requires the fetch API')

return t.pass()
return
}

const mockSiteID = '123456789'
Expand All @@ -34,16 +35,17 @@ test.serial('Calls the purge API endpoint and returns `undefined` if the operati
process.env.SITE_ID = mockSiteID

const mockAPI = new MockFetch().post({
body: (payload) => {
body: (payload: string) => {
const data = JSON.parse(payload)

t.is(data.site_id, mockSiteID)
expect(data.site_id).toBe(mockSiteID)
},
headers: { Authorization: `Bearer ${mockToken}` },
method: 'post',
response: new Response(null, { status: 202 }),
url: `https://api.netlify.com/api/v1/purge`,
})
// eslint-disable-next-line unicorn/consistent-function-scoping
const myFunction = async () => {
await purgeCache()
}
Expand All @@ -52,15 +54,13 @@ test.serial('Calls the purge API endpoint and returns `undefined` if the operati

const response = await invokeLambda(myFunction)

t.is(response, undefined)
t.true(mockAPI.fulfilled)
expect(response).toBeUndefined()
expect(mockAPI.fulfilled).toBeTruthy()
})

test.serial('Throws if the API response does not have a successful status code', async (t) => {
test('Throws if the API response does not have a successful status code', async () => {
if (!hasFetchAPI) {
console.warn('Skipping test requires the fetch API')

return t.pass()
}

const mockSiteID = '123456789'
Expand All @@ -70,42 +70,49 @@ test.serial('Throws if the API response does not have a successful status code',
process.env.SITE_ID = mockSiteID

const mockAPI = new MockFetch().post({
body: (payload) => {
body: (payload: string) => {
const data = JSON.parse(payload)

t.is(data.site_id, mockSiteID)
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

await t.throwsAsync(
async () => await invokeLambda(myFunction),
'Cache purge API call returned an unexpected status code: 500',
)
try {
await invokeLambda(myFunction)

throw new Error('Invocation should have failed')
} catch (error) {
expect((error as NodeJS.ErrnoException).message).toBe(
'Cache purge API call returned an unexpected status code: 500',
)
}
})

test.serial('Ignores purgeCache if in local dev with no token or site', async (t) => {
test('Ignores purgeCache if in local dev with no token or site', async () => {
if (!hasFetchAPI) {
console.warn('Skipping test requires the fetch API')

return t.pass()
return
}

process.env.NETLIFY_LOCAL = '1'

const mockAPI = new MockFetch().post({
body: () => {
t.fail()
}
throw new Error('Unexpected request')
},
})
// eslint-disable-next-line unicorn/consistent-function-scoping
const myFunction = async () => {
await purgeCache()
}
Expand All @@ -114,5 +121,5 @@ test.serial('Ignores purgeCache if in local dev with no token or site', async (t

const response = await invokeLambda(myFunction)

t.is(response, undefined)
expect(response).toBeUndefined()
})
Loading

0 comments on commit 95bf657

Please sign in to comment.