Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: replace ava with vitest #554

Merged
merged 4 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👁️ the esm package is gone from the package-lock so I think we're good

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
Loading