diff --git a/tests/integration/500.command.dev.test.cjs b/tests/integration/500.command.dev.test.cjs deleted file mode 100644 index 5b00ab04ab6..00000000000 --- a/tests/integration/500.command.dev.test.cjs +++ /dev/null @@ -1,476 +0,0 @@ -// Handlers are meant to be async outside tests -const { promises: fs } = require('fs') -const path = require('path') - -// eslint-disable-next-line ava/use-test -const avaTest = require('ava') -const { isCI } = require('ci-info') -const FormData = require('form-data') -const getPort = require('get-port') - -const { withDevServer } = require('./utils/dev-server.cjs') -const got = require('./utils/got.cjs') -const { withSiteBuilder } = require('./utils/site-builder.cjs') - -const test = isCI ? avaTest.serial.bind(avaTest) : avaTest - -test('should return 404 when redirecting to a non existing function', async (t) => { - await withSiteBuilder('site-with-missing-function', async (builder) => { - builder.withNetlifyToml({ - config: { - functions: { directory: 'functions' }, - redirects: [{ from: '/api/*', to: '/.netlify/functions/:splat', status: 200 }], - }, - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory }, async (server) => { - const response = await got - .post(`${server.url}/api/none`, { - body: 'nothing', - }) - .catch((error) => error.response) - - t.is(response.statusCode, 404) - }) - }) -}) - -test('should parse function query parameters using simple parsing', async (t) => { - await withSiteBuilder('site-with-multi-part-function', async (builder) => { - builder - .withNetlifyToml({ - config: { - functions: { directory: 'functions' }, - }, - }) - .withFunction({ - path: 'echo.js', - handler: async (event) => ({ - statusCode: 200, - body: JSON.stringify(event), - }), - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory }, async (server) => { - const response1 = await got(`${server.url}/.netlify/functions/echo?category[SOMETHING][]=something`).json() - const response2 = await got(`${server.url}/.netlify/functions/echo?category=one&category=two`).json() - - t.deepEqual(response1.queryStringParameters, { 'category[SOMETHING][]': 'something' }) - t.deepEqual(response2.queryStringParameters, { category: 'one, two' }) - }) - }) -}) - -test('should handle form submission', async (t) => { - await withSiteBuilder('site-with-form', async (builder) => { - builder - .withContentFile({ - path: 'index.html', - content: '

⊂◉‿◉つ

', - }) - .withNetlifyToml({ - config: { - functions: { directory: 'functions' }, - }, - }) - .withFunction({ - path: 'submission-created.js', - handler: async (event) => ({ - statusCode: 200, - body: JSON.stringify(event), - }), - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory }, async (server) => { - const form = new FormData() - form.append('some', 'thing') - const response = await got - .post(`${server.url}/?ding=dong`, { - body: form, - }) - .json() - - const body = JSON.parse(response.body) - const expectedBody = { - payload: { - created_at: body.payload.created_at, - data: { - ip: '::ffff:127.0.0.1', - some: 'thing', - user_agent: 'got (https://github.com/sindresorhus/got)', - }, - human_fields: { - Some: 'thing', - }, - ordered_human_fields: [ - { - name: 'some', - title: 'Some', - value: 'thing', - }, - ], - site_url: '', - }, - } - - t.is(response.headers.host, `${server.host}:${server.port}`) - t.is(response.headers['content-length'], JSON.stringify(expectedBody).length.toString()) - t.is(response.headers['content-type'], 'application/json') - t.is(response.httpMethod, 'POST') - t.is(response.isBase64Encoded, false) - t.is(response.path, '/') - t.deepEqual(response.queryStringParameters, { ding: 'dong' }) - t.deepEqual(body, expectedBody) - }) - }) -}) - -test('should handle form submission with a background function', async (t) => { - await withSiteBuilder('site-with-form-background-function', async (builder) => { - await builder - .withContentFile({ - path: 'index.html', - content: '

⊂◉‿◉つ

', - }) - .withNetlifyToml({ - config: { - functions: { directory: 'functions' }, - }, - }) - .withFunction({ - path: 'submission-created-background.js', - handler: async (event) => ({ - statusCode: 200, - body: JSON.stringify(event), - }), - }) - .buildAsync() - - await withDevServer({ cwd: builder.directory }, async (server) => { - const form = new FormData() - form.append('some', 'thing') - const response = await got.post(`${server.url}/?ding=dong`, { - body: form, - }) - t.is(response.statusCode, 202) - t.is(response.body, '') - }) - }) -}) - -test('should not handle form submission when content type is `text/plain`', async (t) => { - await withSiteBuilder('site-with-form-text-plain', async (builder) => { - builder - .withContentFile({ - path: 'index.html', - content: '

⊂◉‿◉つ

', - }) - .withNetlifyToml({ - config: { - functions: { directory: 'functions' }, - }, - }) - .withFunction({ - path: 'submission-created.js', - handler: async (event) => ({ - statusCode: 200, - body: JSON.stringify(event), - }), - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory }, async (server) => { - const response = await got - .post(`${server.url}/?ding=dong`, { - body: 'Something', - headers: { - 'content-type': 'text/plain', - }, - }) - .catch((error) => error.response) - t.is(response.statusCode, 405) - t.is(response.body, 'Method Not Allowed') - }) - }) -}) - -test('should return existing local file even when rewrite matches when force=false', async (t) => { - await withSiteBuilder('site-with-shadowing-force-false', async (builder) => { - builder - .withContentFile({ - path: 'foo.html', - content: '

foo', - }) - .withContentFile({ - path: path.join('not-foo', 'index.html'), - content: '

not-foo', - }) - .withNetlifyToml({ - config: { - redirects: [{ from: '/foo', to: '/not-foo', status: 200, force: false }], - }, - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory }, async (server) => { - const response = await got(`${server.url}/foo?ping=pong`).text() - t.is(response, '

foo') - }) - }) -}) - -test('should return existing local file even when redirect matches when force=false', async (t) => { - await withSiteBuilder('site-with-shadowing-force-false', async (builder) => { - builder - .withContentFile({ - path: 'foo.html', - content: '

foo', - }) - .withContentFile({ - path: path.join('not-foo', 'index.html'), - content: '

not-foo', - }) - .withNetlifyToml({ - config: { - redirects: [{ from: '/foo', to: '/not-foo', status: 301, force: false }], - }, - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory }, async (server) => { - const response = await got(`${server.url}/foo?ping=pong`).text() - t.is(response, '

foo') - }) - }) -}) - -test('should ignore existing local file when redirect matches and force=true', async (t) => { - await withSiteBuilder('site-with-shadowing-force-true', async (builder) => { - builder - .withContentFile({ - path: 'foo.html', - content: '

foo', - }) - .withContentFile({ - path: path.join('not-foo', 'index.html'), - content: '

not-foo', - }) - .withNetlifyToml({ - config: { - redirects: [{ from: '/foo', to: '/not-foo', status: 301, force: true }], - }, - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory }, async (server) => { - const response = await got(`${server.url}/foo`, { followRedirect: false }) - t.is(response.headers.location, `/not-foo`) - - const body = await got(`${server.url}/foo`).text() - t.is(body, '

not-foo') - }) - }) -}) - -test('should use existing file when rule contains file extension and force=false', async (t) => { - await withSiteBuilder('site-with-shadowing-file-extension-force-false', async (builder) => { - builder - .withContentFile({ - path: 'foo.html', - content: '

foo', - }) - .withContentFile({ - path: path.join('not-foo', 'index.html'), - content: '

not-foo', - }) - .withNetlifyToml({ - config: { - redirects: [{ from: '/foo.html', to: '/not-foo', status: 301, force: false }], - }, - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory }, async (server) => { - const response = await got(`${server.url}/foo.html`, { followRedirect: false }) - t.is(response.headers.location, undefined) - t.is(response.body, '

foo') - }) - }) -}) - -test('should redirect when rule contains file extension and force=true', async (t) => { - await withSiteBuilder('site-with-shadowing-file-extension-force-true', async (builder) => { - builder - .withContentFile({ - path: 'foo.html', - content: '

foo', - }) - .withContentFile({ - path: path.join('not-foo', 'index.html'), - content: '

not-foo', - }) - .withNetlifyToml({ - config: { - redirects: [{ from: '/foo.html', to: '/not-foo', status: 301, force: true }], - }, - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory }, async (server) => { - const response = await got(`${server.url}/foo.html`, { followRedirect: false }) - t.is(response.headers.location, `/not-foo`) - - const body = await got(`${server.url}/foo.html`).text() - t.is(body, '

not-foo') - }) - }) -}) - -test('should redirect from sub directory to root directory', async (t) => { - await withSiteBuilder('site-with-shadowing-sub-to-root', async (builder) => { - builder - .withContentFile({ - path: 'foo.html', - content: '

foo', - }) - .withContentFile({ - path: path.join('not-foo', 'index.html'), - content: '

not-foo', - }) - .withNetlifyToml({ - config: { - redirects: [{ from: '/not-foo', to: '/foo', status: 200, force: true }], - }, - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory }, async (server) => { - const response1 = await got(`${server.url}/not-foo`).text() - const response2 = await got(`${server.url}/not-foo/`).text() - - // TODO: check why this doesn't redirect - const response3 = await got(`${server.url}/not-foo/index.html`).text() - - t.is(response1, '

foo') - t.is(response2, '

foo') - t.is(response3, '

not-foo') - }) - }) -}) - -test('Runs build plugins with the `onPreDev` event', async (t) => { - const userServerPort = await getPort() - const pluginManifest = 'name: local-plugin' - - // This test plugin starts an HTTP server that we'll hit when the dev server - // is ready, asserting that plugins in dev mode can have long-running jobs. - const pluginSource = ` - const http = require("http"); - - module.exports = { - onPreBuild: () => { - throw new Error("I should not run"); - }, - - onPreDev: () => { - const server = http.createServer((_, res) => res.end("Hello world")); - - server.listen(${userServerPort}, "localhost", () => { - console.log("Server is running on port ${userServerPort}"); - }); - }, - }; - ` - - const { temporaryDirectory } = await import('tempy') - const pluginDirectory = await temporaryDirectory() - - await fs.writeFile(path.join(pluginDirectory, 'manifest.yml'), pluginManifest) - await fs.writeFile(path.join(pluginDirectory, 'index.js'), pluginSource) - - await withSiteBuilder('site-with-custom-server-in-plugin', async (builder) => { - builder - .withNetlifyToml({ - config: { - plugins: [{ package: path.relative(builder.directory, pluginDirectory) }], - }, - }) - .withContentFile({ - path: 'foo.html', - content: '

foo', - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory }, async (server) => { - t.is(await got(`${server.url}/foo`).text(), '

foo') - t.is(await got(`http://localhost:${userServerPort}`).text(), 'Hello world') - }) - }) -}) - -test('Handles errors from the `onPreDev` event', async (t) => { - const userServerPort = await getPort() - const pluginManifest = 'name: local-plugin' - - // This test plugin starts an HTTP server that we'll hit when the dev server - // is ready, asserting that plugins in dev mode can have long-running jobs. - const pluginSource = ` - const http = require("http"); - - module.exports = { - onPreBuild: () => { - throw new Error("I should not run"); - }, - - onPreDev: () => { - throw new Error("Something went wrong"); - }, - }; - ` - - const { temporaryDirectory } = await import('tempy') - const pluginDirectory = await temporaryDirectory() - - await fs.writeFile(path.join(pluginDirectory, 'manifest.yml'), pluginManifest) - await fs.writeFile(path.join(pluginDirectory, 'index.js'), pluginSource) - - await withSiteBuilder('site-with-custom-server-in-plugin', async (builder) => { - builder - .withNetlifyToml({ - config: { - plugins: [{ package: path.relative(builder.directory, pluginDirectory) }], - }, - }) - .withContentFile({ - path: 'foo.html', - content: '

foo', - }) - - await builder.buildAsync() - - await t.throwsAsync(() => - withDevServer( - { cwd: builder.directory }, - async (server) => { - t.is(await got(`${server.url}/foo`).text(), '

foo') - t.is(await got(`http://localhost:${userServerPort}`).text(), 'Hello world') - }, - { message: /Error: Something went wrong/ }, - ), - ) - }) -}) diff --git a/tests/integration/commands/dev/dev-forms-and-redirects.test.mjs b/tests/integration/commands/dev/dev-forms-and-redirects.test.mjs new file mode 100644 index 00000000000..31b681ed576 --- /dev/null +++ b/tests/integration/commands/dev/dev-forms-and-redirects.test.mjs @@ -0,0 +1,487 @@ +// Handlers are meant to be async outside tests +import fs from 'fs/promises' +import path from 'path' + +import FormData from 'form-data' +import getPort from 'get-port' +import fetch from 'node-fetch' +import { describe, test } from 'vitest' + +import { withDevServer } from '../../utils/dev-server.cjs' +import { withSiteBuilder } from '../../utils/site-builder.cjs' + +describe.concurrent('commands/dev-forms-and-redirects', () => { + test('should return 404 when redirecting to a non existing function', async (t) => { + await withSiteBuilder('site-with-missing-function', async (builder) => { + builder.withNetlifyToml({ + config: { + functions: { directory: 'functions' }, + redirects: [{ from: '/api/*', to: '/.netlify/functions/:splat', status: 200 }], + }, + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory }, async (server) => { + const response = await fetch(`${server.url}/api/none`, { + method: 'POST', + body: 'nothing', + }) + + t.expect(response.status).toBe(404) + }) + }) + }) + + test('should parse function query parameters using simple parsing', async (t) => { + await withSiteBuilder('site-with-multi-part-function', async (builder) => { + builder + .withNetlifyToml({ + config: { + functions: { directory: 'functions' }, + }, + }) + .withFunction({ + path: 'echo.js', + handler: async (event) => ({ + statusCode: 200, + body: JSON.stringify(event), + }), + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory }, async (server) => { + const [response1, response2] = await Promise.all([ + fetch(`${server.url}/.netlify/functions/echo?category[SOMETHING][]=something`).then((res) => res.json()), + fetch(`${server.url}/.netlify/functions/echo?category=one&category=two`).then((res) => res.json()), + ]) + + t.expect(response1.queryStringParameters).toStrictEqual({ 'category[SOMETHING][]': 'something' }) + t.expect(response2.queryStringParameters).toStrictEqual({ category: 'one, two' }) + }) + }) + }) + + test('should handle form submission', async (t) => { + await withSiteBuilder('site-with-form', async (builder) => { + builder + .withContentFile({ + path: 'index.html', + content: '

⊂◉‿◉つ

', + }) + .withNetlifyToml({ + config: { + functions: { directory: 'functions' }, + }, + }) + .withFunction({ + path: 'submission-created.js', + handler: async (event) => ({ + statusCode: 200, + body: JSON.stringify(event), + }), + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory }, async (server) => { + const form = new FormData() + form.append('some', 'thing') + const response = await fetch(`${server.url}/?ding=dong`, { + method: 'POST', + body: form, + }).then((res) => res.json()) + + const body = JSON.parse(response.body) + const expectedBody = { + payload: { + created_at: body.payload.created_at, + data: { + ip: '::ffff:127.0.0.1', + some: 'thing', + user_agent: 'node-fetch/1.0 (+https://github.com/bitinn/node-fetch)', + }, + human_fields: { + Some: 'thing', + }, + ordered_human_fields: [ + { + name: 'some', + title: 'Some', + value: 'thing', + }, + ], + site_url: '', + }, + } + + t.expect(response.headers.host).toEqual(`${server.host}:${server.port}`) + t.expect(response.headers['content-length']).toEqual(JSON.stringify(expectedBody).length.toString()) + t.expect(response.headers['content-type']).toEqual('application/json') + t.expect(response.httpMethod).toEqual('POST') + t.expect(response.isBase64Encoded).toBe(false) + t.expect(response.path).toEqual('/') + t.expect(response.queryStringParameters).toStrictEqual({ ding: 'dong' }) + t.expect(body).toStrictEqual(expectedBody) + }) + }) + }) + + test('should handle form submission with a background function', async (t) => { + await withSiteBuilder('site-with-form-background-function', async (builder) => { + await builder + .withContentFile({ + path: 'index.html', + content: '

⊂◉‿◉つ

', + }) + .withNetlifyToml({ + config: { + functions: { directory: 'functions' }, + }, + }) + .withFunction({ + path: 'submission-created-background.js', + handler: async (event) => ({ + statusCode: 200, + body: JSON.stringify(event), + }), + }) + .buildAsync() + + await withDevServer({ cwd: builder.directory }, async (server) => { + const form = new FormData() + form.append('some', 'thing') + const response = await fetch(`${server.url}/?ding=dong`, { + method: 'POST', + body: form, + }) + t.expect(response.status).toBe(202) + t.expect(await response.text()).toEqual('') + }) + }) + }) + + test('should not handle form submission when content type is `text/plain`', async (t) => { + await withSiteBuilder('site-with-form-text-plain', async (builder) => { + builder + .withContentFile({ + path: 'index.html', + content: '

⊂◉‿◉つ

', + }) + .withNetlifyToml({ + config: { + functions: { directory: 'functions' }, + }, + }) + .withFunction({ + path: 'submission-created.js', + handler: async (event) => ({ + statusCode: 200, + body: JSON.stringify(event), + }), + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory }, async (server) => { + const response = await fetch(`${server.url}/?ding=dong`, { + method: 'POST', + body: 'Something', + headers: { + 'content-type': 'text/plain', + }, + }) + t.expect(response.status).toBe(405) + t.expect(await response.text()).toEqual('Method Not Allowed') + }) + }) + }) + + test('should return existing local file even when rewrite matches when force=false', async (t) => { + await withSiteBuilder('site-with-shadowing-force-false', async (builder) => { + builder + .withContentFile({ + path: 'foo.html', + content: '

foo', + }) + .withContentFile({ + path: path.join('not-foo', 'index.html'), + content: '

not-foo', + }) + .withNetlifyToml({ + config: { + redirects: [{ from: '/foo', to: '/not-foo', status: 200, force: false }], + }, + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory }, async (server) => { + const response = await fetch(`${server.url}/foo?ping=pong`).then((res) => res.text()) + t.expect(response).toEqual('

foo') + }) + }) + }) + + test('should return existing local file even when redirect matches when force=false', async (t) => { + await withSiteBuilder('site-with-shadowing-force-false', async (builder) => { + builder + .withContentFile({ + path: 'foo.html', + content: '

foo', + }) + .withContentFile({ + path: path.join('not-foo', 'index.html'), + content: '

not-foo', + }) + .withNetlifyToml({ + config: { + redirects: [{ from: '/foo', to: '/not-foo', status: 301, force: false }], + }, + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory }, async (server) => { + const response = await fetch(`${server.url}/foo?ping=pong`).then((res) => res.text()) + t.expect(response).toEqual('

foo') + }) + }) + }) + + test('should ignore existing local file when redirect matches and force=true', async (t) => { + await withSiteBuilder('site-with-shadowing-force-true', async (builder) => { + builder + .withContentFile({ + path: 'foo.html', + content: '

foo', + }) + .withContentFile({ + path: path.join('not-foo', 'index.html'), + content: '

not-foo', + }) + .withNetlifyToml({ + config: { + redirects: [{ from: '/foo', to: '/not-foo', status: 301, force: true }], + }, + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory }, async (server) => { + const [response, body] = await Promise.all([ + fetch(`${server.url}/foo`, { redirect: 'manual' }), + fetch(`${server.url}/foo`).then((res) => res.text()), + ]) + + t.expect(response.headers.get('location')).toEqual(`${server.url}/not-foo`) + t.expect(body).toEqual('

not-foo') + }) + }) + }) + + test('should use existing file when rule contains file extension and force=false', async (t) => { + await withSiteBuilder('site-with-shadowing-file-extension-force-false', async (builder) => { + builder + .withContentFile({ + path: 'foo.html', + content: '

foo', + }) + .withContentFile({ + path: path.join('not-foo', 'index.html'), + content: '

not-foo', + }) + .withNetlifyToml({ + config: { + redirects: [{ from: '/foo.html', to: '/not-foo', status: 301, force: false }], + }, + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory }, async (server) => { + const response = await fetch(`${server.url}/foo.html`, { follow: 0 }) + t.expect(response.headers.location).toBe(undefined) + t.expect(await response.text()).toEqual('

foo') + }) + }) + }) + + test('should redirect when rule contains file extension and force=true', async (t) => { + await withSiteBuilder('site-with-shadowing-file-extension-force-true', async (builder) => { + builder + .withContentFile({ + path: 'foo.html', + content: '

foo', + }) + .withContentFile({ + path: path.join('not-foo', 'index.html'), + content: '

not-foo', + }) + .withNetlifyToml({ + config: { + redirects: [{ from: '/foo.html', to: '/not-foo', status: 301, force: true }], + }, + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory }, async (server) => { + const [response, body] = await Promise.all([ + fetch(`${server.url}/foo.html`, { redirect: 'manual' }), + fetch(`${server.url}/foo.html`).then((res) => res.text()), + ]) + + t.expect(response.headers.get('location')).toEqual(`${server.url}/not-foo`) + t.expect(body).toEqual('

not-foo') + }) + }) + }) + + test('should redirect from sub directory to root directory', async (t) => { + await withSiteBuilder('site-with-shadowing-sub-to-root', async (builder) => { + builder + .withContentFile({ + path: 'foo.html', + content: '

foo', + }) + .withContentFile({ + path: path.join('not-foo', 'index.html'), + content: '

not-foo', + }) + .withNetlifyToml({ + config: { + redirects: [{ from: '/not-foo', to: '/foo', status: 200, force: true }], + }, + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory }, async (server) => { + const [response1, response2, response3] = await Promise.all([ + fetch(`${server.url}/not-foo`).then((res) => res.text()), + fetch(`${server.url}/not-foo/`).then((res) => res.text()), + // TODO: check why this doesn't redirect + fetch(`${server.url}/not-foo/index.html`).then((res) => res.text()), + ]) + + t.expect(response1).toEqual('

foo') + t.expect(response2).toEqual('

foo') + t.expect(response3).toEqual('

not-foo') + }) + }) + }) + + test('Runs build plugins with the `onPreDev` event', async (t) => { + const userServerPort = await getPort() + const pluginManifest = 'name: local-plugin' + + // This test plugin starts an HTTP server that we'll hit when the dev server + // is ready, asserting that plugins in dev mode can have long-running jobs. + const pluginSource = ` + const http = require("http"); + + module.exports = { + onPreBuild: () => { + throw new Error("I should not run"); + }, + + onPreDev: () => { + const server = http.createServer((_, res) => res.end("Hello world")); + + server.listen(${userServerPort}, "localhost", () => { + console.log("Server is running on port ${userServerPort}"); + }); + }, + }; + ` + + const { temporaryDirectory } = await import('tempy') + const pluginDirectory = await temporaryDirectory() + + await fs.writeFile(path.join(pluginDirectory, 'manifest.yml'), pluginManifest) + await fs.writeFile(path.join(pluginDirectory, 'index.js'), pluginSource) + + await withSiteBuilder('site-with-custom-server-in-plugin', async (builder) => { + builder + .withNetlifyToml({ + config: { + plugins: [{ package: path.relative(builder.directory, pluginDirectory) }], + }, + }) + .withContentFile({ + path: 'foo.html', + content: '

foo', + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory }, async (server) => { + const [response1, response2] = await Promise.all([ + fetch(`${server.url}/foo`).then((res) => res.text()), + fetch(`http://localhost:${userServerPort}`).then((res) => res.text()), + ]) + t.expect(response1).toEqual('

foo') + t.expect(response2).toEqual('Hello world') + }) + }) + }) + + test('Handles errors from the `onPreDev` event', async (t) => { + const userServerPort = await getPort() + const pluginManifest = 'name: local-plugin' + + // This test plugin starts an HTTP server that we'll hit when the dev server + // is ready, asserting that plugins in dev mode can have long-running jobs. + const pluginSource = ` + const http = require("http"); + + module.exports = { + onPreBuild: () => { + throw new Error("I should not run"); + }, + + onPreDev: () => { + throw new Error("Something went wrong"); + }, + }; + ` + + const { temporaryDirectory } = await import('tempy') + const pluginDirectory = await temporaryDirectory() + + await fs.writeFile(path.join(pluginDirectory, 'manifest.yml'), pluginManifest) + await fs.writeFile(path.join(pluginDirectory, 'index.js'), pluginSource) + + await withSiteBuilder('site-with-custom-server-in-plugin', async (builder) => { + builder + .withNetlifyToml({ + config: { + plugins: [{ package: path.relative(builder.directory, pluginDirectory) }], + }, + }) + .withContentFile({ + path: 'foo.html', + content: '

foo', + }) + + await builder.buildAsync() + + t.expect(() => + withDevServer( + { cwd: builder.directory }, + async (server) => { + const [response1, response2] = await Promise.all([ + fetch(`${server.url}/foo`).then((res) => res.text()), + fetch(`http://localhost:${userServerPort}`).then((res) => res.text()), + ]) + await t.expect(response1).toEqual('

foo') + await t.expect(response2).toEqual('Hello world') + }, + { message: /Error: Something went wrong/ }, + ), + ).rejects.toThrowError() + }) + }) +})