diff --git a/tests/integration/200.command.dev.test.cjs b/tests/integration/commands/dev/dev.zisi.test.mjs similarity index 54% rename from tests/integration/200.command.dev.test.cjs rename to tests/integration/commands/dev/dev.zisi.test.mjs index c3e65e4d311..31a4b2cfa56 100644 --- a/tests/integration/200.command.dev.test.cjs +++ b/tests/integration/commands/dev/dev.zisi.test.mjs @@ -1,26 +1,24 @@ // Handlers are meant to be async outside tests -const { copyFile } = require('fs').promises -const os = require('os') -const path = require('path') +import { copyFile } from 'fs/promises' +import { Agent } from 'node:https' +import os from 'os' +import path from 'path' +import { fileURLToPath } from 'url' -// eslint-disable-next-line ava/use-test -const avaTest = require('ava') -const { isCI } = require('ci-info') +import nodeFetch from 'node-fetch' +import { describe, test } from 'vitest' -const { curl } = require('./utils/curl.cjs') -const { withDevServer } = require('./utils/dev-server.cjs') -const got = require('./utils/got.cjs') -const { withMockApi } = require('./utils/mock-api.cjs') -const { withSiteBuilder } = require('./utils/site-builder.cjs') +import { curl } from '../../utils/curl.cjs' +import { withDevServer } from '../../utils/dev-server.cjs' +import { withMockApi } from '../../utils/mock-api.cjs' +import { withSiteBuilder } from '../../utils/site-builder.cjs' -const test = isCI ? avaTest.serial.bind(avaTest) : avaTest +const __dirname = path.dirname(fileURLToPath(import.meta.url)) const testMatrix = [{ args: [] }] -const testName = (title, args) => (args.length <= 0 ? title : `${title} - ${args.join(' ')}`) - -testMatrix.forEach(({ args }) => { - test(testName('should handle query params in redirects', args), async (t) => { +describe.concurrent.each(testMatrix)('withSiteBuilder with args: $args', ({ args }) => { + test('should handle query params in redirects', async (t) => { await withSiteBuilder('site-with-query-redirects', async (builder) => { await builder .withContentFile({ @@ -54,32 +52,32 @@ testMatrix.forEach(({ args }) => { await withDevServer({ cwd: builder.directory, args }, async (server) => { const [fromFunction, queryPassthrough, queryInRedirect, withParamMatching, functionWithSplat] = await Promise.all([ - got(`${server.url}/api/test?foo=1&foo=2&bar=1&bar=2`).json(), - got(`${server.url}/foo?foo=1&foo=2&bar=1&bar=2`, { followRedirect: false }), - got(`${server.url}/bar?foo=1&foo=2&bar=1&bar=2`, { followRedirect: false }), - got(`${server.url}/test?id=1`, { followRedirect: false }), - got(`${server.url}/baz/abc`).json(), + nodeFetch(`${server.url}/api/test?foo=1&foo=2&bar=1&bar=2`).then((res) => res.json()), + nodeFetch(`${server.url}/foo?foo=1&foo=2&bar=1&bar=2`, { redirect: 'manual' }), + nodeFetch(`${server.url}/bar?foo=1&foo=2&bar=1&bar=2`, { redirect: 'manual' }), + nodeFetch(`${server.url}/test?id=1`, { redirect: 'manual' }), + nodeFetch(`${server.url}/baz/abc`).then((res) => res.json()), ]) // query params should be taken from redirect rule for functions - t.deepEqual(fromFunction.multiValueQueryStringParameters, { bar: ['1', '2'], foo: ['1', '2'] }) + t.expect(fromFunction.multiValueQueryStringParameters).toStrictEqual({ bar: ['1', '2'], foo: ['1', '2'] }) // query params should be passed through from the request - t.is(queryPassthrough.headers.location, '/?foo=1&foo=2&bar=1&bar=2') + t.expect(queryPassthrough.headers.get('location')).toEqual(`${server.url}/?foo=1&foo=2&bar=1&bar=2`) // query params should be taken from the redirect rule - t.is(queryInRedirect.headers.location, '/?a=1&a=2') + t.expect(queryInRedirect.headers.get('location')).toEqual(`${server.url}/?a=1&a=2`) // query params should be taken from the redirect rule - t.is(withParamMatching.headers.location, '/?param=1') + t.expect(withParamMatching.headers.get('location')).toEqual(`${server.url}/?param=1`) // splat should be passed as query param in function redirects - t.deepEqual(functionWithSplat.queryStringParameters, { query: 'abc' }) + t.expect(functionWithSplat.queryStringParameters).toStrictEqual({ query: 'abc' }) }) }) }) - test(testName('Should not use the ZISI function bundler if not using esbuild', args), async (t) => { + test('Should not use the ZISI function bundler if not using esbuild', async (t) => { await withSiteBuilder('site-with-esm-function', async (builder) => { builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withContentFile({ path: path.join('functions', 'esm-function', 'esm-function.js'), @@ -95,15 +93,15 @@ export async function handler(event, context) { await builder.buildAsync() - await t.throwsAsync(() => + t.expect(() => withDevServer({ cwd: builder.directory, args }, async (server) => - got(`${server.url}/.netlify/functions/esm-function`).text(), + nodeFetch(`${server.url}/.netlify/functions/esm-function`).text(), ), - ) + ).rejects.toThrow() }) }) - test(testName('Should use the ZISI function bundler and serve ESM functions if using esbuild', args), async (t) => { + test('Should use the ZISI function bundler and serve ESM functions if using esbuild', async (t) => { await withSiteBuilder('site-with-esm-function', async (builder) => { builder .withNetlifyToml({ config: { functions: { directory: 'functions', node_bundler: 'esbuild' } } }) @@ -122,21 +120,19 @@ export async function handler(event, context) { await builder.buildAsync() await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got(`${server.url}/.netlify/functions/esm-function`).text() - t.is(response, 'esm') + const response = await nodeFetch(`${server.url}/.netlify/functions/esm-function`).then((res) => res.text()) + t.expect(response).toEqual('esm') }) }) }) - test( - testName('Should use the ZISI function bundler and serve TypeScript functions if using esbuild', args), - async (t) => { - await withSiteBuilder('site-with-ts-function', async (builder) => { - builder - .withNetlifyToml({ config: { functions: { directory: 'functions', node_bundler: 'esbuild' } } }) - .withContentFile({ - path: path.join('functions', 'ts-function', 'ts-function.ts'), - content: ` + test('Should use the ZISI function bundler and serve TypeScript functions if using esbuild', async (t) => { + await withSiteBuilder('site-with-ts-function', async (builder) => { + builder + .withNetlifyToml({ config: { functions: { directory: 'functions', node_bundler: 'esbuild' } } }) + .withContentFile({ + path: path.join('functions', 'ts-function', 'ts-function.ts'), + content: ` type CustomResponse = string; export const handler = async function () { @@ -149,25 +145,22 @@ export const handler = async function () { }; `, - }) + }) - await builder.buildAsync() + await builder.buildAsync() - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got(`${server.url}/.netlify/functions/ts-function`).text() - t.is(response, 'ts') - }) + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await nodeFetch(`${server.url}/.netlify/functions/ts-function`).then((res) => res.text()) + t.expect(response).toEqual('ts') }) - }, - ) - - test( - testName('Should use the ZISI function bundler and serve TypeScript functions if not using esbuild', args), - async (t) => { - await withSiteBuilder('site-with-ts-function', async (builder) => { - builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withContentFile({ - path: path.join('functions', 'ts-function', 'ts-function.ts'), - content: ` + }) + }) + + test('Should use the ZISI function bundler and serve TypeScript functions if not using esbuild', async (t) => { + await withSiteBuilder('site-with-ts-function', async (builder) => { + builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withContentFile({ + path: path.join('functions', 'ts-function', 'ts-function.ts'), + content: ` type CustomResponse = string; export const handler = async function () { @@ -180,19 +173,18 @@ export const handler = async function () { }; `, - }) + }) - await builder.buildAsync() + await builder.buildAsync() - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got(`${server.url}/.netlify/functions/ts-function`).text() - t.is(response, 'ts') - }) + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await nodeFetch(`${server.url}/.netlify/functions/ts-function`).then((res) => res.text()) + t.expect(response).toEqual('ts') }) - }, - ) + }) + }) - test(testName(`should start https server when https dev block is configured`, args), async (t) => { + test(`should start https server when https dev block is configured`, async (t) => { await withSiteBuilder('sites-with-https-certificate', async (builder) => { await builder .withNetlifyToml({ @@ -251,23 +243,37 @@ export const handler = async function () { .buildAsync() await Promise.all([ - copyFile(`${__dirname}/../../localhost.crt`, `${builder.directory}/localhost.crt`), - copyFile(`${__dirname}/../../localhost.key`, `${builder.directory}/localhost.key`), + copyFile(`${__dirname}/../../../../localhost.crt`, `${builder.directory}/localhost.crt`), + copyFile(`${__dirname}/../../../../localhost.key`, `${builder.directory}/localhost.key`), ]) await withDevServer({ cwd: builder.directory, args }, async ({ port }) => { - const options = { https: { rejectUnauthorized: false }, throwHttpErrors: false } - t.is(await got(`https://localhost:${port}/?ef=url`, options).text(), `https://localhost:${port}/?ef=url`) - t.is(await got(`https://localhost:${port}`, options).text(), 'index') - t.is(await got(`https://localhost:${port}?ef=true`, options).text(), 'INDEX') - t.deepEqual(await got(`https://localhost:${port}/api/hello`, options).json(), { + const options = { + agent: new Agent({ rejectUnauthorized: false }), + } + + t.expect(await nodeFetch(`https://localhost:${port}`, options).then((res) => res.text())).toEqual('index') + t.expect(await nodeFetch(`https://localhost:${port}?ef=true`, options).then((res) => res.text())).toEqual( + 'INDEX', + ) + t.expect(await nodeFetch(`https://localhost:${port}?ef=fetch`, options).then((res) => res.text())).toEqual( + 'origin', + ) + t.expect( + await nodeFetch(`https://localhost:${port}/api/hello`, options).then((res) => res.json()), + ).toStrictEqual({ rawUrl: `https://localhost:${port}/api/hello`, }) - t.is(await got(`https://localhost:${port}?ef=fetch`, options).text(), 'origin') + + // the fetch will go against the `https://` url of the dev server, which isn't trusted system-wide. + // this is the expected behaviour for fetch, so we shouldn't change anything about it. + t.expect(await nodeFetch(`https://localhost:${port}?ef=fetch`, options).then((res) => res.text())).toEqual( + 'origin', + ) }) }) }) - test(testName(`should use custom functions timeouts`, args), async (t) => { + test(`should use custom functions timeouts`, async (t) => { await withSiteBuilder('site-with-custom-functions-timeout', async (builder) => { await builder .withNetlifyToml({ @@ -320,8 +326,8 @@ export const handler = async function () { }, }, async ({ url }) => { - const error = await t.throwsAsync(() => got(`${url}/.netlify/functions/hello`)) - t.true(error.response.body.includes('TimeoutError: Task timed out after 1.00 seconds')) + const error = await nodeFetch(`${url}/.netlify/functions/hello`).then((res) => res.text()) + t.expect(error.includes('TimeoutError: Task timed out after 1.00 seconds')).toBe(true) }, ) }) @@ -329,38 +335,36 @@ export const handler = async function () { }) // we need curl to reproduce this issue - if (os.platform() !== 'win32') { - test(testName(`don't hang on 'Expect: 100-continue' header`, args), async () => { - await withSiteBuilder('site-with-expect-header', async (builder) => { - await builder - .withNetlifyToml({ - config: { - functions: { directory: 'functions' }, - }, - }) - .withFunction({ - path: 'hello.js', - handler: async () => ({ statusCode: 200, body: 'Hello' }), - }) - .buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - await curl(`${server.url}/.netlify/functions/hello`, [ - '-i', - '-v', - '-d', - '{"somefield":"somevalue"}', - '-H', - 'Content-Type: application/json', - '-H', - `Expect: 100-continue' header`, - ]) + test.skipIf(os.platform() === 'win32')(`don't hang on 'Expect: 100-continue' header`, async () => { + await withSiteBuilder('site-with-expect-header', async (builder) => { + await builder + .withNetlifyToml({ + config: { + functions: { directory: 'functions' }, + }, + }) + .withFunction({ + path: 'hello.js', + handler: async () => ({ statusCode: 200, body: 'Hello' }), }) + .buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + await curl(`${server.url}/.netlify/functions/hello`, [ + '-i', + '-v', + '-d', + '{"somefield":"somevalue"}', + '-H', + 'Content-Type: application/json', + '-H', + `Expect: 100-continue' header`, + ]) }) }) - } + }) - test(testName(`serves non ascii static files correctly`, args), async (t) => { + test(`serves non ascii static files correctly`, async (t) => { await withSiteBuilder('site-with-non-ascii-files', async (builder) => { await builder .withContentFile({ @@ -376,13 +380,13 @@ export const handler = async function () { .buildAsync() await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got(`${server.url}/${encodeURIComponent('范.txt')}`) - t.is(response.body, 'success') + const response = await nodeFetch(`${server.url}/${encodeURIComponent('范.txt')}`) + t.expect(await response.text()).toEqual('success') }) }) }) - test(testName(`returns headers set by function`, args), async (t) => { + test(`returns headers set by function`, async (t) => { await withSiteBuilder('site-with-function-with-custom-headers', async (builder) => { await builder .withFunction({ @@ -399,19 +403,20 @@ export const handler = async function () { .buildAsync() await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got(`${server.url}/.netlify/functions/custom-headers`) - t.falsy(response.headers.etag) - t.is(response.headers['single-value-header'], 'custom-value') - t.is(response.headers['multi-value-header'], 'custom-value1, custom-value2') - const builderResponse = await got(`${server.url}/.netlify/builders/custom-headers`) - t.falsy(builderResponse.headers.etag) - t.is(builderResponse.headers['single-value-header'], 'custom-value') - t.is(builderResponse.headers['multi-value-header'], 'custom-value1, custom-value2') + const response = await nodeFetch(`${server.url}/.netlify/functions/custom-headers`) + t.expect(response.headers.get('etag')).toBeFalsy() + t.expect(response.headers.get('single-value-header')).toEqual('custom-value') + t.expect(response.headers.get('multi-value-header')).toEqual('custom-value1, custom-value2') + + const builderResponse = await nodeFetch(`${server.url}/.netlify/builders/custom-headers`) + t.expect(builderResponse.headers.get('etag')).toBeFalsy() + t.expect(builderResponse.headers.get('single-value-header')).toEqual('custom-value') + t.expect(builderResponse.headers.get('multi-value-header')).toEqual('custom-value1, custom-value2') }) }) }) - test(testName('should match redirect when path is URL encoded', args), async (t) => { + test('should match redirect when path is URL encoded', async (t) => { await withSiteBuilder('site-with-encoded-redirect', async (builder) => { await builder .withContentFile({ path: 'static/special[test].txt', content: `special` }) @@ -420,32 +425,32 @@ export const handler = async function () { await withDevServer({ cwd: builder.directory, args }, async (server) => { const [response1, response2] = await Promise.all([ - got(`${server.url}/_next/static/special[test].txt`).text(), - got(`${server.url}/_next/static/special%5Btest%5D.txt`).text(), + nodeFetch(`${server.url}/_next/static/special[test].txt`).then((res) => res.text()), + nodeFetch(`${server.url}/_next/static/special%5Btest%5D.txt`).then((res) => res.text()), ]) - t.is(response1, 'special') - t.is(response2, 'special') + t.expect(response1).toEqual('special') + t.expect(response2).toEqual('special') }) }) }) - test(testName(`should not redirect POST request to functions server when it doesn't exists`, args), async (t) => { + test(`should not redirect POST request to functions server when it doesn't exists`, async (t) => { await withSiteBuilder('site-with-post-request', async (builder) => { await builder.buildAsync() await withDevServer({ cwd: builder.directory, args }, async (server) => { // an error is expected since we're sending a POST request to a static server // the important thing is that it's not proxied to the functions server - const error = await t.throwsAsync(() => - got.post(`${server.url}/api/test`, { - headers: { - 'content-type': 'application/x-www-form-urlencoded', - }, - body: 'some=thing', - }), - ) + const error = await nodeFetch(`${server.url}/api/test`, { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded', + }, + body: 'some=thing', + }) - t.is(error.message, 'Response code 405 (Method Not Allowed)') + t.expect(error.status).toBe(405) + t.expect(await error.text()).toEqual('Method Not Allowed') }) }) })