diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index f6971aa4fa5..cffb4c8cc4e 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -8,19 +8,55 @@ on:
jobs:
build:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [ubuntu-latest, macOS-latest, windows-latest]
+ node-version: ['*']
+ steps:
+ # Sets an output parameter if this is a release PR
+ - name: Check for release
+ id: release-check
+ run: echo "::set-output name=IS_RELEASE::true"
+ if: "${{ startsWith(github.head_ref, 'release-') }}"
+ - name: Git checkout
+ uses: actions/checkout@v2
+ if: '${{!steps.release-check.outputs.IS_RELEASE}}'
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v2
+ with:
+ node-version: ${{ matrix.node-version }}
+ cache: 'npm'
+ cache-dependency-path: 'npm-shrinkwrap.json'
+ check-latest: true
+ if: '${{!steps.release-check.outputs.IS_RELEASE}}'
+ - name: Install core dependencies
+ run: npm ci --no-audit
+ if: '${{!steps.release-check.outputs.IS_RELEASE}}'
+ - name: Install site dependencies
+ run: npm run site:build:install
+ if: '${{!steps.release-check.outputs.IS_RELEASE}}'
+ - name: Linting
+ run: npm run format:ci
+ if: '${{!steps.release-check.outputs.IS_RELEASE}}'
+ - name: Run unit tests
+ run: npm run test:ci:ava:unit
+ if: '${{!steps.release-check.outputs.IS_RELEASE}}'
+ test:
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
node-version: [12.x, '*']
+ machine: ['0', '1', '2', '3', '4', '5', '6']
+
exclude:
- os: macOS-latest
node-version: '12.x'
- os: windows-latest
node-version: '12.x'
fail-fast: false
-
steps:
# Sets an output parameter if this is a release PR
- name: Check for release
@@ -32,11 +68,12 @@ jobs:
run: |
REG ADD HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\TCPIP\Parameters /v MaxUserPort /t REG_DWORD /d 32768 /f
REG ADD HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\TCPIP\Parameters /v TcpTimedWaitDelay /t REG_DWORD /d 30 /f
- if: "${{ matrix.os == 'windows-latest' }}"
+ if: "${{ matrix.os == 'windows-latest' && !steps.release-check.outputs.IS_RELEASE }}"
- name: Git checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
+ if: '${{!steps.release-check.outputs.IS_RELEASE}}'
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
@@ -44,24 +81,22 @@ jobs:
cache: 'npm'
cache-dependency-path: 'npm-shrinkwrap.json'
check-latest: true
+ if: '${{!steps.release-check.outputs.IS_RELEASE}}'
- name: Install core dependencies
run: npm ci --no-audit
- - name: Install site dependencies
- run: npm run site:build:install
- - name: Linting
- run: npm run format:ci
- if: "${{ matrix.node-version == '*' && !steps.release-check.outputs.IS_RELEASE}}"
+ if: '${{!steps.release-check.outputs.IS_RELEASE}}'
- name: Determine Test Command
uses: haya14busa/action-cond@v1
id: testCommand
with:
cond: ${{ github.event_name == 'pull_request' }}
if_true: 'npm run test:affected ${{ github.event.pull_request.base.sha }}' # on pull requests test with the project graph only the affected tests
- if_false: 'npm run test:ci' # on the base branch run all the tests as security measure
+ if_false: 'npm run test:ci:ava:integration' # on the base branch run all the tests as security measure
+ if: '${{ !steps.release-check.outputs.IS_RELEASE }}'
- name: Prepare tests
run: npm run test:init
- - name: Tests
if: '${{ !steps.release-check.outputs.IS_RELEASE }}'
+ - name: Tests
run: ${{ steps.testCommand.outputs.value }}
env:
# GitHub secrets are not available when running on PR from forks
@@ -74,8 +109,12 @@ jobs:
# Changes the polling interval used by the file watcher
CHOKIDAR_INTERVAL: 20
CHOKIDAR_USEPOLLING: 1
- - name: Get test coverage flags
+
+ # split tests across multiple machines
+ CI_NODE_INDEX: ${{ matrix.machine }}
+ CI_NODE_TOTAL: 7
if: '${{ !steps.release-check.outputs.IS_RELEASE }}'
+ - name: Get test coverage flags
id: test-coverage-flags
run: |-
os=${{ matrix.os }}
@@ -83,9 +122,16 @@ jobs:
echo "::set-output name=os::${os/-latest/}"
echo "::set-output name=node::node_${node//[.*]/}"
shell: bash
- - uses: codecov/codecov-action@v2
if: '${{ !steps.release-check.outputs.IS_RELEASE }}'
+ - uses: codecov/codecov-action@v2
continue-on-error: true
with:
file: coverage/coverage-final.json
flags: ${{ steps.test-coverage-flags.outputs.os }},${{ steps.test-coverage-flags.outputs.node }}
+ if: '${{ !steps.release-check.outputs.IS_RELEASE }}'
+ all:
+ needs: [build, test]
+ runs-on: ubuntu-latest
+ steps:
+ - name: Log success
+ run: echo "Finished running all tests"
diff --git a/.gitignore b/.gitignore
index 01f99a450d1..c26234151c9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,6 +28,6 @@ site/src/**/*.md
# tests
.eslintcache
-tests/hugo-site/resources
-tests/hugo-site/out
-tests/hugo-site/.hugo_build.lock
+tests/integration/hugo-site/resources
+tests/integration/hugo-site/out
+tests/integration/hugo-site/.hugo_build.lock
diff --git a/package.json b/package.json
index b275001f2f9..49d4e73219e 100644
--- a/package.json
+++ b/package.json
@@ -55,14 +55,14 @@
"format:check:prettier": "cross-env-shell prettier --check $npm_package_config_prettier",
"format:fix:prettier": "cross-env-shell prettier --write $npm_package_config_prettier",
"test:dev": "run-s test:init:* test:dev:*",
- "test:ci": "run-s test:ci:*",
"test:init": "run-s test:init:*",
"test:init:cli-version": "npm run start -- --version",
"test:init:cli-help": "npm run start -- --help",
- "test:init:eleventy-deps": "npm ci --prefix tests/eleventy-site --no-audit",
- "test:init:hugo-deps": "npm ci --prefix tests/hugo-site --no-audit",
+ "test:init:eleventy-deps": "npm ci --prefix tests/integration/eleventy-site --no-audit",
+ "test:init:hugo-deps": "npm ci --prefix tests/integration/hugo-site --no-audit",
"test:dev:ava": "ava --verbose",
- "test:ci:ava": "c8 -r json ava",
+ "test:ci:ava:unit": "c8 -r json ava --no-worker-threads tests/unit/**/*.test.js tools/**/*.test.js",
+ "test:ci:ava:integration": "c8 -r json ava --concurrency 1 --no-worker-threads tests/integration/**/*.test.js",
"test:affected": "node ./tools/affected-test.js",
"e2e": "node ./tools/e2e/run.mjs",
"docs": "node ./site/scripts/docs.js",
@@ -215,10 +215,8 @@
},
"ava": {
"files": [
- "site/**/*.test.js",
- "src/**/*.test.js",
"tools/**/*.test.js",
- "tests/*.test.js"
+ "tests/**/*.test.js"
],
"cache": true,
"concurrency": 5,
diff --git a/src/utils/rules-proxy.js b/src/utils/rules-proxy.js
index 932bd294b9a..cb9ac8013d8 100644
--- a/src/utils/rules-proxy.js
+++ b/src/utils/rules-proxy.js
@@ -11,14 +11,21 @@ const { fileExistsAsync } = require('../lib/fs')
const { NETLIFYDEVLOG } = require('./command-helpers')
const { parseRedirects } = require('./redirects')
+const watchers = []
+
const onChanges = function (files, listener) {
files.forEach((file) => {
const watcher = chokidar.watch(file)
watcher.on('change', listener)
watcher.on('unlink', listener)
+ watchers.push(watcher)
})
}
+const getWatchers = function () {
+ return watchers
+}
+
const getLanguage = function (headers) {
if (headers['accept-language']) {
return headers['accept-language'].split(',')[0].slice(0, 2)
@@ -97,4 +104,5 @@ module.exports = {
onChanges,
getLanguage,
createRewriter,
+ getWatchers,
}
diff --git a/tests/command.dev.test.js b/tests/command.dev.test.js
deleted file mode 100644
index 9647409bf7b..00000000000
--- a/tests/command.dev.test.js
+++ /dev/null
@@ -1,1913 +0,0 @@
-// Handlers are meant to be async outside tests
-/* eslint-disable require-await */
-const { copyFile } = require('fs').promises
-const http = require('http')
-const os = require('os')
-const path = require('path')
-const process = require('process')
-
-// eslint-disable-next-line ava/use-test
-const avaTest = require('ava')
-const { isCI } = require('ci-info')
-const dotProp = require('dot-prop')
-const FormData = require('form-data')
-const jwt = require('jsonwebtoken')
-
-const { curl } = require('./utils/curl')
-const { withDevServer } = require('./utils/dev-server')
-const { startExternalServer } = require('./utils/external-server')
-const got = require('./utils/got')
-const { withMockApi } = require('./utils/mock-api')
-const { withSiteBuilder } = require('./utils/site-builder')
-
-const test = isCI ? avaTest.serial.bind(avaTest) : avaTest
-
-const testMatrix = [
- { args: [] },
-
- // some tests are still failing with this enabled
- // { args: ['--edgeHandlers'] }
-]
-
-const testName = (title, args) => (args.length <= 0 ? title : `${title} - ${args.join(' ')}`)
-
-const JWT_EXPIRY = 1_893_456_000
-const getToken = ({ jwtRolePath = 'app_metadata.authorization.roles', jwtSecret = 'secret', roles }) => {
- const payload = {
- exp: JWT_EXPIRY,
- sub: '12345678',
- }
- return jwt.sign(dotProp.set(payload, jwtRolePath, roles), jwtSecret)
-}
-
-const setupRoleBasedRedirectsSite = (builder) => {
- builder
- .withContentFiles([
- {
- path: 'index.html',
- content: 'index',
- },
- {
- path: 'admin/foo.html',
- content: 'foo',
- },
- ])
- .withRedirectsFile({
- redirects: [{ from: `/admin/*`, to: ``, status: '200!', condition: 'Role=admin' }],
- })
- return builder
-}
-
-const validateRoleBasedRedirectsSite = async ({ args, builder, jwtRolePath, jwtSecret, t }) => {
- const adminToken = getToken({ jwtSecret, jwtRolePath, roles: ['admin'] })
- const editorToken = getToken({ jwtSecret, jwtRolePath, roles: ['editor'] })
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const unauthenticatedResponse = await got(`${server.url}/admin`, { throwHttpErrors: false })
- t.is(unauthenticatedResponse.statusCode, 404)
- t.is(unauthenticatedResponse.body, 'Not Found')
-
- const authenticatedResponse = await got(`${server.url}/admin/foo`, {
- headers: {
- cookie: `nf_jwt=${adminToken}`,
- },
- })
- t.is(authenticatedResponse.statusCode, 200)
- t.is(authenticatedResponse.body, 'foo')
-
- const wrongRoleResponse = await got(`${server.url}/admin/foo`, {
- headers: {
- cookie: `nf_jwt=${editorToken}`,
- },
- throwHttpErrors: false,
- })
- t.is(wrongRoleResponse.statusCode, 404)
- t.is(wrongRoleResponse.body, 'Not Found')
- })
-}
-
-testMatrix.forEach(({ args }) => {
- test(testName('should return index file when / is accessed', args), async (t) => {
- await withSiteBuilder('site-with-index-file', async (builder) => {
- builder.withContentFile({
- path: 'index.html',
- content: '
⊂◉‿◉つ
',
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const response = await got(server.url).text()
- t.is(response, '⊂◉‿◉つ
')
- })
- })
- })
-
- test(testName('should return user defined headers when / is accessed', args), async (t) => {
- await withSiteBuilder('site-with-headers-on-root', async (builder) => {
- builder.withContentFile({
- path: 'index.html',
- content: '⊂◉‿◉つ
',
- })
-
- const headerName = 'X-Frame-Options'
- const headerValue = 'SAMEORIGIN'
- builder.withHeadersFile({ headers: [{ path: '/*', headers: [`${headerName}: ${headerValue}`] }] })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const { headers } = await got(server.url)
- t.is(headers[headerName.toLowerCase()], headerValue)
- })
- })
- })
-
- test(testName('should return user defined headers when non-root path is accessed', args), async (t) => {
- await withSiteBuilder('site-with-headers-on-non-root', async (builder) => {
- builder.withContentFile({
- path: 'foo/index.html',
- content: '⊂◉‿◉つ
',
- })
-
- const headerName = 'X-Frame-Options'
- const headerValue = 'SAMEORIGIN'
- builder.withHeadersFile({ headers: [{ path: '/*', headers: [`${headerName}: ${headerValue}`] }] })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const { headers } = await got(`${server.url}/foo`)
- t.is(headers[headerName.toLowerCase()], headerValue)
- })
- })
- })
-
- test(testName('should return response from a function with setTimeout', args), async (t) => {
- await withSiteBuilder('site-with-set-timeout-function', async (builder) => {
- builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({
- path: 'timeout.js',
- handler: async () => {
- console.log('ding')
- // Wait for 4 seconds
- const FUNCTION_TIMEOUT = 4e3
- await new Promise((resolve) => {
- setTimeout(resolve, FUNCTION_TIMEOUT)
- })
- return {
- statusCode: 200,
- body: 'ping',
- metadata: { builder_function: true },
- }
- },
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const response = await got(`${server.url}/.netlify/functions/timeout`).text()
- t.is(response, 'ping')
- const builderResponse = await got(`${server.url}/.netlify/builders/timeout`).text()
- t.is(builderResponse, 'ping')
- })
- })
- })
-
- test(testName('should fail when no metadata is set for builder function', args), async (t) => {
- await withSiteBuilder('site-with-misconfigured-builder-function', async (builder) => {
- builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({
- path: 'builder.js',
- handler: async () => ({
- statusCode: 200,
- body: 'ping',
- }),
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const response = await got(`${server.url}/.netlify/functions/builder`)
- t.is(response.body, 'ping')
- t.is(response.statusCode, 200)
- const builderResponse = await got(`${server.url}/.netlify/builders/builder`, {
- throwHttpErrors: false,
- })
- t.is(
- builderResponse.body,
- `{"message":"Function is not an on-demand builder. See https://ntl.fyi/create-builder for how to convert a function to a builder."}`,
- )
- t.is(builderResponse.statusCode, 400)
- })
- })
- })
-
- test(testName('should serve function from a subdirectory', args), async (t) => {
- await withSiteBuilder('site-with-from-subdirectory', async (builder) => {
- builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({
- path: path.join('echo', 'echo.js'),
- handler: async () => ({
- statusCode: 200,
- body: 'ping',
- metadata: { builder_function: true },
- }),
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const response = await got(`${server.url}/.netlify/functions/echo`).text()
- t.is(response, 'ping')
- const builderResponse = await got(`${server.url}/.netlify/builders/echo`).text()
- t.is(builderResponse, 'ping')
- })
- })
- })
-
- test(testName('should pass .env.development vars to function', args), async (t) => {
- await withSiteBuilder('site-with-env-development', async (builder) => {
- builder
- .withNetlifyToml({ config: { functions: { directory: 'functions' } } })
- .withEnvFile({ path: '.env.development', env: { TEST: 'FROM_DEV_FILE' } })
- .withFunction({
- path: 'env.js',
- handler: async () => ({
- statusCode: 200,
- body: `${process.env.TEST}`,
- metadata: { builder_function: true },
- }),
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const response = await got(`${server.url}/.netlify/functions/env`).text()
- t.is(response, 'FROM_DEV_FILE')
- const builderResponse = await got(`${server.url}/.netlify/builders/env`).text()
- t.is(builderResponse, 'FROM_DEV_FILE')
- })
- })
- })
-
- test(testName('should pass process env vars to function', args), async (t) => {
- await withSiteBuilder('site-with-process-env', async (builder) => {
- builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({
- path: 'env.js',
- handler: async () => ({
- statusCode: 200,
- body: `${process.env.TEST}`,
- metadata: { builder_function: true },
- }),
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, env: { TEST: 'FROM_PROCESS_ENV' }, args }, async (server) => {
- const response = await got(`${server.url}/.netlify/functions/env`).text()
- t.is(response, 'FROM_PROCESS_ENV')
- const builderResponse = await got(`${server.url}/.netlify/builders/env`).text()
- t.is(builderResponse, 'FROM_PROCESS_ENV')
- })
- })
- })
-
- test(testName('should pass [build.environment] env vars to function', args), async (t) => {
- await withSiteBuilder('site-with-build-environment', async (builder) => {
- builder
- .withNetlifyToml({
- config: { build: { environment: { TEST: 'FROM_CONFIG_FILE' } }, functions: { directory: 'functions' } },
- })
- .withFunction({
- path: 'env.js',
- handler: async () => ({
- statusCode: 200,
- body: `${process.env.TEST}`,
- metadata: { builder_function: true },
- }),
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const response = await got(`${server.url}/.netlify/functions/env`).text()
- t.is(response, 'FROM_CONFIG_FILE')
- const builderResponse = await got(`${server.url}/.netlify/builders/env`).text()
- t.is(builderResponse, 'FROM_CONFIG_FILE')
- })
- })
- })
-
- test(testName('[context.dev.environment] should override [build.environment]', args), async (t) => {
- await withSiteBuilder('site-with-build-environment', async (builder) => {
- builder
- .withNetlifyToml({
- config: {
- build: { environment: { TEST: 'DEFAULT_CONTEXT' } },
- context: { dev: { environment: { TEST: 'DEV_CONTEXT' } } },
- functions: { directory: 'functions' },
- },
- })
- .withFunction({
- path: 'env.js',
- handler: async () => ({
- statusCode: 200,
- body: `${process.env.TEST}`,
- }),
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const response = await got(`${server.url}/.netlify/functions/env`).text()
- t.is(response, 'DEV_CONTEXT')
- })
- })
- })
-
- test(testName('should use [build.environment] and not [context.production.environment]', args), async (t) => {
- await withSiteBuilder('site-with-build-environment', async (builder) => {
- builder
- .withNetlifyToml({
- config: {
- build: { environment: { TEST: 'DEFAULT_CONTEXT' } },
- context: { production: { environment: { TEST: 'PRODUCTION_CONTEXT' } } },
- functions: { directory: 'functions' },
- },
- })
- .withFunction({
- path: 'env.js',
- handler: async () => ({
- statusCode: 200,
- body: `${process.env.TEST}`,
- }),
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const response = await got(`${server.url}/.netlify/functions/env`).text()
- t.is(response, 'DEFAULT_CONTEXT')
- })
- })
- })
-
- test(testName('should override .env.development with process env', args), async (t) => {
- await withSiteBuilder('site-with-override', async (builder) => {
- builder
- .withNetlifyToml({ config: { functions: { directory: 'functions' } } })
- .withEnvFile({ path: '.env.development', env: { TEST: 'FROM_DEV_FILE' } })
- .withFunction({
- path: 'env.js',
- handler: async () => ({
- statusCode: 200,
- body: `${process.env.TEST}`,
- }),
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, env: { TEST: 'FROM_PROCESS_ENV' }, args }, async (server) => {
- const response = await got(`${server.url}/.netlify/functions/env`).text()
- t.is(response, 'FROM_PROCESS_ENV')
- })
- })
- })
-
- test(testName('should override [build.environment] with process env', args), async (t) => {
- await withSiteBuilder('site-with-build-environment-override', async (builder) => {
- builder
- .withNetlifyToml({
- config: { build: { environment: { TEST: 'FROM_CONFIG_FILE' } }, functions: { directory: 'functions' } },
- })
- .withFunction({
- path: 'env.js',
- handler: async () => ({
- statusCode: 200,
- body: `${process.env.TEST}`,
- }),
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, env: { TEST: 'FROM_PROCESS_ENV' }, args }, async (server) => {
- const response = await got(`${server.url}/.netlify/functions/env`).text()
- t.is(response, 'FROM_PROCESS_ENV')
- })
- })
- })
-
- test(testName('should override value of the NETLIFY_DEV env variable', args), async (t) => {
- await withSiteBuilder('site-with-netlify-dev-override', async (builder) => {
- builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({
- path: 'env.js',
- handler: async () => ({
- statusCode: 200,
- body: `${process.env.NETLIFY_DEV}`,
- }),
- })
-
- await builder.buildAsync()
-
- await withDevServer(
- { cwd: builder.directory, env: { NETLIFY_DEV: 'FROM_PROCESS_ENV' }, args },
- async (server) => {
- const response = await got(`${server.url}/.netlify/functions/env`).text()
- t.is(response, 'true')
- },
- )
- })
- })
-
- test(testName('should set value of the CONTEXT env variable', args), async (t) => {
- await withSiteBuilder('site-with-context-override', async (builder) => {
- builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({
- path: 'env.js',
- handler: async () => ({
- statusCode: 200,
- body: `${process.env.CONTEXT}`,
- }),
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const response = await got(`${server.url}/.netlify/functions/env`).text()
- t.is(response, 'dev')
- })
- })
- })
-
- test(testName('should redirect using a wildcard when set in netlify.toml', args), async (t) => {
- await withSiteBuilder('site-with-redirect-function', async (builder) => {
- builder
- .withNetlifyToml({
- config: {
- functions: { directory: 'functions' },
- redirects: [{ from: '/api/*', to: '/.netlify/functions/:splat', status: 200 }],
- },
- })
- .withFunction({
- path: 'ping.js',
- handler: async () => ({
- statusCode: 200,
- body: 'ping',
- }),
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const response = await got(`${server.url}/api/ping`).text()
- t.is(response, 'ping')
- })
- })
- })
-
- test(testName('should pass undefined body to functions event for GET requests when redirecting', args), async (t) => {
- await withSiteBuilder('site-with-get-echo-function', async (builder) => {
- builder
- .withNetlifyToml({
- config: {
- functions: { directory: 'functions' },
- redirects: [{ from: '/api/*', to: '/.netlify/functions/:splat', status: 200 }],
- },
- })
- .withFunction({
- path: 'echo.js',
- handler: async (event) => ({
- statusCode: 200,
- body: JSON.stringify(event),
- }),
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const response = await got(`${server.url}/api/echo?ding=dong`).json()
- t.is(response.body, undefined)
- t.is(response.headers.host, `${server.host}:${server.port}`)
- t.is(response.httpMethod, 'GET')
- t.is(response.isBase64Encoded, true)
- t.is(response.path, '/api/echo')
- t.deepEqual(response.queryStringParameters, { ding: 'dong' })
- })
- })
- })
-
- test(testName('should pass body to functions event for POST requests when redirecting', args), async (t) => {
- await withSiteBuilder('site-with-post-echo-function', async (builder) => {
- builder
- .withNetlifyToml({
- config: {
- functions: { directory: 'functions' },
- redirects: [{ from: '/api/*', to: '/.netlify/functions/:splat', status: 200 }],
- },
- })
- .withFunction({
- path: 'echo.js',
- handler: async (event) => ({
- statusCode: 200,
- body: JSON.stringify(event),
- }),
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const response = await got
- .post(`${server.url}/api/echo?ding=dong`, {
- headers: {
- 'content-type': 'application/x-www-form-urlencoded',
- },
- body: 'some=thing',
- })
- .json()
-
- t.is(response.body, 'some=thing')
- t.is(response.headers.host, `${server.host}:${server.port}`)
- t.is(response.headers['content-type'], 'application/x-www-form-urlencoded')
- t.is(response.headers['content-length'], '10')
- t.is(response.httpMethod, 'POST')
- t.is(response.isBase64Encoded, false)
- t.is(response.path, '/api/echo')
- t.deepEqual(response.queryStringParameters, { ding: 'dong' })
- })
- })
- })
-
- test(testName('should return an empty body for a function with no body when redirecting', args), async (t) => {
- await withSiteBuilder('site-with-no-body-function', async (builder) => {
- builder
- .withNetlifyToml({
- config: {
- functions: { directory: 'functions' },
- redirects: [{ from: '/api/*', to: '/.netlify/functions/:splat', status: 200 }],
- },
- })
- .withFunction({
- path: 'echo.js',
- handler: async () => ({
- statusCode: 200,
- }),
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const response = await got.post(`${server.url}/api/echo?ding=dong`, {
- headers: {
- 'content-type': 'application/x-www-form-urlencoded',
- },
- body: 'some=thing',
- })
-
- t.is(response.body, '')
- t.is(response.statusCode, 200)
- })
- })
- })
-
- test(testName('should handle multipart form data when redirecting', args), async (t) => {
- await withSiteBuilder('site-with-multi-part-function', async (builder) => {
- builder
- .withNetlifyToml({
- config: {
- functions: { directory: 'functions' },
- redirects: [{ from: '/api/*', to: '/.netlify/functions/:splat', status: 200 }],
- },
- })
- .withFunction({
- path: 'echo.js',
- handler: async (event) => ({
- statusCode: 200,
- body: JSON.stringify(event),
- }),
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const form = new FormData()
- form.append('some', 'thing')
-
- const expectedBoundary = form.getBoundary()
- const expectedResponseBody = form.getBuffer().toString('base64')
-
- const response = await got
- .post(`${server.url}/api/echo?ding=dong`, {
- body: form,
- })
- .json()
-
- t.is(response.headers.host, `${server.host}:${server.port}`)
- t.is(response.headers['content-type'], `multipart/form-data; boundary=${expectedBoundary}`)
- t.is(response.headers['content-length'], '164')
- t.is(response.httpMethod, 'POST')
- t.is(response.isBase64Encoded, true)
- t.is(response.path, '/api/echo')
- t.deepEqual(response.queryStringParameters, { ding: 'dong' })
- t.is(response.body, expectedResponseBody)
- })
- })
- })
-
- test(testName('should return 404 when redirecting to a non existing function', args), 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, args }, async (server) => {
- const response = await got
- .post(`${server.url}/api/none`, {
- body: 'nothing',
- })
- .catch((error) => error.response)
-
- t.is(response.statusCode, 404)
- })
- })
- })
-
- test(testName('should parse function query parameters using simple parsing', args), 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, args }, 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(testName('should handle form submission', args), 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, args }, 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)
-
- t.is(response.headers.host, `${server.host}:${server.port}`)
- t.is(response.headers['content-length'], '276')
- 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, {
- 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: '',
- },
- })
- })
- })
- })
-
- test(testName('should handle form submission with a background function', args), 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, args }, 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(testName('should not handle form submission when content type is `text/plain`', args), 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, args }, 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.body, 'Method Not Allowed')
- })
- })
- })
-
- test(testName('should return existing local file even when rewrite matches when force=false', args), 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, args }, async (server) => {
- const response = await got(`${server.url}/foo?ping=pong`).text()
- t.is(response, 'foo')
- })
- })
- })
-
- test(testName('should return existing local file even when redirect matches when force=false', args), 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, args }, async (server) => {
- const response = await got(`${server.url}/foo?ping=pong`).text()
- t.is(response, 'foo')
- })
- })
- })
-
- test(testName('should ignore existing local file when redirect matches and force=true', args), 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: 200, force: true }],
- },
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const response = await got(`${server.url}/foo`).text()
- t.is(response, 'not-foo')
- })
- })
- })
-
- test(testName('should use existing file when rule contains file extension and force=false', args), 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: 200, force: false }],
- },
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const response = await got(`${server.url}/foo.html`).text()
- t.is(response, 'foo')
- })
- })
- })
-
- test(testName('should redirect when rule contains file extension and force=true', args), 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: 200, force: true }],
- },
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const response = await got(`${server.url}/foo.html`).text()
- t.is(response, 'not-foo')
- })
- })
- })
-
- test(testName('should redirect from sub directory to root directory', args), 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, args }, 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(testName('should return 404.html if exists for non existing routes', args), async (t) => {
- await withSiteBuilder('site-with-shadowing-404', async (builder) => {
- builder.withContentFile({
- path: '404.html',
- content: '404 - Page not found
',
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const response = await got(`${server.url}/non-existent`, { throwHttpErrors: false })
- t.is(response.body, '404 - Page not found
')
- })
- })
- })
-
- test(testName('should return 404.html from publish folder if exists for non existing routes', args), async (t) => {
- await withSiteBuilder('site-with-shadowing-404-in-publish-folder', async (builder) => {
- builder
- .withContentFile({
- path: 'public/404.html',
- content: '404 - My Custom 404 Page
',
- })
- .withNetlifyToml({
- config: {
- build: {
- publish: 'public/',
- },
- },
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const response = await got(`${server.url}/non-existent`, { throwHttpErrors: false })
- t.is(response.statusCode, 404)
- t.is(response.body, '404 - My Custom 404 Page
')
- })
- })
- })
-
- test(testName('should return 404 for redirect', args), async (t) => {
- await withSiteBuilder('site-with-shadowing-404-redirect', async (builder) => {
- builder
- .withContentFile({
- path: 'foo.html',
- content: 'foo',
- })
- .withNetlifyToml({
- config: {
- redirects: [{ from: '/test-404', to: '/foo', status: 404 }],
- },
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const response = await got(`${server.url}/test-404`, { throwHttpErrors: false })
- t.is(response.statusCode, 404)
- t.is(response.body, 'foo')
- })
- })
- })
-
- test(testName('should ignore 404 redirect for existing file', args), async (t) => {
- await withSiteBuilder('site-with-shadowing-404-redirect-existing', async (builder) => {
- builder
- .withContentFile({
- path: 'foo.html',
- content: 'foo',
- })
- .withContentFile({
- path: 'test-404.html',
- content: 'This page actually exists',
- })
- .withNetlifyToml({
- config: {
- redirects: [{ from: '/test-404', to: '/foo', status: 404 }],
- },
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const response = await got(`${server.url}/test-404`)
-
- t.is(response.statusCode, 200)
- t.is(response.body, 'This page actually exists')
- })
- })
- })
-
- test(testName('should follow 404 redirect even with existing file when force=true', args), async (t) => {
- await withSiteBuilder('site-with-shadowing-404-redirect-force', async (builder) => {
- builder
- .withContentFile({
- path: 'foo.html',
- content: 'foo',
- })
- .withContentFile({
- path: 'test-404.html',
- content: 'This page actually exists',
- })
- .withNetlifyToml({
- config: {
- redirects: [{ from: '/test-404', to: '/foo', status: 404, force: true }],
- },
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const response = await got(`${server.url}/test-404`, { throwHttpErrors: false })
-
- t.is(response.statusCode, 404)
- t.is(response.body, 'foo')
- })
- })
- })
-
- test(testName('should source redirects file from publish directory', args), async (t) => {
- await withSiteBuilder('site-redirects-file-inside-publish', async (builder) => {
- builder
- .withContentFile({
- path: 'public/index.html',
- content: 'index',
- })
- .withRedirectsFile({
- pathPrefix: 'public',
- redirects: [{ from: '/*', to: `/index.html`, status: 200 }],
- })
- .withNetlifyToml({
- config: {
- build: { publish: 'public' },
- },
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const response = await got(`${server.url}/test`)
-
- t.is(response.statusCode, 200)
- t.is(response.body, 'index')
- })
- })
- })
-
- test(testName('should redirect requests to an external server', args), async (t) => {
- await withSiteBuilder('site-redirects-file-to-external', async (builder) => {
- const externalServer = startExternalServer()
- const { port } = externalServer.address()
- builder.withRedirectsFile({
- redirects: [{ from: '/api/*', to: `http://localhost:${port}/:splat`, status: 200 }],
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const getResponse = await got(`${server.url}/api/ping`).json()
- t.deepEqual(getResponse, { body: {}, method: 'GET', url: '/ping' })
-
- const postResponse = await got
- .post(`${server.url}/api/ping`, {
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- },
- body: 'param=value',
- })
- .json()
- t.deepEqual(postResponse, { body: { param: 'value' }, method: 'POST', url: '/ping' })
- })
-
- externalServer.close()
- })
- })
-
- test(testName('should redirect POST request if content-type is missing', args), async (t) => {
- await withSiteBuilder('site-with-post-no-content-type', async (builder) => {
- builder.withNetlifyToml({
- config: {
- functions: { directory: 'functions' },
- redirects: [{ from: '/api/*', to: '/other/:splat', status: 200 }],
- },
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const options = {
- host: server.host,
- port: server.port,
- path: '/api/echo',
- method: 'POST',
- }
- let data = ''
- await new Promise((resolve) => {
- const callback = (response) => {
- response.on('data', (chunk) => {
- data += chunk
- })
- response.on('end', resolve)
- }
- const req = http.request(options, callback)
- req.write('param=value')
- req.end()
- })
-
- // we're testing Netlify Dev didn't crash
- t.is(data, 'Method Not Allowed')
- })
- })
- })
-
- test(testName('should return .html file when file and folder have the same name', args), async (t) => {
- await withSiteBuilder('site-with-same-name-for-file-and-folder', async (builder) => {
- builder
- .withContentFile({
- path: 'foo.html',
- content: 'foo',
- })
- .withContentFile({
- path: 'foo/file.html',
- content: 'file in folder',
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const response = await got(`${server.url}/foo`)
-
- t.is(response.statusCode, 200)
- t.is(response.body, 'foo')
- })
- })
- })
-
- test(testName('should not shadow an existing file that has unsafe URL characters', args), async (t) => {
- await withSiteBuilder('site-with-unsafe-url-file-names', async (builder) => {
- builder
- .withContentFile({
- path: 'public/index.html',
- content: 'index',
- })
- .withContentFile({
- path: 'public/files/file with spaces.html',
- content: 'file with spaces',
- })
- .withContentFile({
- path: 'public/files/[file_with_brackets].html',
- content: 'file with brackets',
- })
- .withNetlifyToml({
- config: {
- build: { publish: 'public' },
- redirects: [{ from: '/*', to: '/index.html', status: 200 }],
- },
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const [spaces, brackets] = await Promise.all([
- got(`${server.url}/files/file with spaces`).text(),
- got(`${server.url}/files/[file_with_brackets]`).text(),
- ])
-
- t.is(spaces, 'file with spaces')
- t.is(brackets, 'file with brackets')
- })
- })
- })
-
- test(testName('should follow redirect for fully qualified rule', args), async (t) => {
- await withSiteBuilder('site-with-fully-qualified-redirect-rule', async (builder) => {
- const publicDir = 'public'
- builder
- .withNetlifyToml({
- config: {
- build: { publish: publicDir },
- },
- })
- .withContentFiles([
- {
- path: path.join(publicDir, 'index.html'),
- content: 'index',
- },
- {
- path: path.join(publicDir, 'local-hello.html'),
- content: 'hello',
- },
- ])
- .withRedirectsFile({
- redirects: [{ from: `http://localhost/hello-world`, to: `/local-hello`, status: 200 }],
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const response = await got(`${server.url}/hello-world`)
-
- t.is(response.statusCode, 200)
- t.is(response.body, 'hello')
- })
- })
- })
-
- test(testName('should return 202 ok and empty response for background function', args), async (t) => {
- await withSiteBuilder('site-with-background-function', async (builder) => {
- builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({
- path: 'hello-background.js',
- handler: () => {
- console.log("Look at me I'm a background task")
- },
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const response = await got(`${server.url}/.netlify/functions/hello-background`)
- t.is(response.statusCode, 202)
- t.is(response.body, '')
- })
- })
- })
-
- test(testName('should enforce role based redirects with default secret and role path', args), async (t) => {
- await withSiteBuilder('site-with-default-role-based-redirects', async (builder) => {
- setupRoleBasedRedirectsSite(builder)
- await builder.buildAsync()
- await validateRoleBasedRedirectsSite({ builder, args, t })
- })
- })
-
- test(testName('should enforce role based redirects with custom secret and role path', args), async (t) => {
- await withSiteBuilder('site-with-custom-role-based-redirects', async (builder) => {
- const jwtSecret = 'custom'
- const jwtRolePath = 'roles'
- setupRoleBasedRedirectsSite(builder).withNetlifyToml({
- config: {
- dev: {
- jwtSecret,
- jwtRolePath,
- },
- },
- })
- await builder.buildAsync()
- await validateRoleBasedRedirectsSite({ builder, args, t, jwtSecret, jwtRolePath })
- })
- })
-
- test(testName('routing-local-proxy serves edge handlers with --edgeHandlers flag', args), async (t) => {
- await withSiteBuilder('site-with-fully-qualified-redirect-rule', async (builder) => {
- const publicDir = 'public'
- builder
- .withNetlifyToml({
- config: {
- build: {
- publish: publicDir,
- edge_handlers: 'netlify/edge-handlers',
- },
- 'edge-handlers': [
- {
- handler: 'smoke',
- path: '/edge-handler',
- },
- ],
- },
- })
- .withContentFiles([
- {
- path: path.join(publicDir, 'index.html'),
- content: 'index',
- },
- ])
- .withEdgeHandlers({
- fileName: 'smoke.js',
- handlers: {
- onRequest: (event) => {
- event.replaceResponse(
- // eslint-disable-next-line no-undef
- new Response(null, {
- headers: {
- Location: 'https://google.com/',
- },
- status: 301,
- }),
- )
- },
- },
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args: [...args, '--edgeHandlers'] }, async (server) => {
- const response = await got(`${server.url}/edge-handler`, {
- followRedirect: false,
- })
-
- t.is(response.statusCode, 301)
- t.is(response.headers.location, 'https://google.com/')
- })
- })
- })
-
- test(testName('routing-local-proxy serves edge handlers with deprecated --trafficMesh flag', args), async (t) => {
- await withSiteBuilder('site-with-fully-qualified-redirect-rule', async (builder) => {
- const publicDir = 'public'
- builder
- .withNetlifyToml({
- config: {
- build: {
- publish: publicDir,
- edge_handlers: 'netlify/edge-handlers',
- },
- 'edge-handlers': [
- {
- handler: 'smoke',
- path: '/edge-handler',
- },
- ],
- },
- })
- .withContentFiles([
- {
- path: path.join(publicDir, 'index.html'),
- content: 'index',
- },
- ])
- .withEdgeHandlers({
- fileName: 'smoke.js',
- handlers: {
- onRequest: (event) => {
- event.replaceResponse(
- // eslint-disable-next-line no-undef
- new Response(null, {
- headers: {
- Location: 'https://google.com/',
- },
- status: 301,
- }),
- )
- },
- },
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args: [...args, '--trafficMesh'] }, async (server) => {
- const response = await got(`${server.url}/edge-handler`, {
- followRedirect: false,
- })
-
- t.is(response.statusCode, 301)
- t.is(response.headers.location, 'https://google.com/')
- })
- })
- })
-
- test(testName('routing-local-proxy builds projects w/o edge handlers', args), async (t) => {
- await withSiteBuilder('site-with-fully-qualified-redirect-rule', async (builder) => {
- const publicDir = 'public'
- builder
- .withNetlifyToml({
- config: {
- build: { publish: publicDir },
- },
- })
- .withContentFiles([
- {
- path: path.join(publicDir, 'index.html'),
- content: 'index',
- },
- ])
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args: [...args, '--edgeHandlers'] }, async (server) => {
- const response = await got(`${server.url}/index.html`)
-
- t.is(response.statusCode, 200)
- })
- })
- })
-
- test(testName('redirect with country cookie', args), async (t) => {
- await withSiteBuilder('site-with-country-cookie', async (builder) => {
- builder
- .withContentFiles([
- {
- path: 'index.html',
- content: 'index',
- },
- {
- path: 'index-es.html',
- content: 'index in spanish',
- },
- ])
- .withRedirectsFile({
- redirects: [{ from: `/`, to: `/index-es.html`, status: '200!', condition: 'Country=ES' }],
- })
-
- await builder.buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const response = await got(`${server.url}/`, {
- headers: {
- cookie: `nf_country=ES`,
- },
- })
- t.is(response.statusCode, 200)
- t.is(response.body, 'index in spanish')
- })
- })
- })
-
- test(testName(`doesn't hang when sending a application/json POST request to function server`, args), async (t) => {
- await withSiteBuilder('site-with-functions', async (builder) => {
- const functionsPort = 6666
- await builder
- .withNetlifyToml({ config: { functions: { directory: 'functions' }, dev: { functionsPort } } })
- .buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async ({ port, url }) => {
- const response = await got(`${url.replace(port, functionsPort)}/test`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: '{}',
- throwHttpErrors: false,
- })
- t.is(response.statusCode, 404)
- t.is(response.body, 'Function not found...')
- })
- })
- })
-
- test(testName(`catches invalid function names`, args), async (t) => {
- await withSiteBuilder('site-with-functions', async (builder) => {
- const functionsPort = 6667
- await builder
- .withNetlifyToml({ config: { functions: { directory: 'functions' }, dev: { functionsPort } } })
- .withFunction({
- path: 'exclamat!on.js',
- handler: async (event) => ({
- statusCode: 200,
- body: JSON.stringify(event),
- }),
- })
- .buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async ({ port, url }) => {
- const response = await got(`${url.replace(port, functionsPort)}/exclamat!on`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: '{}',
- throwHttpErrors: false,
- })
- t.is(response.statusCode, 400)
- t.is(response.body, 'Function name should consist only of alphanumeric characters, hyphen & underscores.')
- })
- })
- })
-
- test(testName('should handle query params in redirects', args), async (t) => {
- await withSiteBuilder('site-with-query-redirects', async (builder) => {
- await builder
- .withContentFile({
- path: 'public/index.html',
- content: 'home',
- })
- .withNetlifyToml({
- config: {
- build: { publish: 'public' },
- functions: { directory: 'functions' },
- },
- })
- .withRedirectsFile({
- redirects: [
- { from: `/api/*`, to: `/.netlify/functions/echo?a=1&a=2`, status: '200' },
- { from: `/foo`, to: `/`, status: '302' },
- { from: `/bar`, to: `/?a=1&a=2`, status: '302' },
- { from: `/test id=:id`, to: `/?param=:id` },
- ],
- })
- .withFunction({
- path: 'echo.js',
- handler: async (event) => ({
- statusCode: 200,
- body: JSON.stringify(event),
- }),
- })
- .buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const [fromFunction, queryPassthrough, queryInRedirect, withParamMatching] = 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 }),
- ])
-
- // query params should be taken from the request
- t.deepEqual(fromFunction.multiValueQueryStringParameters, { foo: ['1', '2'], bar: ['1', '2'] })
-
- // query params should be passed through from the request
- t.is(queryPassthrough.headers.location, '/?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')
-
- // query params should be taken from the redirect rule
- t.is(withParamMatching.headers.location, '/?param=1')
- })
- })
- })
-
- test(testName('Should not use the ZISI function bundler if not using esbuild', args), 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'),
- content: `
-export async function handler(event, context) {
- return {
- statusCode: 200,
- body: 'esm',
- };
-}
- `,
- })
-
- await builder.buildAsync()
-
- await t.throwsAsync(() =>
- withDevServer({ cwd: builder.directory, args }, async (server) =>
- got(`${server.url}/.netlify/functions/esm-function`).text(),
- ),
- )
- })
- })
-
- test(testName('Should use the ZISI function bundler and serve ESM functions if using esbuild', args), async (t) => {
- await withSiteBuilder('site-with-esm-function', async (builder) => {
- builder
- .withNetlifyToml({ config: { functions: { directory: 'functions', node_bundler: 'esbuild' } } })
- .withContentFile({
- path: path.join('functions', 'esm-function', 'esm-function.js'),
- content: `
-export async function handler(event, context) {
- return {
- statusCode: 200,
- body: 'esm',
- };
-}
- `,
- })
-
- 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')
- })
- })
- })
-
- 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: `
-type CustomResponse = string;
-
-export const handler = async function () {
- const response: CustomResponse = "ts";
-
- return {
- statusCode: 200,
- body: response,
- };
-};
-
- `,
- })
-
- 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')
- })
- })
- },
- )
-
- 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: `
-type CustomResponse = string;
-
-export const handler = async function () {
- const response: CustomResponse = "ts";
-
- return {
- statusCode: 200,
- body: response,
- };
-};
-
- `,
- })
-
- 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')
- })
- })
- },
- )
-
- test(testName(`should start https server when https dev block is configured`, args), async (t) => {
- await withSiteBuilder('sites-with-https-certificate', async (builder) => {
- await builder
- .withNetlifyToml({
- config: {
- build: { publish: 'public' },
- functions: { directory: 'functions' },
- dev: { https: { certFile: 'cert.pem', keyFile: 'key.pem' } },
- },
- })
- .withContentFile({
- path: 'public/index.html',
- content: 'index',
- })
- .withRedirectsFile({
- redirects: [{ from: `/api/*`, to: `/.netlify/functions/:splat`, status: '200' }],
- })
- .withFunction({
- path: 'hello.js',
- handler: async () => ({
- statusCode: 200,
- body: 'Hello World',
- }),
- })
- .buildAsync()
-
- await Promise.all([
- copyFile(`${__dirname}/assets/cert.pem`, `${builder.directory}/cert.pem`),
- copyFile(`${__dirname}/assets/key.pem`, `${builder.directory}/key.pem`),
- ])
- await withDevServer({ cwd: builder.directory, args }, async ({ port }) => {
- const options = { https: { rejectUnauthorized: false } }
- t.is(await got(`https://localhost:${port}`, options).text(), 'index')
- t.is(await got(`https://localhost:${port}/api/hello`, options).text(), 'Hello World')
- })
- })
- })
-
- test(testName(`should use custom functions timeouts`, args), async (t) => {
- await withSiteBuilder('site-with-custom-functions-timeout', async (builder) => {
- await builder
- .withNetlifyToml({
- config: {
- build: { publish: 'public' },
- functions: { directory: 'functions' },
- },
- })
- .withFunction({
- path: 'hello.js',
- handler: async () => {
- await new Promise((resolve) => {
- const SLEEP_TIME = 2000
- setTimeout(resolve, SLEEP_TIME)
- })
- return {
- statusCode: 200,
- body: 'Hello World',
- }
- },
- })
- .buildAsync()
-
- const siteInfo = {
- account_slug: 'test-account',
- id: 'site_id',
- name: 'site-name',
- functions_config: { timeout: 1 },
- }
-
- const routes = [
- { path: 'sites/site_id', response: siteInfo },
-
- { path: 'sites/site_id/service-instances', response: [] },
- {
- path: 'accounts',
- response: [{ slug: siteInfo.account_slug }],
- },
- ]
-
- await withMockApi(routes, async ({ apiUrl }) => {
- await withDevServer(
- {
- cwd: builder.directory,
- offline: false,
- env: {
- NETLIFY_API_URL: apiUrl,
- NETLIFY_SITE_ID: 'site_id',
- NETLIFY_AUTH_TOKEN: 'fake-token',
- },
- },
- 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'))
- },
- )
- })
- })
- })
-
- // 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(testName(`serves non ascii static files correctly`, args), async (t) => {
- await withSiteBuilder('site-with-non-ascii-files', async (builder) => {
- await builder
- .withContentFile({
- path: 'public/范.txt',
- content: 'success',
- })
- .withNetlifyToml({
- config: {
- build: { publish: 'public' },
- redirects: [{ from: '/*', to: '/index.html', status: 200 }],
- },
- })
- .buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const response = await got(`${server.url}/${encodeURIComponent('范.txt')}`)
- t.is(response.body, 'success')
- })
- })
- })
-
- test(testName(`returns headers set by function`, args), async (t) => {
- await withSiteBuilder('site-with-function-with-custom-headers', async (builder) => {
- await builder
- .withFunction({
- pathPrefix: 'netlify/functions',
- path: 'custom-headers.js',
- handler: async () => ({
- statusCode: 200,
- body: '',
- headers: { 'single-value-header': 'custom-value' },
- multiValueHeaders: { 'multi-value-header': ['custom-value1', 'custom-value2'] },
- metadata: { builder_function: true },
- }),
- })
- .buildAsync()
-
- await withDevServer({ cwd: builder.directory, args }, async (server) => {
- const response = await got(`${server.url}/.netlify/functions/custom-headers`)
- 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.is(builderResponse.headers['single-value-header'], 'custom-value')
- t.is(builderResponse.headers['multi-value-header'], 'custom-value1, custom-value2')
- })
- })
- })
-
- test(testName('should match redirect when path is URL encoded', args), async (t) => {
- await withSiteBuilder('site-with-encoded-redirect', async (builder) => {
- await builder
- .withContentFile({ path: 'static/special[test].txt', content: `special` })
- .withRedirectsFile({ redirects: [{ from: '/_next/static/*', to: '/static/:splat', status: 200 }] })
- .buildAsync()
-
- 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(),
- ])
- t.is(response1, 'special')
- t.is(response2, 'special')
- })
- })
- })
-
- test(testName(`should not redirect POST request to functions server when it doesn't exists`, args), 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',
- }),
- )
-
- t.is(error.message, 'Response code 405 (Method Not Allowed)')
- })
- })
- })
-})
-/* eslint-enable require-await */
diff --git a/tests/integration/0.command.dev.test.js b/tests/integration/0.command.dev.test.js
new file mode 100644
index 00000000000..37238b4dee0
--- /dev/null
+++ b/tests/integration/0.command.dev.test.js
@@ -0,0 +1,297 @@
+// Handlers are meant to be async outside tests
+const http = require('http')
+
+// eslint-disable-next-line ava/use-test
+const avaTest = require('ava')
+const { isCI } = require('ci-info')
+
+const { withDevServer } = require('./utils/dev-server')
+const { startExternalServer } = require('./utils/external-server')
+const got = require('./utils/got')
+const { withSiteBuilder } = require('./utils/site-builder')
+
+const test = isCI ? avaTest.serial.bind(avaTest) : avaTest
+
+const testMatrix = [
+ { args: [] },
+
+ // some tests are still failing with this enabled
+ // { args: ['--edgeHandlers'] }
+]
+
+const testName = (title, args) => (args.length <= 0 ? title : `${title} - ${args.join(' ')}`)
+
+testMatrix.forEach(({ args }) => {
+ test(testName('should return 404.html if exists for non existing routes', args), async (t) => {
+ await withSiteBuilder('site-with-shadowing-404', async (builder) => {
+ builder.withContentFile({
+ path: '404.html',
+ content: '404 - Page not found
',
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const response = await got(`${server.url}/non-existent`, { throwHttpErrors: false })
+ t.is(response.body, '404 - Page not found
')
+ })
+ })
+ })
+
+ test(testName('should return 404.html from publish folder if exists for non existing routes', args), async (t) => {
+ await withSiteBuilder('site-with-shadowing-404-in-publish-folder', async (builder) => {
+ builder
+ .withContentFile({
+ path: 'public/404.html',
+ content: '404 - My Custom 404 Page
',
+ })
+ .withNetlifyToml({
+ config: {
+ build: {
+ publish: 'public/',
+ },
+ },
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const response = await got(`${server.url}/non-existent`, { throwHttpErrors: false })
+ t.is(response.statusCode, 404)
+ t.is(response.body, '404 - My Custom 404 Page
')
+ })
+ })
+ })
+
+ test(testName('should return 404 for redirect', args), async (t) => {
+ await withSiteBuilder('site-with-shadowing-404-redirect', async (builder) => {
+ builder
+ .withContentFile({
+ path: 'foo.html',
+ content: 'foo',
+ })
+ .withNetlifyToml({
+ config: {
+ redirects: [{ from: '/test-404', to: '/foo', status: 404 }],
+ },
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const response = await got(`${server.url}/test-404`, { throwHttpErrors: false })
+ t.is(response.statusCode, 404)
+ t.is(response.body, 'foo')
+ })
+ })
+ })
+
+ test(testName('should ignore 404 redirect for existing file', args), async (t) => {
+ await withSiteBuilder('site-with-shadowing-404-redirect-existing', async (builder) => {
+ builder
+ .withContentFile({
+ path: 'foo.html',
+ content: 'foo',
+ })
+ .withContentFile({
+ path: 'test-404.html',
+ content: 'This page actually exists',
+ })
+ .withNetlifyToml({
+ config: {
+ redirects: [{ from: '/test-404', to: '/foo', status: 404 }],
+ },
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const response = await got(`${server.url}/test-404`)
+
+ t.is(response.statusCode, 200)
+ t.is(response.body, 'This page actually exists')
+ })
+ })
+ })
+
+ test(testName('should follow 404 redirect even with existing file when force=true', args), async (t) => {
+ await withSiteBuilder('site-with-shadowing-404-redirect-force', async (builder) => {
+ builder
+ .withContentFile({
+ path: 'foo.html',
+ content: 'foo',
+ })
+ .withContentFile({
+ path: 'test-404.html',
+ content: 'This page actually exists',
+ })
+ .withNetlifyToml({
+ config: {
+ redirects: [{ from: '/test-404', to: '/foo', status: 404, force: true }],
+ },
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const response = await got(`${server.url}/test-404`, { throwHttpErrors: false })
+
+ t.is(response.statusCode, 404)
+ t.is(response.body, 'foo')
+ })
+ })
+ })
+
+ test(testName('should source redirects file from publish directory', args), async (t) => {
+ await withSiteBuilder('site-redirects-file-inside-publish', async (builder) => {
+ builder
+ .withContentFile({
+ path: 'public/index.html',
+ content: 'index',
+ })
+ .withRedirectsFile({
+ pathPrefix: 'public',
+ redirects: [{ from: '/*', to: `/index.html`, status: 200 }],
+ })
+ .withNetlifyToml({
+ config: {
+ build: { publish: 'public' },
+ },
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const response = await got(`${server.url}/test`)
+
+ t.is(response.statusCode, 200)
+ t.is(response.body, 'index')
+ })
+ })
+ })
+
+ test(testName('should redirect requests to an external server', args), async (t) => {
+ await withSiteBuilder('site-redirects-file-to-external', async (builder) => {
+ const externalServer = startExternalServer()
+ const { port } = externalServer.address()
+ builder.withRedirectsFile({
+ redirects: [{ from: '/api/*', to: `http://localhost:${port}/:splat`, status: 200 }],
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const getResponse = await got(`${server.url}/api/ping`).json()
+ t.deepEqual(getResponse, { body: {}, method: 'GET', url: '/ping' })
+
+ const postResponse = await got
+ .post(`${server.url}/api/ping`, {
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ body: 'param=value',
+ })
+ .json()
+ t.deepEqual(postResponse, { body: { param: 'value' }, method: 'POST', url: '/ping' })
+ })
+
+ externalServer.close()
+ })
+ })
+
+ test(testName('should redirect POST request if content-type is missing', args), async (t) => {
+ await withSiteBuilder('site-with-post-no-content-type', async (builder) => {
+ builder.withNetlifyToml({
+ config: {
+ functions: { directory: 'functions' },
+ redirects: [{ from: '/api/*', to: '/other/:splat', status: 200 }],
+ },
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const options = {
+ host: server.host,
+ port: server.port,
+ path: '/api/echo',
+ method: 'POST',
+ }
+ let data = ''
+ await new Promise((resolve) => {
+ const callback = (response) => {
+ response.on('data', (chunk) => {
+ data += chunk
+ })
+ response.on('end', resolve)
+ }
+ const req = http.request(options, callback)
+ req.write('param=value')
+ req.end()
+ })
+
+ // we're testing Netlify Dev didn't crash
+ t.is(data, 'Method Not Allowed')
+ })
+ })
+ })
+
+ test(testName('should return .html file when file and folder have the same name', args), async (t) => {
+ await withSiteBuilder('site-with-same-name-for-file-and-folder', async (builder) => {
+ builder
+ .withContentFile({
+ path: 'foo.html',
+ content: 'foo',
+ })
+ .withContentFile({
+ path: 'foo/file.html',
+ content: 'file in folder',
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const response = await got(`${server.url}/foo`)
+
+ t.is(response.statusCode, 200)
+ t.is(response.body, 'foo')
+ })
+ })
+ })
+
+ test(testName('should not shadow an existing file that has unsafe URL characters', args), async (t) => {
+ await withSiteBuilder('site-with-unsafe-url-file-names', async (builder) => {
+ builder
+ .withContentFile({
+ path: 'public/index.html',
+ content: 'index',
+ })
+ .withContentFile({
+ path: 'public/files/file with spaces.html',
+ content: 'file with spaces',
+ })
+ .withContentFile({
+ path: 'public/files/[file_with_brackets].html',
+ content: 'file with brackets',
+ })
+ .withNetlifyToml({
+ config: {
+ build: { publish: 'public' },
+ redirects: [{ from: '/*', to: '/index.html', status: 200 }],
+ },
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const [spaces, brackets] = await Promise.all([
+ got(`${server.url}/files/file with spaces`).text(),
+ got(`${server.url}/files/[file_with_brackets]`).text(),
+ ])
+
+ t.is(spaces, 'file with spaces')
+ t.is(brackets, 'file with brackets')
+ })
+ })
+ })
+})
diff --git a/tests/command.addons.test.js b/tests/integration/10.command.addons.test.js
similarity index 100%
rename from tests/command.addons.test.js
rename to tests/integration/10.command.addons.test.js
diff --git a/tests/integration/100.command.dev.test.js b/tests/integration/100.command.dev.test.js
new file mode 100644
index 00000000000..38c9593f6f9
--- /dev/null
+++ b/tests/integration/100.command.dev.test.js
@@ -0,0 +1,376 @@
+// Handlers are meant to be async outside tests
+/* eslint-disable require-await */
+const path = require('path')
+
+// eslint-disable-next-line ava/use-test
+const avaTest = require('ava')
+const { isCI } = require('ci-info')
+const dotProp = require('dot-prop')
+const jwt = require('jsonwebtoken')
+
+const { withDevServer } = require('./utils/dev-server')
+const got = require('./utils/got')
+const { withSiteBuilder } = require('./utils/site-builder')
+
+const test = isCI ? avaTest.serial.bind(avaTest) : avaTest
+
+const testMatrix = [
+ { args: [] },
+
+ // some tests are still failing with this enabled
+ // { args: ['--edgeHandlers'] }
+]
+
+const testName = (title, args) => (args.length <= 0 ? title : `${title} - ${args.join(' ')}`)
+
+const JWT_EXPIRY = 1_893_456_000
+const getToken = ({ jwtRolePath = 'app_metadata.authorization.roles', jwtSecret = 'secret', roles }) => {
+ const payload = {
+ exp: JWT_EXPIRY,
+ sub: '12345678',
+ }
+ return jwt.sign(dotProp.set(payload, jwtRolePath, roles), jwtSecret)
+}
+
+const setupRoleBasedRedirectsSite = (builder) => {
+ builder
+ .withContentFiles([
+ {
+ path: 'index.html',
+ content: 'index',
+ },
+ {
+ path: 'admin/foo.html',
+ content: 'foo',
+ },
+ ])
+ .withRedirectsFile({
+ redirects: [{ from: `/admin/*`, to: ``, status: '200!', condition: 'Role=admin' }],
+ })
+ return builder
+}
+
+const validateRoleBasedRedirectsSite = async ({ args, builder, jwtRolePath, jwtSecret, t }) => {
+ const adminToken = getToken({ jwtSecret, jwtRolePath, roles: ['admin'] })
+ const editorToken = getToken({ jwtSecret, jwtRolePath, roles: ['editor'] })
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const unauthenticatedResponse = await got(`${server.url}/admin`, { throwHttpErrors: false })
+ t.is(unauthenticatedResponse.statusCode, 404)
+ t.is(unauthenticatedResponse.body, 'Not Found')
+
+ const authenticatedResponse = await got(`${server.url}/admin/foo`, {
+ headers: {
+ cookie: `nf_jwt=${adminToken}`,
+ },
+ })
+ t.is(authenticatedResponse.statusCode, 200)
+ t.is(authenticatedResponse.body, 'foo')
+
+ const wrongRoleResponse = await got(`${server.url}/admin/foo`, {
+ headers: {
+ cookie: `nf_jwt=${editorToken}`,
+ },
+ throwHttpErrors: false,
+ })
+ t.is(wrongRoleResponse.statusCode, 404)
+ t.is(wrongRoleResponse.body, 'Not Found')
+ })
+}
+
+testMatrix.forEach(({ args }) => {
+ test(testName('should follow redirect for fully qualified rule', args), async (t) => {
+ await withSiteBuilder('site-with-fully-qualified-redirect-rule', async (builder) => {
+ const publicDir = 'public'
+ builder
+ .withNetlifyToml({
+ config: {
+ build: { publish: publicDir },
+ },
+ })
+ .withContentFiles([
+ {
+ path: path.join(publicDir, 'index.html'),
+ content: 'index',
+ },
+ {
+ path: path.join(publicDir, 'local-hello.html'),
+ content: 'hello',
+ },
+ ])
+ .withRedirectsFile({
+ redirects: [{ from: `http://localhost/hello-world`, to: `/local-hello`, status: 200 }],
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const response = await got(`${server.url}/hello-world`)
+
+ t.is(response.statusCode, 200)
+ t.is(response.body, 'hello')
+ })
+ })
+ })
+
+ test(testName('should return 202 ok and empty response for background function', args), async (t) => {
+ await withSiteBuilder('site-with-background-function', async (builder) => {
+ builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({
+ path: 'hello-background.js',
+ handler: () => {
+ console.log("Look at me I'm a background task")
+ },
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const response = await got(`${server.url}/.netlify/functions/hello-background`)
+ t.is(response.statusCode, 202)
+ t.is(response.body, '')
+ })
+ })
+ })
+
+ test(testName('should enforce role based redirects with default secret and role path', args), async (t) => {
+ await withSiteBuilder('site-with-default-role-based-redirects', async (builder) => {
+ setupRoleBasedRedirectsSite(builder)
+ await builder.buildAsync()
+ await validateRoleBasedRedirectsSite({ builder, args, t })
+ })
+ })
+
+ test(testName('should enforce role based redirects with custom secret and role path', args), async (t) => {
+ await withSiteBuilder('site-with-custom-role-based-redirects', async (builder) => {
+ const jwtSecret = 'custom'
+ const jwtRolePath = 'roles'
+ setupRoleBasedRedirectsSite(builder).withNetlifyToml({
+ config: {
+ dev: {
+ jwtSecret,
+ jwtRolePath,
+ },
+ },
+ })
+ await builder.buildAsync()
+ await validateRoleBasedRedirectsSite({ builder, args, t, jwtSecret, jwtRolePath })
+ })
+ })
+
+ test(testName('routing-local-proxy serves edge handlers with --edgeHandlers flag', args), async (t) => {
+ await withSiteBuilder('site-with-fully-qualified-redirect-rule', async (builder) => {
+ const publicDir = 'public'
+ builder
+ .withNetlifyToml({
+ config: {
+ build: {
+ publish: publicDir,
+ edge_handlers: 'netlify/edge-handlers',
+ },
+ 'edge-handlers': [
+ {
+ handler: 'smoke',
+ path: '/edge-handler',
+ },
+ ],
+ },
+ })
+ .withContentFiles([
+ {
+ path: path.join(publicDir, 'index.html'),
+ content: 'index',
+ },
+ ])
+ .withEdgeHandlers({
+ fileName: 'smoke.js',
+ handlers: {
+ onRequest: (event) => {
+ event.replaceResponse(
+ // eslint-disable-next-line no-undef
+ new Response(null, {
+ headers: {
+ Location: 'https://google.com/',
+ },
+ status: 301,
+ }),
+ )
+ },
+ },
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args: [...args, '--edgeHandlers'] }, async (server) => {
+ const response = await got(`${server.url}/edge-handler`, {
+ followRedirect: false,
+ })
+
+ t.is(response.statusCode, 301)
+ t.is(response.headers.location, 'https://google.com/')
+ })
+ })
+ })
+
+ test(testName('routing-local-proxy serves edge handlers with deprecated --trafficMesh flag', args), async (t) => {
+ await withSiteBuilder('site-with-fully-qualified-redirect-rule', async (builder) => {
+ const publicDir = 'public'
+ builder
+ .withNetlifyToml({
+ config: {
+ build: {
+ publish: publicDir,
+ edge_handlers: 'netlify/edge-handlers',
+ },
+ 'edge-handlers': [
+ {
+ handler: 'smoke',
+ path: '/edge-handler',
+ },
+ ],
+ },
+ })
+ .withContentFiles([
+ {
+ path: path.join(publicDir, 'index.html'),
+ content: 'index',
+ },
+ ])
+ .withEdgeHandlers({
+ fileName: 'smoke.js',
+ handlers: {
+ onRequest: (event) => {
+ event.replaceResponse(
+ // eslint-disable-next-line no-undef
+ new Response(null, {
+ headers: {
+ Location: 'https://google.com/',
+ },
+ status: 301,
+ }),
+ )
+ },
+ },
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args: [...args, '--trafficMesh'] }, async (server) => {
+ const response = await got(`${server.url}/edge-handler`, {
+ followRedirect: false,
+ })
+
+ t.is(response.statusCode, 301)
+ t.is(response.headers.location, 'https://google.com/')
+ })
+ })
+ })
+
+ test(testName('routing-local-proxy builds projects w/o edge handlers', args), async (t) => {
+ await withSiteBuilder('site-with-fully-qualified-redirect-rule', async (builder) => {
+ const publicDir = 'public'
+ builder
+ .withNetlifyToml({
+ config: {
+ build: { publish: publicDir },
+ },
+ })
+ .withContentFiles([
+ {
+ path: path.join(publicDir, 'index.html'),
+ content: 'index',
+ },
+ ])
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args: [...args, '--edgeHandlers'] }, async (server) => {
+ const response = await got(`${server.url}/index.html`)
+
+ t.is(response.statusCode, 200)
+ })
+ })
+ })
+
+ test(testName('redirect with country cookie', args), async (t) => {
+ await withSiteBuilder('site-with-country-cookie', async (builder) => {
+ builder
+ .withContentFiles([
+ {
+ path: 'index.html',
+ content: 'index',
+ },
+ {
+ path: 'index-es.html',
+ content: 'index in spanish',
+ },
+ ])
+ .withRedirectsFile({
+ redirects: [{ from: `/`, to: `/index-es.html`, status: '200!', condition: 'Country=ES' }],
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const response = await got(`${server.url}/`, {
+ headers: {
+ cookie: `nf_country=ES`,
+ },
+ })
+ t.is(response.statusCode, 200)
+ t.is(response.body, 'index in spanish')
+ })
+ })
+ })
+
+ test(testName(`doesn't hang when sending a application/json POST request to function server`, args), async (t) => {
+ await withSiteBuilder('site-with-functions', async (builder) => {
+ const functionsPort = 6666
+ await builder
+ .withNetlifyToml({ config: { functions: { directory: 'functions' }, dev: { functionsPort } } })
+ .buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async ({ port, url }) => {
+ const response = await got(`${url.replace(port, functionsPort)}/test`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: '{}',
+ throwHttpErrors: false,
+ })
+ t.is(response.statusCode, 404)
+ t.is(response.body, 'Function not found...')
+ })
+ })
+ })
+
+ test(testName(`catches invalid function names`, args), async (t) => {
+ await withSiteBuilder('site-with-functions', async (builder) => {
+ const functionsPort = 6667
+ await builder
+ .withNetlifyToml({ config: { functions: { directory: 'functions' }, dev: { functionsPort } } })
+ .withFunction({
+ path: 'exclamat!on.js',
+ handler: async (event) => ({
+ statusCode: 200,
+ body: JSON.stringify(event),
+ }),
+ })
+ .buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async ({ port, url }) => {
+ const response = await got(`${url.replace(port, functionsPort)}/exclamat!on`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: '{}',
+ throwHttpErrors: false,
+ })
+ t.is(response.statusCode, 400)
+ t.is(response.body, 'Function name should consist only of alphanumeric characters, hyphen & underscores.')
+ })
+ })
+ })
+})
+/* eslint-enable require-await */
diff --git a/tests/command.build.test.js b/tests/integration/110.command.build.test.js
similarity index 100%
rename from tests/command.build.test.js
rename to tests/integration/110.command.build.test.js
diff --git a/tests/command.status.test.js b/tests/integration/120.command.status.test.js
similarity index 100%
rename from tests/command.status.test.js
rename to tests/integration/120.command.status.test.js
diff --git a/tests/eleventy.test.js b/tests/integration/130.eleventy.test.js
similarity index 100%
rename from tests/eleventy.test.js
rename to tests/integration/130.eleventy.test.js
diff --git a/tests/command.functions.test.js b/tests/integration/20.command.functions.test.js
similarity index 98%
rename from tests/command.functions.test.js
rename to tests/integration/20.command.functions.test.js
index a740eaa4e88..23638ed48f2 100644
--- a/tests/command.functions.test.js
+++ b/tests/integration/20.command.functions.test.js
@@ -1,11 +1,13 @@
// Handlers are meant to be async outside tests
/* eslint-disable require-await */
-const test = require('ava')
+// eslint-disable-next-line ava/use-test
+const avaTest = require('ava')
+const { isCI } = require('ci-info')
const execa = require('execa')
const getPort = require('get-port')
const waitPort = require('wait-port')
-const fs = require('../src/lib/fs')
+const fs = require('../../src/lib/fs')
const callCli = require('./utils/call-cli')
const cliPath = require('./utils/cli-path')
@@ -17,6 +19,8 @@ const { pause } = require('./utils/pause')
const { killProcess } = require('./utils/process')
const { withSiteBuilder } = require('./utils/site-builder')
+const test = isCI ? avaTest.serial.bind(avaTest) : avaTest
+
test('should return function response when invoked with no identity argument', async (t) => {
await withSiteBuilder('function-invoke-with-no-identity-argument', async (builder) => {
builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({
@@ -492,7 +496,7 @@ const withFunctionsServer = async ({ builder, args = [], port = DEFAULT_PORT },
}
}
-test.skip('should serve functions on default port', async (t) => {
+test('should serve functions on default port', async (t) => {
await withSiteBuilder('site-with-ping-function', async (builder) => {
await builder
.withNetlifyToml({ config: { functions: { directory: 'functions' } } })
@@ -512,7 +516,7 @@ test.skip('should serve functions on default port', async (t) => {
})
})
-test.skip('should serve functions on custom port', async (t) => {
+test('should serve functions on custom port', async (t) => {
await withSiteBuilder('site-with-ping-function', async (builder) => {
await builder
.withNetlifyToml({ config: { functions: { directory: 'functions' } } })
@@ -533,7 +537,7 @@ test.skip('should serve functions on custom port', async (t) => {
})
})
-test.skip('should use settings from netlify.toml dev', async (t) => {
+test('should use settings from netlify.toml dev', async (t) => {
await withSiteBuilder('site-with-ping-function', async (builder) => {
const port = await getPort()
await builder
diff --git a/tests/integration/200.command.dev.test.js b/tests/integration/200.command.dev.test.js
new file mode 100644
index 00000000000..fd884f03eb5
--- /dev/null
+++ b/tests/integration/200.command.dev.test.js
@@ -0,0 +1,414 @@
+// Handlers are meant to be async outside tests
+/* eslint-disable require-await */
+const { copyFile } = require('fs').promises
+const os = require('os')
+const path = require('path')
+
+// eslint-disable-next-line ava/use-test
+const avaTest = require('ava')
+const { isCI } = require('ci-info')
+
+const { curl } = require('./utils/curl')
+const { withDevServer } = require('./utils/dev-server')
+const got = require('./utils/got')
+const { withMockApi } = require('./utils/mock-api')
+const { withSiteBuilder } = require('./utils/site-builder')
+
+const test = isCI ? avaTest.serial.bind(avaTest) : avaTest
+
+const testMatrix = [
+ { args: [] },
+
+ // some tests are still failing with this enabled
+ // { args: ['--edgeHandlers'] }
+]
+
+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) => {
+ await withSiteBuilder('site-with-query-redirects', async (builder) => {
+ await builder
+ .withContentFile({
+ path: 'public/index.html',
+ content: 'home',
+ })
+ .withNetlifyToml({
+ config: {
+ build: { publish: 'public' },
+ functions: { directory: 'functions' },
+ },
+ })
+ .withRedirectsFile({
+ redirects: [
+ { from: `/api/*`, to: `/.netlify/functions/echo?a=1&a=2`, status: '200' },
+ { from: `/foo`, to: `/`, status: '302' },
+ { from: `/bar`, to: `/?a=1&a=2`, status: '302' },
+ { from: `/test id=:id`, to: `/?param=:id` },
+ ],
+ })
+ .withFunction({
+ path: 'echo.js',
+ handler: async (event) => ({
+ statusCode: 200,
+ body: JSON.stringify(event),
+ }),
+ })
+ .buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const [fromFunction, queryPassthrough, queryInRedirect, withParamMatching] = 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 }),
+ ])
+
+ // query params should be taken from the request
+ t.deepEqual(fromFunction.multiValueQueryStringParameters, { foo: ['1', '2'], bar: ['1', '2'] })
+
+ // query params should be passed through from the request
+ t.is(queryPassthrough.headers.location, '/?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')
+
+ // query params should be taken from the redirect rule
+ t.is(withParamMatching.headers.location, '/?param=1')
+ })
+ })
+ })
+
+ test(testName('Should not use the ZISI function bundler if not using esbuild', args), 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'),
+ content: `
+export async function handler(event, context) {
+ return {
+ statusCode: 200,
+ body: 'esm',
+ };
+}
+ `,
+ })
+
+ await builder.buildAsync()
+
+ await t.throwsAsync(() =>
+ withDevServer({ cwd: builder.directory, args }, async (server) =>
+ got(`${server.url}/.netlify/functions/esm-function`).text(),
+ ),
+ )
+ })
+ })
+
+ test(testName('Should use the ZISI function bundler and serve ESM functions if using esbuild', args), async (t) => {
+ await withSiteBuilder('site-with-esm-function', async (builder) => {
+ builder
+ .withNetlifyToml({ config: { functions: { directory: 'functions', node_bundler: 'esbuild' } } })
+ .withContentFile({
+ path: path.join('functions', 'esm-function', 'esm-function.js'),
+ content: `
+export async function handler(event, context) {
+ return {
+ statusCode: 200,
+ body: 'esm',
+ };
+}
+ `,
+ })
+
+ 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')
+ })
+ })
+ })
+
+ 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: `
+type CustomResponse = string;
+
+export const handler = async function () {
+ const response: CustomResponse = "ts";
+
+ return {
+ statusCode: 200,
+ body: response,
+ };
+};
+
+ `,
+ })
+
+ 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')
+ })
+ })
+ },
+ )
+
+ 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: `
+type CustomResponse = string;
+
+export const handler = async function () {
+ const response: CustomResponse = "ts";
+
+ return {
+ statusCode: 200,
+ body: response,
+ };
+};
+
+ `,
+ })
+
+ 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')
+ })
+ })
+ },
+ )
+
+ test(testName(`should start https server when https dev block is configured`, args), async (t) => {
+ await withSiteBuilder('sites-with-https-certificate', async (builder) => {
+ await builder
+ .withNetlifyToml({
+ config: {
+ build: { publish: 'public' },
+ functions: { directory: 'functions' },
+ dev: { https: { certFile: 'cert.pem', keyFile: 'key.pem' } },
+ },
+ })
+ .withContentFile({
+ path: 'public/index.html',
+ content: 'index',
+ })
+ .withRedirectsFile({
+ redirects: [{ from: `/api/*`, to: `/.netlify/functions/:splat`, status: '200' }],
+ })
+ .withFunction({
+ path: 'hello.js',
+ handler: async () => ({
+ statusCode: 200,
+ body: 'Hello World',
+ }),
+ })
+ .buildAsync()
+
+ await Promise.all([
+ copyFile(`${__dirname}/assets/cert.pem`, `${builder.directory}/cert.pem`),
+ copyFile(`${__dirname}/assets/key.pem`, `${builder.directory}/key.pem`),
+ ])
+ await withDevServer({ cwd: builder.directory, args }, async ({ port }) => {
+ const options = { https: { rejectUnauthorized: false } }
+ t.is(await got(`https://localhost:${port}`, options).text(), 'index')
+ t.is(await got(`https://localhost:${port}/api/hello`, options).text(), 'Hello World')
+ })
+ })
+ })
+
+ test(testName(`should use custom functions timeouts`, args), async (t) => {
+ await withSiteBuilder('site-with-custom-functions-timeout', async (builder) => {
+ await builder
+ .withNetlifyToml({
+ config: {
+ build: { publish: 'public' },
+ functions: { directory: 'functions' },
+ },
+ })
+ .withFunction({
+ path: 'hello.js',
+ handler: async () => {
+ await new Promise((resolve) => {
+ const SLEEP_TIME = 2000
+ setTimeout(resolve, SLEEP_TIME)
+ })
+ return {
+ statusCode: 200,
+ body: 'Hello World',
+ }
+ },
+ })
+ .buildAsync()
+
+ const siteInfo = {
+ account_slug: 'test-account',
+ id: 'site_id',
+ name: 'site-name',
+ functions_config: { timeout: 1 },
+ }
+
+ const routes = [
+ { path: 'sites/site_id', response: siteInfo },
+
+ { path: 'sites/site_id/service-instances', response: [] },
+ {
+ path: 'accounts',
+ response: [{ slug: siteInfo.account_slug }],
+ },
+ ]
+
+ await withMockApi(routes, async ({ apiUrl }) => {
+ await withDevServer(
+ {
+ cwd: builder.directory,
+ offline: false,
+ env: {
+ NETLIFY_API_URL: apiUrl,
+ NETLIFY_SITE_ID: 'site_id',
+ NETLIFY_AUTH_TOKEN: 'fake-token',
+ },
+ },
+ 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'))
+ },
+ )
+ })
+ })
+ })
+
+ // 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(testName(`serves non ascii static files correctly`, args), async (t) => {
+ await withSiteBuilder('site-with-non-ascii-files', async (builder) => {
+ await builder
+ .withContentFile({
+ path: 'public/范.txt',
+ content: 'success',
+ })
+ .withNetlifyToml({
+ config: {
+ build: { publish: 'public' },
+ redirects: [{ from: '/*', to: '/index.html', status: 200 }],
+ },
+ })
+ .buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const response = await got(`${server.url}/${encodeURIComponent('范.txt')}`)
+ t.is(response.body, 'success')
+ })
+ })
+ })
+
+ test(testName(`returns headers set by function`, args), async (t) => {
+ await withSiteBuilder('site-with-function-with-custom-headers', async (builder) => {
+ await builder
+ .withFunction({
+ pathPrefix: 'netlify/functions',
+ path: 'custom-headers.js',
+ handler: async () => ({
+ statusCode: 200,
+ body: '',
+ headers: { 'single-value-header': 'custom-value' },
+ multiValueHeaders: { 'multi-value-header': ['custom-value1', 'custom-value2'] },
+ metadata: { builder_function: true },
+ }),
+ })
+ .buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const response = await got(`${server.url}/.netlify/functions/custom-headers`)
+ 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.is(builderResponse.headers['single-value-header'], 'custom-value')
+ t.is(builderResponse.headers['multi-value-header'], 'custom-value1, custom-value2')
+ })
+ })
+ })
+
+ test(testName('should match redirect when path is URL encoded', args), async (t) => {
+ await withSiteBuilder('site-with-encoded-redirect', async (builder) => {
+ await builder
+ .withContentFile({ path: 'static/special[test].txt', content: `special` })
+ .withRedirectsFile({ redirects: [{ from: '/_next/static/*', to: '/static/:splat', status: 200 }] })
+ .buildAsync()
+
+ 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(),
+ ])
+ t.is(response1, 'special')
+ t.is(response2, 'special')
+ })
+ })
+ })
+
+ test(testName(`should not redirect POST request to functions server when it doesn't exists`, args), 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',
+ }),
+ )
+
+ t.is(error.message, 'Response code 405 (Method Not Allowed)')
+ })
+ })
+ })
+})
+/* eslint-enable require-await */
diff --git a/tests/command.deploy.test.js b/tests/integration/210.command.deploy.test.js
similarity index 99%
rename from tests/command.deploy.test.js
rename to tests/integration/210.command.deploy.test.js
index 99c5fb4eb58..2461835e123 100644
--- a/tests/command.deploy.test.js
+++ b/tests/integration/210.command.deploy.test.js
@@ -5,8 +5,8 @@ const process = require('process')
const test = require('ava')
const omit = require('omit.js').default
-const { supportsEdgeHandlers } = require('../src/lib/account')
-const { getToken } = require('../src/utils/command-helpers')
+const { supportsEdgeHandlers } = require('../../src/lib/account')
+const { getToken } = require('../../src/utils/command-helpers')
const callCli = require('./utils/call-cli')
const { createLiveTestSite, generateSiteName } = require('./utils/create-live-test-site')
diff --git a/tests/command.graph.test.js b/tests/integration/220.command.graph.test.js
similarity index 100%
rename from tests/command.graph.test.js
rename to tests/integration/220.command.graph.test.js
diff --git a/tests/rules-proxy.test.js b/tests/integration/230.rules-proxy.test.js
similarity index 84%
rename from tests/rules-proxy.test.js
rename to tests/integration/230.rules-proxy.test.js
index aec23643fcc..1b66b9dbfe6 100644
--- a/tests/rules-proxy.test.js
+++ b/tests/integration/230.rules-proxy.test.js
@@ -4,7 +4,7 @@ const path = require('path')
const test = require('ava')
const getPort = require('get-port')
-const { createRewriter } = require('../src/utils/rules-proxy')
+const { createRewriter, getWatchers } = require('../../src/utils/rules-proxy')
const got = require('./utils/got')
const { createSiteBuilder } = require('./utils/site-builder')
@@ -34,7 +34,9 @@ test.before(async (t) => {
t.context.server = server
t.context.builder = builder
- return server.listen(port)
+ await new Promise((resolve) => {
+ server.listen(port, 'localhost', resolve)
+ })
})
const PORT = 8888
@@ -44,8 +46,8 @@ test.after(async (t) => {
t.context.server.on('close', resolve)
t.context.server.close()
})
- // TODO: check why this line breaks the rewriter on windows
- // await t.context.builder.cleanupAsync()
+ await Promise.all(getWatchers().map((watcher) => watcher.close()))
+ await t.context.builder.cleanupAsync()
})
test('should apply re-write rule based on _redirects file', async (t) => {
diff --git a/tests/telemetry.test.js b/tests/integration/240.telemetry.test.js
similarity index 96%
rename from tests/telemetry.test.js
rename to tests/integration/240.telemetry.test.js
index f3243066343..e75268bf10f 100644
--- a/tests/telemetry.test.js
+++ b/tests/integration/240.telemetry.test.js
@@ -3,7 +3,7 @@ const process = require('process')
const test = require('ava')
const { version: uuidVersion } = require('uuid')
-const { name, version } = require('../package.json')
+const { name, version } = require('../../package.json')
const callCli = require('./utils/call-cli')
const { withMockApi } = require('./utils/mock-api')
diff --git a/tests/command.lm.test.js b/tests/integration/30.command.lm.test.js
similarity index 98%
rename from tests/command.lm.test.js
rename to tests/integration/30.command.lm.test.js
index 670b0398e21..67be02f8484 100644
--- a/tests/command.lm.test.js
+++ b/tests/integration/30.command.lm.test.js
@@ -6,7 +6,7 @@ const test = require('ava')
const execa = require('execa')
const ini = require('ini')
-const { getPathInHome } = require('../src/lib/settings')
+const { getPathInHome } = require('../../src/lib/settings')
const callCli = require('./utils/call-cli')
const { getCLIOptions, startMockApi } = require('./utils/mock-api')
diff --git a/tests/integration/300.command.dev.test.js b/tests/integration/300.command.dev.test.js
new file mode 100644
index 00000000000..7ed8ae86f7c
--- /dev/null
+++ b/tests/integration/300.command.dev.test.js
@@ -0,0 +1,262 @@
+// Handlers are meant to be async outside tests
+/* eslint-disable require-await */
+const path = require('path')
+const process = require('process')
+
+// eslint-disable-next-line ava/use-test
+const avaTest = require('ava')
+const { isCI } = require('ci-info')
+
+const { withDevServer } = require('./utils/dev-server')
+const got = require('./utils/got')
+const { withSiteBuilder } = require('./utils/site-builder')
+
+const test = isCI ? avaTest.serial.bind(avaTest) : avaTest
+
+const testMatrix = [
+ { args: [] },
+
+ // some tests are still failing with this enabled
+ // { args: ['--edgeHandlers'] }
+]
+
+const testName = (title, args) => (args.length <= 0 ? title : `${title} - ${args.join(' ')}`)
+
+testMatrix.forEach(({ args }) => {
+ test(testName('should return index file when / is accessed', args), async (t) => {
+ await withSiteBuilder('site-with-index-file', async (builder) => {
+ builder.withContentFile({
+ path: 'index.html',
+ content: '⊂◉‿◉つ
',
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const response = await got(server.url).text()
+ t.is(response, '⊂◉‿◉つ
')
+ })
+ })
+ })
+
+ test(testName('should return user defined headers when / is accessed', args), async (t) => {
+ await withSiteBuilder('site-with-headers-on-root', async (builder) => {
+ builder.withContentFile({
+ path: 'index.html',
+ content: '⊂◉‿◉つ
',
+ })
+
+ const headerName = 'X-Frame-Options'
+ const headerValue = 'SAMEORIGIN'
+ builder.withHeadersFile({ headers: [{ path: '/*', headers: [`${headerName}: ${headerValue}`] }] })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const { headers } = await got(server.url)
+ t.is(headers[headerName.toLowerCase()], headerValue)
+ })
+ })
+ })
+
+ test(testName('should return user defined headers when non-root path is accessed', args), async (t) => {
+ await withSiteBuilder('site-with-headers-on-non-root', async (builder) => {
+ builder.withContentFile({
+ path: 'foo/index.html',
+ content: '⊂◉‿◉つ
',
+ })
+
+ const headerName = 'X-Frame-Options'
+ const headerValue = 'SAMEORIGIN'
+ builder.withHeadersFile({ headers: [{ path: '/*', headers: [`${headerName}: ${headerValue}`] }] })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const { headers } = await got(`${server.url}/foo`)
+ t.is(headers[headerName.toLowerCase()], headerValue)
+ })
+ })
+ })
+
+ test(testName('should return response from a function with setTimeout', args), async (t) => {
+ await withSiteBuilder('site-with-set-timeout-function', async (builder) => {
+ builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({
+ path: 'timeout.js',
+ handler: async () => {
+ console.log('ding')
+ // Wait for 4 seconds
+ const FUNCTION_TIMEOUT = 4e3
+ await new Promise((resolve) => {
+ setTimeout(resolve, FUNCTION_TIMEOUT)
+ })
+ return {
+ statusCode: 200,
+ body: 'ping',
+ metadata: { builder_function: true },
+ }
+ },
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const response = await got(`${server.url}/.netlify/functions/timeout`).text()
+ t.is(response, 'ping')
+ const builderResponse = await got(`${server.url}/.netlify/builders/timeout`).text()
+ t.is(builderResponse, 'ping')
+ })
+ })
+ })
+
+ test(testName('should fail when no metadata is set for builder function', args), async (t) => {
+ await withSiteBuilder('site-with-misconfigured-builder-function', async (builder) => {
+ builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({
+ path: 'builder.js',
+ handler: async () => ({
+ statusCode: 200,
+ body: 'ping',
+ }),
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const response = await got(`${server.url}/.netlify/functions/builder`)
+ t.is(response.body, 'ping')
+ t.is(response.statusCode, 200)
+ const builderResponse = await got(`${server.url}/.netlify/builders/builder`, {
+ throwHttpErrors: false,
+ })
+ t.is(
+ builderResponse.body,
+ `{"message":"Function is not an on-demand builder. See https://ntl.fyi/create-builder for how to convert a function to a builder."}`,
+ )
+ t.is(builderResponse.statusCode, 400)
+ })
+ })
+ })
+
+ test(testName('should serve function from a subdirectory', args), async (t) => {
+ await withSiteBuilder('site-with-from-subdirectory', async (builder) => {
+ builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({
+ path: path.join('echo', 'echo.js'),
+ handler: async () => ({
+ statusCode: 200,
+ body: 'ping',
+ metadata: { builder_function: true },
+ }),
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const response = await got(`${server.url}/.netlify/functions/echo`).text()
+ t.is(response, 'ping')
+ const builderResponse = await got(`${server.url}/.netlify/builders/echo`).text()
+ t.is(builderResponse, 'ping')
+ })
+ })
+ })
+
+ test(testName('should pass .env.development vars to function', args), async (t) => {
+ await withSiteBuilder('site-with-env-development', async (builder) => {
+ builder
+ .withNetlifyToml({ config: { functions: { directory: 'functions' } } })
+ .withEnvFile({ path: '.env.development', env: { TEST: 'FROM_DEV_FILE' } })
+ .withFunction({
+ path: 'env.js',
+ handler: async () => ({
+ statusCode: 200,
+ body: `${process.env.TEST}`,
+ metadata: { builder_function: true },
+ }),
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const response = await got(`${server.url}/.netlify/functions/env`).text()
+ t.is(response, 'FROM_DEV_FILE')
+ const builderResponse = await got(`${server.url}/.netlify/builders/env`).text()
+ t.is(builderResponse, 'FROM_DEV_FILE')
+ })
+ })
+ })
+
+ test(testName('should pass process env vars to function', args), async (t) => {
+ await withSiteBuilder('site-with-process-env', async (builder) => {
+ builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({
+ path: 'env.js',
+ handler: async () => ({
+ statusCode: 200,
+ body: `${process.env.TEST}`,
+ metadata: { builder_function: true },
+ }),
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, env: { TEST: 'FROM_PROCESS_ENV' }, args }, async (server) => {
+ const response = await got(`${server.url}/.netlify/functions/env`).text()
+ t.is(response, 'FROM_PROCESS_ENV')
+ const builderResponse = await got(`${server.url}/.netlify/builders/env`).text()
+ t.is(builderResponse, 'FROM_PROCESS_ENV')
+ })
+ })
+ })
+
+ test(testName('should pass [build.environment] env vars to function', args), async (t) => {
+ await withSiteBuilder('site-with-build-environment', async (builder) => {
+ builder
+ .withNetlifyToml({
+ config: { build: { environment: { TEST: 'FROM_CONFIG_FILE' } }, functions: { directory: 'functions' } },
+ })
+ .withFunction({
+ path: 'env.js',
+ handler: async () => ({
+ statusCode: 200,
+ body: `${process.env.TEST}`,
+ metadata: { builder_function: true },
+ }),
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const response = await got(`${server.url}/.netlify/functions/env`).text()
+ t.is(response, 'FROM_CONFIG_FILE')
+ const builderResponse = await got(`${server.url}/.netlify/builders/env`).text()
+ t.is(builderResponse, 'FROM_CONFIG_FILE')
+ })
+ })
+ })
+
+ test(testName('[context.dev.environment] should override [build.environment]', args), async (t) => {
+ await withSiteBuilder('site-with-build-environment', async (builder) => {
+ builder
+ .withNetlifyToml({
+ config: {
+ build: { environment: { TEST: 'DEFAULT_CONTEXT' } },
+ context: { dev: { environment: { TEST: 'DEV_CONTEXT' } } },
+ functions: { directory: 'functions' },
+ },
+ })
+ .withFunction({
+ path: 'env.js',
+ handler: async () => ({
+ statusCode: 200,
+ body: `${process.env.TEST}`,
+ }),
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const response = await got(`${server.url}/.netlify/functions/env`).text()
+ t.is(response, 'DEV_CONTEXT')
+ })
+ })
+ })
+})
+/* eslint-enable require-await */
diff --git a/tests/command.dev.exec.test.js b/tests/integration/310.command.dev.exec.test.js
similarity index 100%
rename from tests/command.dev.exec.test.js
rename to tests/integration/310.command.dev.exec.test.js
diff --git a/tests/command.help.test.js b/tests/integration/320.command.help.test.js
similarity index 100%
rename from tests/command.help.test.js
rename to tests/integration/320.command.help.test.js
diff --git a/tests/serving-functions.test.js b/tests/integration/330.serving-functions.test.js
similarity index 98%
rename from tests/serving-functions.test.js
rename to tests/integration/330.serving-functions.test.js
index 5618aaf7abf..8431317fd0b 100644
--- a/tests/serving-functions.test.js
+++ b/tests/integration/330.serving-functions.test.js
@@ -1,7 +1,9 @@
/* eslint-disable require-await */
const { join } = require('path')
-const test = require('ava')
+// eslint-disable-next-line ava/use-test
+const avaTest = require('ava')
+const { isCI } = require('ci-info')
const pWaitFor = require('p-wait-for')
const { tryAndLogOutput, withDevServer } = require('./utils/dev-server')
@@ -16,6 +18,8 @@ const WAIT_INTERVAL = 1800
const WAIT_TIMEOUT = 30_000
const WAIT_WRITE = 3000
+const test = isCI ? avaTest.serial.bind(avaTest) : avaTest
+
const gotCatch404 = async (url, options) => {
try {
return await got(url, options)
@@ -83,7 +87,7 @@ testMatrix.forEach(({ args }) => {
})
})
- test.skip(testName('Updates a TypeScript function when its main file is modified', args), async (t) => {
+ test(testName('Updates a TypeScript function when its main file is modified', args), async (t) => {
await withSiteBuilder('ts-function-update-main-file', async (builder) => {
const bundlerConfig = args.includes('esbuild') ? { node_bundler: 'esbuild' } : {}
@@ -359,7 +363,7 @@ testMatrix.forEach(({ args }) => {
})
})
- test.skip(testName('Adds a new TypeScript function when a function file is created', args), async (t) => {
+ test(testName('Adds a new TypeScript function when a function file is created', args), async (t) => {
await withSiteBuilder('ts-function-create-function-file', async (builder) => {
const bundlerConfig = args.includes('esbuild') ? { node_bundler: 'esbuild' } : {}
diff --git a/tests/integration/400.command.dev.test.js b/tests/integration/400.command.dev.test.js
new file mode 100644
index 00000000000..5063ecba97d
--- /dev/null
+++ b/tests/integration/400.command.dev.test.js
@@ -0,0 +1,662 @@
+// Handlers are meant to be async outside tests
+/* eslint-disable require-await */
+const path = require('path')
+const process = require('process')
+
+// eslint-disable-next-line ava/use-test
+const avaTest = require('ava')
+const { isCI } = require('ci-info')
+const FormData = require('form-data')
+
+const { withDevServer } = require('./utils/dev-server')
+const got = require('./utils/got')
+const { withSiteBuilder } = require('./utils/site-builder')
+
+const test = isCI ? avaTest.serial.bind(avaTest) : avaTest
+
+const testMatrix = [
+ { args: [] },
+
+ // some tests are still failing with this enabled
+ // { args: ['--edgeHandlers'] }
+]
+
+const testName = (title, args) => (args.length <= 0 ? title : `${title} - ${args.join(' ')}`)
+
+testMatrix.forEach(({ args }) => {
+ test(testName('should use [build.environment] and not [context.production.environment]', args), async (t) => {
+ await withSiteBuilder('site-with-build-environment', async (builder) => {
+ builder
+ .withNetlifyToml({
+ config: {
+ build: { environment: { TEST: 'DEFAULT_CONTEXT' } },
+ context: { production: { environment: { TEST: 'PRODUCTION_CONTEXT' } } },
+ functions: { directory: 'functions' },
+ },
+ })
+ .withFunction({
+ path: 'env.js',
+ handler: async () => ({
+ statusCode: 200,
+ body: `${process.env.TEST}`,
+ }),
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const response = await got(`${server.url}/.netlify/functions/env`).text()
+ t.is(response, 'DEFAULT_CONTEXT')
+ })
+ })
+ })
+
+ test(testName('should override .env.development with process env', args), async (t) => {
+ await withSiteBuilder('site-with-override', async (builder) => {
+ builder
+ .withNetlifyToml({ config: { functions: { directory: 'functions' } } })
+ .withEnvFile({ path: '.env.development', env: { TEST: 'FROM_DEV_FILE' } })
+ .withFunction({
+ path: 'env.js',
+ handler: async () => ({
+ statusCode: 200,
+ body: `${process.env.TEST}`,
+ }),
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, env: { TEST: 'FROM_PROCESS_ENV' }, args }, async (server) => {
+ const response = await got(`${server.url}/.netlify/functions/env`).text()
+ t.is(response, 'FROM_PROCESS_ENV')
+ })
+ })
+ })
+
+ test(testName('should override [build.environment] with process env', args), async (t) => {
+ await withSiteBuilder('site-with-build-environment-override', async (builder) => {
+ builder
+ .withNetlifyToml({
+ config: { build: { environment: { TEST: 'FROM_CONFIG_FILE' } }, functions: { directory: 'functions' } },
+ })
+ .withFunction({
+ path: 'env.js',
+ handler: async () => ({
+ statusCode: 200,
+ body: `${process.env.TEST}`,
+ }),
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, env: { TEST: 'FROM_PROCESS_ENV' }, args }, async (server) => {
+ const response = await got(`${server.url}/.netlify/functions/env`).text()
+ t.is(response, 'FROM_PROCESS_ENV')
+ })
+ })
+ })
+
+ test(testName('should override value of the NETLIFY_DEV env variable', args), async (t) => {
+ await withSiteBuilder('site-with-netlify-dev-override', async (builder) => {
+ builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({
+ path: 'env.js',
+ handler: async () => ({
+ statusCode: 200,
+ body: `${process.env.NETLIFY_DEV}`,
+ }),
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer(
+ { cwd: builder.directory, env: { NETLIFY_DEV: 'FROM_PROCESS_ENV' }, args },
+ async (server) => {
+ const response = await got(`${server.url}/.netlify/functions/env`).text()
+ t.is(response, 'true')
+ },
+ )
+ })
+ })
+
+ test(testName('should set value of the CONTEXT env variable', args), async (t) => {
+ await withSiteBuilder('site-with-context-override', async (builder) => {
+ builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({
+ path: 'env.js',
+ handler: async () => ({
+ statusCode: 200,
+ body: `${process.env.CONTEXT}`,
+ }),
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const response = await got(`${server.url}/.netlify/functions/env`).text()
+ t.is(response, 'dev')
+ })
+ })
+ })
+
+ test(testName('should redirect using a wildcard when set in netlify.toml', args), async (t) => {
+ await withSiteBuilder('site-with-redirect-function', async (builder) => {
+ builder
+ .withNetlifyToml({
+ config: {
+ functions: { directory: 'functions' },
+ redirects: [{ from: '/api/*', to: '/.netlify/functions/:splat', status: 200 }],
+ },
+ })
+ .withFunction({
+ path: 'ping.js',
+ handler: async () => ({
+ statusCode: 200,
+ body: 'ping',
+ }),
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const response = await got(`${server.url}/api/ping`).text()
+ t.is(response, 'ping')
+ })
+ })
+ })
+
+ test(testName('should pass undefined body to functions event for GET requests when redirecting', args), async (t) => {
+ await withSiteBuilder('site-with-get-echo-function', async (builder) => {
+ builder
+ .withNetlifyToml({
+ config: {
+ functions: { directory: 'functions' },
+ redirects: [{ from: '/api/*', to: '/.netlify/functions/:splat', status: 200 }],
+ },
+ })
+ .withFunction({
+ path: 'echo.js',
+ handler: async (event) => ({
+ statusCode: 200,
+ body: JSON.stringify(event),
+ }),
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const response = await got(`${server.url}/api/echo?ding=dong`).json()
+ t.is(response.body, undefined)
+ t.is(response.headers.host, `${server.host}:${server.port}`)
+ t.is(response.httpMethod, 'GET')
+ t.is(response.isBase64Encoded, true)
+ t.is(response.path, '/api/echo')
+ t.deepEqual(response.queryStringParameters, { ding: 'dong' })
+ })
+ })
+ })
+
+ test(testName('should pass body to functions event for POST requests when redirecting', args), async (t) => {
+ await withSiteBuilder('site-with-post-echo-function', async (builder) => {
+ builder
+ .withNetlifyToml({
+ config: {
+ functions: { directory: 'functions' },
+ redirects: [{ from: '/api/*', to: '/.netlify/functions/:splat', status: 200 }],
+ },
+ })
+ .withFunction({
+ path: 'echo.js',
+ handler: async (event) => ({
+ statusCode: 200,
+ body: JSON.stringify(event),
+ }),
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const response = await got
+ .post(`${server.url}/api/echo?ding=dong`, {
+ headers: {
+ 'content-type': 'application/x-www-form-urlencoded',
+ },
+ body: 'some=thing',
+ })
+ .json()
+
+ t.is(response.body, 'some=thing')
+ t.is(response.headers.host, `${server.host}:${server.port}`)
+ t.is(response.headers['content-type'], 'application/x-www-form-urlencoded')
+ t.is(response.headers['content-length'], '10')
+ t.is(response.httpMethod, 'POST')
+ t.is(response.isBase64Encoded, false)
+ t.is(response.path, '/api/echo')
+ t.deepEqual(response.queryStringParameters, { ding: 'dong' })
+ })
+ })
+ })
+
+ test(testName('should return an empty body for a function with no body when redirecting', args), async (t) => {
+ await withSiteBuilder('site-with-no-body-function', async (builder) => {
+ builder
+ .withNetlifyToml({
+ config: {
+ functions: { directory: 'functions' },
+ redirects: [{ from: '/api/*', to: '/.netlify/functions/:splat', status: 200 }],
+ },
+ })
+ .withFunction({
+ path: 'echo.js',
+ handler: async () => ({
+ statusCode: 200,
+ }),
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const response = await got.post(`${server.url}/api/echo?ding=dong`, {
+ headers: {
+ 'content-type': 'application/x-www-form-urlencoded',
+ },
+ body: 'some=thing',
+ })
+
+ t.is(response.body, '')
+ t.is(response.statusCode, 200)
+ })
+ })
+ })
+
+ test(testName('should handle multipart form data when redirecting', args), async (t) => {
+ await withSiteBuilder('site-with-multi-part-function', async (builder) => {
+ builder
+ .withNetlifyToml({
+ config: {
+ functions: { directory: 'functions' },
+ redirects: [{ from: '/api/*', to: '/.netlify/functions/:splat', status: 200 }],
+ },
+ })
+ .withFunction({
+ path: 'echo.js',
+ handler: async (event) => ({
+ statusCode: 200,
+ body: JSON.stringify(event),
+ }),
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const form = new FormData()
+ form.append('some', 'thing')
+
+ const expectedBoundary = form.getBoundary()
+ const expectedResponseBody = form.getBuffer().toString('base64')
+
+ const response = await got
+ .post(`${server.url}/api/echo?ding=dong`, {
+ body: form,
+ })
+ .json()
+
+ t.is(response.headers.host, `${server.host}:${server.port}`)
+ t.is(response.headers['content-type'], `multipart/form-data; boundary=${expectedBoundary}`)
+ t.is(response.headers['content-length'], '164')
+ t.is(response.httpMethod, 'POST')
+ t.is(response.isBase64Encoded, true)
+ t.is(response.path, '/api/echo')
+ t.deepEqual(response.queryStringParameters, { ding: 'dong' })
+ t.is(response.body, expectedResponseBody)
+ })
+ })
+ })
+
+ test(testName('should return 404 when redirecting to a non existing function', args), 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, args }, async (server) => {
+ const response = await got
+ .post(`${server.url}/api/none`, {
+ body: 'nothing',
+ })
+ .catch((error) => error.response)
+
+ t.is(response.statusCode, 404)
+ })
+ })
+ })
+
+ test(testName('should parse function query parameters using simple parsing', args), 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, args }, 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(testName('should handle form submission', args), 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, args }, 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)
+
+ t.is(response.headers.host, `${server.host}:${server.port}`)
+ t.is(response.headers['content-length'], '276')
+ 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, {
+ 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: '',
+ },
+ })
+ })
+ })
+ })
+
+ test(testName('should handle form submission with a background function', args), 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, args }, 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(testName('should not handle form submission when content type is `text/plain`', args), 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, args }, 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.body, 'Method Not Allowed')
+ })
+ })
+ })
+
+ test(testName('should return existing local file even when rewrite matches when force=false', args), 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, args }, async (server) => {
+ const response = await got(`${server.url}/foo?ping=pong`).text()
+ t.is(response, 'foo')
+ })
+ })
+ })
+
+ test(testName('should return existing local file even when redirect matches when force=false', args), 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, args }, async (server) => {
+ const response = await got(`${server.url}/foo?ping=pong`).text()
+ t.is(response, 'foo')
+ })
+ })
+ })
+
+ test(testName('should ignore existing local file when redirect matches and force=true', args), 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: 200, force: true }],
+ },
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const response = await got(`${server.url}/foo`).text()
+ t.is(response, 'not-foo')
+ })
+ })
+ })
+
+ test(testName('should use existing file when rule contains file extension and force=false', args), 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: 200, force: false }],
+ },
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const response = await got(`${server.url}/foo.html`).text()
+ t.is(response, 'foo')
+ })
+ })
+ })
+
+ test(testName('should redirect when rule contains file extension and force=true', args), 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: 200, force: true }],
+ },
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const response = await got(`${server.url}/foo.html`).text()
+ t.is(response, 'not-foo')
+ })
+ })
+ })
+
+ test(testName('should redirect from sub directory to root directory', args), 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, args }, 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')
+ })
+ })
+ })
+})
+/* eslint-enable require-await */
diff --git a/tests/command.dev.trace.test.js b/tests/integration/410.command.dev.trace.test.js
similarity index 100%
rename from tests/command.dev.trace.test.js
rename to tests/integration/410.command.dev.trace.test.js
diff --git a/tests/command.init.test.js b/tests/integration/420.command.init.test.js
similarity index 100%
rename from tests/command.init.test.js
rename to tests/integration/420.command.init.test.js
diff --git a/tests/integration/500.command.dev.test.js b/tests/integration/500.command.dev.test.js
new file mode 100644
index 00000000000..c26810b24e8
--- /dev/null
+++ b/tests/integration/500.command.dev.test.js
@@ -0,0 +1,374 @@
+// Handlers are meant to be async outside tests
+/* eslint-disable require-await */
+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 { withDevServer } = require('./utils/dev-server')
+const got = require('./utils/got')
+const { withSiteBuilder } = require('./utils/site-builder')
+
+const test = isCI ? avaTest.serial.bind(avaTest) : avaTest
+
+const testMatrix = [
+ { args: [] },
+
+ // some tests are still failing with this enabled
+ // { args: ['--edgeHandlers'] }
+]
+
+const testName = (title, args) => (args.length <= 0 ? title : `${title} - ${args.join(' ')}`)
+
+testMatrix.forEach(({ args }) => {
+ test(testName('should return 404 when redirecting to a non existing function', args), 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, args }, async (server) => {
+ const response = await got
+ .post(`${server.url}/api/none`, {
+ body: 'nothing',
+ })
+ .catch((error) => error.response)
+
+ t.is(response.statusCode, 404)
+ })
+ })
+ })
+
+ test(testName('should parse function query parameters using simple parsing', args), 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, args }, 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(testName('should handle form submission', args), 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, args }, 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)
+
+ t.is(response.headers.host, `${server.host}:${server.port}`)
+ t.is(response.headers['content-length'], '276')
+ 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, {
+ 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: '',
+ },
+ })
+ })
+ })
+ })
+
+ test(testName('should handle form submission with a background function', args), 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, args }, 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(testName('should not handle form submission when content type is `text/plain`', args), 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, args }, 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.body, 'Method Not Allowed')
+ })
+ })
+ })
+
+ test(testName('should return existing local file even when rewrite matches when force=false', args), 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, args }, async (server) => {
+ const response = await got(`${server.url}/foo?ping=pong`).text()
+ t.is(response, 'foo')
+ })
+ })
+ })
+
+ test(testName('should return existing local file even when redirect matches when force=false', args), 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, args }, async (server) => {
+ const response = await got(`${server.url}/foo?ping=pong`).text()
+ t.is(response, 'foo')
+ })
+ })
+ })
+
+ test(testName('should ignore existing local file when redirect matches and force=true', args), 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: 200, force: true }],
+ },
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const response = await got(`${server.url}/foo`).text()
+ t.is(response, 'not-foo')
+ })
+ })
+ })
+
+ test(testName('should use existing file when rule contains file extension and force=false', args), 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: 200, force: false }],
+ },
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const response = await got(`${server.url}/foo.html`).text()
+ t.is(response, 'foo')
+ })
+ })
+ })
+
+ test(testName('should redirect when rule contains file extension and force=true', args), 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: 200, force: true }],
+ },
+ })
+
+ await builder.buildAsync()
+
+ await withDevServer({ cwd: builder.directory, args }, async (server) => {
+ const response = await got(`${server.url}/foo.html`).text()
+ t.is(response, 'not-foo')
+ })
+ })
+ })
+
+ test(testName('should redirect from sub directory to root directory', args), 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, args }, 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')
+ })
+ })
+ })
+})
+/* eslint-enable require-await */
diff --git a/tests/hugo.test.js b/tests/integration/510.hugo.test.js
similarity index 100%
rename from tests/hugo.test.js
rename to tests/integration/510.hugo.test.js
diff --git a/tests/command.link.test.js b/tests/integration/520.command.link.test.js
similarity index 96%
rename from tests/command.link.test.js
rename to tests/integration/520.command.link.test.js
index 3bb43d9d590..76a7ad47fe8 100644
--- a/tests/command.link.test.js
+++ b/tests/integration/520.command.link.test.js
@@ -2,7 +2,7 @@ const process = require('process')
const test = require('ava')
-const { isFileAsync } = require('../src/lib/fs')
+const { isFileAsync } = require('../../src/lib/fs')
const callCli = require('./utils/call-cli')
const { getCLIOptions, withMockApi } = require('./utils/mock-api')
diff --git a/tests/graph-codegen.test.js b/tests/integration/530.graph-codegen.test.js
similarity index 98%
rename from tests/graph-codegen.test.js
rename to tests/integration/530.graph-codegen.test.js
index 06d004645dd..e14e8e80806 100644
--- a/tests/graph-codegen.test.js
+++ b/tests/integration/530.graph-codegen.test.js
@@ -9,7 +9,7 @@ const {
generateFunctionsSource,
generateHandlerSource,
parse,
-} = require('../src/lib/one-graph/cli-netlify-graph')
+} = require('../../src/lib/one-graph/cli-netlify-graph')
const { normalize } = require('./utils/snapshots')
diff --git a/tests/framework-detection.test.js b/tests/integration/600.framework-detection.test.js
similarity index 98%
rename from tests/framework-detection.test.js
rename to tests/integration/600.framework-detection.test.js
index a270009cded..0f433a2fd1d 100644
--- a/tests/framework-detection.test.js
+++ b/tests/integration/600.framework-detection.test.js
@@ -1,4 +1,6 @@
-const test = require('ava')
+// eslint-disable-next-line ava/use-test
+const avaTest = require('ava')
+const { isCI } = require('ci-info')
const execa = require('execa')
const cliPath = require('./utils/cli-path')
@@ -10,6 +12,8 @@ const { normalize } = require('./utils/snapshots')
const content = 'Hello World!'
+const test = isCI ? avaTest.serial.bind(avaTest) : avaTest
+
test('should default to process.cwd() and static server', async (t) => {
await withSiteBuilder('site-with-index-file', async (builder) => {
await builder
diff --git a/tests/command.env.test.js b/tests/integration/610.command.env.test.js
similarity index 100%
rename from tests/command.env.test.js
rename to tests/integration/610.command.env.test.js
diff --git a/tests/serving-functions-rust.test.js b/tests/integration/620.serving-functions-rust.test.js
similarity index 100%
rename from tests/serving-functions-rust.test.js
rename to tests/integration/620.serving-functions-rust.test.js
diff --git a/tests/serving-functions-go.test.js b/tests/integration/630.serving-functions-go.test.js
similarity index 100%
rename from tests/serving-functions-go.test.js
rename to tests/integration/630.serving-functions-go.test.js
diff --git a/tests/assets/bundled-function-1.zip b/tests/integration/assets/bundled-function-1.zip
similarity index 100%
rename from tests/assets/bundled-function-1.zip
rename to tests/integration/assets/bundled-function-1.zip
diff --git a/tests/assets/cert.pem b/tests/integration/assets/cert.pem
similarity index 100%
rename from tests/assets/cert.pem
rename to tests/integration/assets/cert.pem
diff --git a/tests/assets/key.pem b/tests/integration/assets/key.pem
similarity index 100%
rename from tests/assets/key.pem
rename to tests/integration/assets/key.pem
diff --git a/tests/assets/netlifyGraphOperationsLibrary.graphql b/tests/integration/assets/netlifyGraphOperationsLibrary.graphql
similarity index 100%
rename from tests/assets/netlifyGraphOperationsLibrary.graphql
rename to tests/integration/assets/netlifyGraphOperationsLibrary.graphql
diff --git a/tests/assets/netlifyGraphSchema.graphql b/tests/integration/assets/netlifyGraphSchema.graphql
similarity index 100%
rename from tests/assets/netlifyGraphSchema.graphql
rename to tests/integration/assets/netlifyGraphSchema.graphql
diff --git a/tests/eleventy-site/.eleventy.js b/tests/integration/eleventy-site/.eleventy.js
similarity index 100%
rename from tests/eleventy-site/.eleventy.js
rename to tests/integration/eleventy-site/.eleventy.js
diff --git a/tests/eleventy-site/.gitignore b/tests/integration/eleventy-site/.gitignore
similarity index 100%
rename from tests/eleventy-site/.gitignore
rename to tests/integration/eleventy-site/.gitignore
diff --git a/tests/eleventy-site/_redirects b/tests/integration/eleventy-site/_redirects
similarity index 100%
rename from tests/eleventy-site/_redirects
rename to tests/integration/eleventy-site/_redirects
diff --git a/tests/eleventy-site/force.html b/tests/integration/eleventy-site/force.html
similarity index 100%
rename from tests/eleventy-site/force.html
rename to tests/integration/eleventy-site/force.html
diff --git a/tests/eleventy-site/functions/echo.js b/tests/integration/eleventy-site/functions/echo.js
similarity index 100%
rename from tests/eleventy-site/functions/echo.js
rename to tests/integration/eleventy-site/functions/echo.js
diff --git a/tests/eleventy-site/index.html b/tests/integration/eleventy-site/index.html
similarity index 100%
rename from tests/eleventy-site/index.html
rename to tests/integration/eleventy-site/index.html
diff --git a/tests/eleventy-site/netlify.toml b/tests/integration/eleventy-site/netlify.toml
similarity index 100%
rename from tests/eleventy-site/netlify.toml
rename to tests/integration/eleventy-site/netlify.toml
diff --git a/tests/eleventy-site/otherthing.html b/tests/integration/eleventy-site/otherthing.html
similarity index 100%
rename from tests/eleventy-site/otherthing.html
rename to tests/integration/eleventy-site/otherthing.html
diff --git a/tests/eleventy-site/package-lock.json b/tests/integration/eleventy-site/package-lock.json
similarity index 100%
rename from tests/eleventy-site/package-lock.json
rename to tests/integration/eleventy-site/package-lock.json
diff --git a/tests/eleventy-site/package.json b/tests/integration/eleventy-site/package.json
similarity index 100%
rename from tests/eleventy-site/package.json
rename to tests/integration/eleventy-site/package.json
diff --git a/tests/eleventy-site/test.html b/tests/integration/eleventy-site/test.html
similarity index 100%
rename from tests/eleventy-site/test.html
rename to tests/integration/eleventy-site/test.html
diff --git a/tests/hugo-site/config.toml b/tests/integration/hugo-site/config.toml
similarity index 100%
rename from tests/hugo-site/config.toml
rename to tests/integration/hugo-site/config.toml
diff --git a/tests/hugo-site/content/_index.html b/tests/integration/hugo-site/content/_index.html
similarity index 100%
rename from tests/hugo-site/content/_index.html
rename to tests/integration/hugo-site/content/_index.html
diff --git a/tests/hugo-site/layouts/_default/list.html b/tests/integration/hugo-site/layouts/_default/list.html
similarity index 100%
rename from tests/hugo-site/layouts/_default/list.html
rename to tests/integration/hugo-site/layouts/_default/list.html
diff --git a/tests/hugo-site/netlify.toml b/tests/integration/hugo-site/netlify.toml
similarity index 100%
rename from tests/hugo-site/netlify.toml
rename to tests/integration/hugo-site/netlify.toml
diff --git a/tests/hugo-site/package-lock.json b/tests/integration/hugo-site/package-lock.json
similarity index 100%
rename from tests/hugo-site/package-lock.json
rename to tests/integration/hugo-site/package-lock.json
diff --git a/tests/hugo-site/package.json b/tests/integration/hugo-site/package.json
similarity index 100%
rename from tests/hugo-site/package.json
rename to tests/integration/hugo-site/package.json
diff --git a/tests/hugo-site/static/_redirects b/tests/integration/hugo-site/static/_redirects
similarity index 100%
rename from tests/hugo-site/static/_redirects
rename to tests/integration/hugo-site/static/_redirects
diff --git a/tests/snapshots/command.graph.test.js.md b/tests/integration/snapshots/220.command.graph.test.js.md
similarity index 93%
rename from tests/snapshots/command.graph.test.js.md
rename to tests/integration/snapshots/220.command.graph.test.js.md
index 3d137590b47..717ba55c824 100644
--- a/tests/snapshots/command.graph.test.js.md
+++ b/tests/integration/snapshots/220.command.graph.test.js.md
@@ -1,6 +1,6 @@
-# Snapshot report for `tests/command.graph.test.js`
+# Snapshot report for `tests/220.command.graph.test.js`
-The actual snapshot is saved in `command.graph.test.js.snap`.
+The actual snapshot is saved in `220.command.graph.test.js.snap`.
Generated by [AVA](https://avajs.dev).
diff --git a/tests/snapshots/command.graph.test.js.snap b/tests/integration/snapshots/220.command.graph.test.js.snap
similarity index 100%
rename from tests/snapshots/command.graph.test.js.snap
rename to tests/integration/snapshots/220.command.graph.test.js.snap
diff --git a/tests/snapshots/command.help.test.js.md b/tests/integration/snapshots/320.command.help.test.js.md
similarity index 95%
rename from tests/snapshots/command.help.test.js.md
rename to tests/integration/snapshots/320.command.help.test.js.md
index bfdc8989c5b..219dc1d6082 100644
--- a/tests/snapshots/command.help.test.js.md
+++ b/tests/integration/snapshots/320.command.help.test.js.md
@@ -1,6 +1,6 @@
-# Snapshot report for `tests/command.help.test.js`
+# Snapshot report for `tests/320.command.help.test.js`
-The actual snapshot is saved in `command.help.test.js.snap`.
+The actual snapshot is saved in `320.command.help.test.js.snap`.
Generated by [AVA](https://avajs.dev).
diff --git a/tests/snapshots/command.help.test.js.snap b/tests/integration/snapshots/320.command.help.test.js.snap
similarity index 100%
rename from tests/snapshots/command.help.test.js.snap
rename to tests/integration/snapshots/320.command.help.test.js.snap
diff --git a/tests/snapshots/graph-codegen.test.js.md b/tests/integration/snapshots/530.graph-codegen.test.js.md
similarity index 98%
rename from tests/snapshots/graph-codegen.test.js.md
rename to tests/integration/snapshots/530.graph-codegen.test.js.md
index b5843f754aa..ae99965c6a2 100644
--- a/tests/snapshots/graph-codegen.test.js.md
+++ b/tests/integration/snapshots/530.graph-codegen.test.js.md
@@ -1,6 +1,6 @@
-# Snapshot report for `tests/graph-codegen.test.js`
+# Snapshot report for `tests/integration/530.graph-codegen.test.js`
-The actual snapshot is saved in `graph-codegen.test.js.snap`.
+The actual snapshot is saved in `530.graph-codegen.test.js.snap`.
Generated by [AVA](https://avajs.dev).
diff --git a/tests/snapshots/graph-codegen.test.js.snap b/tests/integration/snapshots/530.graph-codegen.test.js.snap
similarity index 100%
rename from tests/snapshots/graph-codegen.test.js.snap
rename to tests/integration/snapshots/530.graph-codegen.test.js.snap
diff --git a/tests/snapshots/framework-detection.test.js.md b/tests/integration/snapshots/600.framework-detection.test.js.md
similarity index 98%
rename from tests/snapshots/framework-detection.test.js.md
rename to tests/integration/snapshots/600.framework-detection.test.js.md
index a4408a302f4..f2f0d7f3398 100644
--- a/tests/snapshots/framework-detection.test.js.md
+++ b/tests/integration/snapshots/600.framework-detection.test.js.md
@@ -1,6 +1,6 @@
-# Snapshot report for `tests/framework-detection.test.js`
+# Snapshot report for `tests/600.framework-detection.test.js`
-The actual snapshot is saved in `framework-detection.test.js.snap`.
+The actual snapshot is saved in `600.framework-detection.test.js.snap`.
Generated by [AVA](https://avajs.dev).
diff --git a/tests/snapshots/framework-detection.test.js.snap b/tests/integration/snapshots/600.framework-detection.test.js.snap
similarity index 100%
rename from tests/snapshots/framework-detection.test.js.snap
rename to tests/integration/snapshots/600.framework-detection.test.js.snap
diff --git a/tests/snapshots/command.env.test.js.md b/tests/integration/snapshots/610.command.env.test.js.md
similarity index 96%
rename from tests/snapshots/command.env.test.js.md
rename to tests/integration/snapshots/610.command.env.test.js.md
index af74e0afbf7..6bc61aa1a53 100644
--- a/tests/snapshots/command.env.test.js.md
+++ b/tests/integration/snapshots/610.command.env.test.js.md
@@ -1,6 +1,6 @@
-# Snapshot report for `tests/command.env.test.js`
+# Snapshot report for `tests/610.command.env.test.js`
-The actual snapshot is saved in `command.env.test.js.snap`.
+The actual snapshot is saved in `610.command.env.test.js.snap`.
Generated by [AVA](https://avajs.dev).
diff --git a/tests/snapshots/command.env.test.js.snap b/tests/integration/snapshots/610.command.env.test.js.snap
similarity index 100%
rename from tests/snapshots/command.env.test.js.snap
rename to tests/integration/snapshots/610.command.env.test.js.snap
diff --git a/tests/utils/call-cli.js b/tests/integration/utils/call-cli.js
similarity index 100%
rename from tests/utils/call-cli.js
rename to tests/integration/utils/call-cli.js
diff --git a/tests/integration/utils/cli-path.js b/tests/integration/utils/cli-path.js
new file mode 100644
index 00000000000..33b066dec44
--- /dev/null
+++ b/tests/integration/utils/cli-path.js
@@ -0,0 +1,5 @@
+const path = require('path')
+
+const cliPath = path.resolve(__dirname, '../../../bin/run')
+
+module.exports = cliPath
diff --git a/tests/utils/create-live-test-site.js b/tests/integration/utils/create-live-test-site.js
similarity index 100%
rename from tests/utils/create-live-test-site.js
rename to tests/integration/utils/create-live-test-site.js
diff --git a/tests/utils/curl.js b/tests/integration/utils/curl.js
similarity index 100%
rename from tests/utils/curl.js
rename to tests/integration/utils/curl.js
diff --git a/tests/utils/dev-server.js b/tests/integration/utils/dev-server.js
similarity index 100%
rename from tests/utils/dev-server.js
rename to tests/integration/utils/dev-server.js
diff --git a/tests/utils/external-server.js b/tests/integration/utils/external-server.js
similarity index 100%
rename from tests/utils/external-server.js
rename to tests/integration/utils/external-server.js
diff --git a/tests/utils/got.js b/tests/integration/utils/got.js
similarity index 100%
rename from tests/utils/got.js
rename to tests/integration/utils/got.js
diff --git a/tests/utils/handle-questions.js b/tests/integration/utils/handle-questions.js
similarity index 100%
rename from tests/utils/handle-questions.js
rename to tests/integration/utils/handle-questions.js
diff --git a/tests/utils/mock-api.js b/tests/integration/utils/mock-api.js
similarity index 100%
rename from tests/utils/mock-api.js
rename to tests/integration/utils/mock-api.js
diff --git a/tests/utils/mock-execa.js b/tests/integration/utils/mock-execa.js
similarity index 91%
rename from tests/utils/mock-execa.js
rename to tests/integration/utils/mock-execa.js
index 6c908b1115a..b02aee47252 100644
--- a/tests/utils/mock-execa.js
+++ b/tests/integration/utils/mock-execa.js
@@ -2,7 +2,7 @@ const { writeFile } = require('fs').promises
const tempy = require('tempy')
-const { rmdirRecursiveAsync } = require('../../src/lib/fs')
+const { rmdirRecursiveAsync } = require('../../../src/lib/fs')
// Saves to disk a JavaScript file with the contents provided and returns
// an environment variable that replaces the `execa` module implementation.
diff --git a/tests/utils/pause.js b/tests/integration/utils/pause.js
similarity index 100%
rename from tests/utils/pause.js
rename to tests/integration/utils/pause.js
diff --git a/tests/utils/process.js b/tests/integration/utils/process.js
similarity index 100%
rename from tests/utils/process.js
rename to tests/integration/utils/process.js
diff --git a/tests/utils/site-builder.js b/tests/integration/utils/site-builder.js
similarity index 98%
rename from tests/utils/site-builder.js
rename to tests/integration/utils/site-builder.js
index 3312a8c4174..88a63858b51 100644
--- a/tests/utils/site-builder.js
+++ b/tests/integration/utils/site-builder.js
@@ -10,7 +10,7 @@ const tempDirectory = require('temp-dir')
const { toToml } = require('tomlify-j0.4')
const { v4: uuidv4 } = require('uuid')
-const { rmdirRecursiveAsync } = require('../../src/lib/fs')
+const { rmdirRecursiveAsync } = require('../../../src/lib/fs')
const ensureDir = (file) => mkdir(file, { recursive: true })
diff --git a/tests/utils/snapshots.js b/tests/integration/utils/snapshots.js
similarity index 100%
rename from tests/utils/snapshots.js
rename to tests/integration/utils/snapshots.js
diff --git a/src/lib/completion/tests/completion.test.js b/tests/unit/lib/completion/completion.test.js
similarity index 94%
rename from src/lib/completion/tests/completion.test.js
rename to tests/unit/lib/completion/completion.test.js
index 84d1744206e..851428e9d41 100644
--- a/src/lib/completion/tests/completion.test.js
+++ b/tests/unit/lib/completion/completion.test.js
@@ -5,8 +5,8 @@ const test = require('ava')
const { Argument } = require('commander')
const sinon = require('sinon')
-const { BaseCommand } = require('../../../commands/base-command')
-const { getAutocompletion } = require('../script')
+const { BaseCommand } = require('../../../../src/commands/base-command')
+const { getAutocompletion } = require('../../../../src/lib/completion/script')
const createTestCommand = () => {
const program = new BaseCommand('chef')
@@ -37,7 +37,7 @@ test('should generate a completion file', (t) => {
const stub = sinon.stub(fs, 'writeFileSync').callsFake(() => {})
const program = createTestCommand()
// eslint-disable-next-line node/global-require
- const { createAutocompletion } = require('../generate-autocompletion')
+ const { createAutocompletion } = require('../../../../src/lib/completion/generate-autocompletion')
createAutocompletion(program)
// @ts-ignore
diff --git a/src/lib/completion/tests/snapshots/completion.test.js.md b/tests/unit/lib/completion/snapshots/completion.test.js.md
similarity index 100%
rename from src/lib/completion/tests/snapshots/completion.test.js.md
rename to tests/unit/lib/completion/snapshots/completion.test.js.md
diff --git a/src/lib/completion/tests/snapshots/completion.test.js.snap b/tests/unit/lib/completion/snapshots/completion.test.js.snap
similarity index 100%
rename from src/lib/completion/tests/snapshots/completion.test.js.snap
rename to tests/unit/lib/completion/snapshots/completion.test.js.snap
diff --git a/src/lib/exec-fetcher.test.js b/tests/unit/lib/exec-fetcher.test.js
similarity index 98%
rename from src/lib/exec-fetcher.test.js
rename to tests/unit/lib/exec-fetcher.test.js
index ad1d4c2e1ac..4ee8bef0fbe 100644
--- a/src/lib/exec-fetcher.test.js
+++ b/tests/unit/lib/exec-fetcher.test.js
@@ -10,7 +10,7 @@ const sinon = require('sinon')
const processSpy = {}
const fetchLatestSpy = sinon.stub()
-const { fetchLatestVersion, getArch, getExecName } = proxyquire('./exec-fetcher', {
+const { fetchLatestVersion, getArch, getExecName } = proxyquire('../../../src/lib/exec-fetcher', {
'gh-release-fetch': {
fetchLatest: fetchLatestSpy,
},
diff --git a/src/lib/functions/runtimes/js/builders/tests/netlify-lambda.test.js b/tests/unit/lib/functions/runtimes/js/builders/netlify-lambda.test.js
similarity index 97%
rename from src/lib/functions/runtimes/js/builders/tests/netlify-lambda.test.js
rename to tests/unit/lib/functions/runtimes/js/builders/netlify-lambda.test.js
index 45ac9634d58..6004704b19b 100644
--- a/src/lib/functions/runtimes/js/builders/tests/netlify-lambda.test.js
+++ b/tests/unit/lib/functions/runtimes/js/builders/netlify-lambda.test.js
@@ -1,7 +1,7 @@
const test = require('ava')
const sinon = require('sinon')
-const { detectNetlifyLambda } = require('../netlify-lambda')
+const { detectNetlifyLambda } = require('../../../../../../../src/lib/functions/runtimes/js/builders/netlify-lambda')
test(`should not find netlify-lambda from netlify-cli package.json`, async (t) => {
t.is(await detectNetlifyLambda(), false)
diff --git a/src/lib/functions/scheduled.test.js b/tests/unit/lib/functions/scheduled.test.js
similarity index 94%
rename from src/lib/functions/scheduled.test.js
rename to tests/unit/lib/functions/scheduled.test.js
index 24013abea86..2bb016e8fb8 100644
--- a/src/lib/functions/scheduled.test.js
+++ b/tests/unit/lib/functions/scheduled.test.js
@@ -1,6 +1,6 @@
const test = require('ava')
-const { buildHelpResponse } = require('./scheduled')
+const { buildHelpResponse } = require('../../../../src/lib/functions/scheduled')
const withAccept = (accept) =>
buildHelpResponse({
diff --git a/src/lib/functions/server.test.js b/tests/unit/lib/functions/server.test.js
similarity index 92%
rename from src/lib/functions/server.test.js
rename to tests/unit/lib/functions/server.test.js
index 1e7906cc3e6..e4f7aa338e0 100644
--- a/src/lib/functions/server.test.js
+++ b/tests/unit/lib/functions/server.test.js
@@ -6,8 +6,8 @@ const test = require('ava')
const express = require('express')
const request = require('supertest')
-const { FunctionsRegistry } = require('./registry')
-const { createHandler } = require('./server')
+const { FunctionsRegistry } = require('../../../../src/lib/functions/registry')
+const { createHandler } = require('../../../../src/lib/functions/server')
/** @type { express.Express} */
let app
diff --git a/src/lib/http-agent.test.js b/tests/unit/lib/http-agent.test.js
similarity index 95%
rename from src/lib/http-agent.test.js
rename to tests/unit/lib/http-agent.test.js
index 3e76e8ecf1a..3f7626620d9 100644
--- a/src/lib/http-agent.test.js
+++ b/tests/unit/lib/http-agent.test.js
@@ -4,7 +4,7 @@ const test = require('ava')
const { createProxyServer } = require('http-proxy')
const { HttpsProxyAgent } = require('https-proxy-agent')
-const { tryGetAgent } = require('./http-agent')
+const { tryGetAgent } = require('../../../src/lib/http-agent')
test(`should return an empty object when there is no httpProxy`, async (t) => {
t.deepEqual(await tryGetAgent({}), {})
diff --git a/src/utils/deploy/hash-files.test.js b/tests/unit/utils/deploy/hash-files.test.js
similarity index 82%
rename from src/utils/deploy/hash-files.test.js
rename to tests/unit/utils/deploy/hash-files.test.js
index 9e231ef66ca..830101ad8a2 100644
--- a/src/utils/deploy/hash-files.test.js
+++ b/tests/unit/utils/deploy/hash-files.test.js
@@ -1,9 +1,8 @@
const test = require('ava')
-const { withSiteBuilder } = require('../../../tests/utils/site-builder')
-
-const { DEFAULT_CONCURRENT_HASH } = require('./constants')
-const { hashFiles } = require('./hash-files')
+const { DEFAULT_CONCURRENT_HASH } = require('../../../../src/utils/deploy/constants')
+const { hashFiles } = require('../../../../src/utils/deploy/hash-files')
+const { withSiteBuilder } = require('../../../integration/utils/site-builder')
test('Hashes files in a folder', async (t) => {
await withSiteBuilder('site-with-content', async (builder) => {
diff --git a/src/utils/deploy/hash-fns.test.js b/tests/unit/utils/deploy/hash-fns.test.js
similarity index 86%
rename from src/utils/deploy/hash-fns.test.js
rename to tests/unit/utils/deploy/hash-fns.test.js
index d8f790a4cdb..8b4d1d954f3 100644
--- a/src/utils/deploy/hash-fns.test.js
+++ b/tests/unit/utils/deploy/hash-fns.test.js
@@ -2,10 +2,9 @@
const test = require('ava')
const tempy = require('tempy')
-const { withSiteBuilder } = require('../../../tests/utils/site-builder')
-
-const { DEFAULT_CONCURRENT_HASH } = require('./constants')
-const { hashFns } = require('./hash-fns')
+const { DEFAULT_CONCURRENT_HASH } = require('../../../../src/utils/deploy/constants')
+const { hashFns } = require('../../../../src/utils/deploy/hash-fns')
+const { withSiteBuilder } = require('../../../integration/utils/site-builder')
test('Hashes files in a folder', async (t) => {
await withSiteBuilder('site-with-functions', async (builder) => {
diff --git a/src/utils/deploy/util.test.js b/tests/unit/utils/deploy/util.test.js
similarity index 86%
rename from src/utils/deploy/util.test.js
rename to tests/unit/utils/deploy/util.test.js
index 04cd8252b3a..d4046f3f3af 100644
--- a/src/utils/deploy/util.test.js
+++ b/tests/unit/utils/deploy/util.test.js
@@ -2,7 +2,7 @@ const { join } = require('path')
const test = require('ava')
-const { normalizePath } = require('./util')
+const { normalizePath } = require('../../../../src/utils/deploy/util')
test('normalizes relative file paths', (t) => {
const input = join('foo', 'bar', 'baz.js')
diff --git a/src/utils/dot-env.test.js b/tests/unit/utils/dot-env.test.js
similarity index 94%
rename from src/utils/dot-env.test.js
rename to tests/unit/utils/dot-env.test.js
index 61b44340a67..b93bc8a6728 100644
--- a/src/utils/dot-env.test.js
+++ b/tests/unit/utils/dot-env.test.js
@@ -2,9 +2,8 @@ const process = require('process')
const test = require('ava')
-const { withSiteBuilder } = require('../../tests/utils/site-builder')
-
-const { tryLoadDotEnvFiles } = require('./dot-env')
+const { tryLoadDotEnvFiles } = require('../../../src/utils/dot-env')
+const { withSiteBuilder } = require('../../integration/utils/site-builder')
test('should return an empty array for a site with no .env file', async (t) => {
await withSiteBuilder('site-without-env-file', async (builder) => {
diff --git a/src/utils/functions/get-functions.test.js b/tests/unit/utils/functions/get-functions.test.js
similarity index 94%
rename from src/utils/functions/get-functions.test.js
rename to tests/unit/utils/functions/get-functions.test.js
index ff79f34397f..a6ba31380da 100644
--- a/src/utils/functions/get-functions.test.js
+++ b/tests/unit/utils/functions/get-functions.test.js
@@ -3,9 +3,8 @@ const path = require('path')
const test = require('ava')
const sortOn = require('sort-on')
-const { withSiteBuilder } = require('../../../tests/utils/site-builder')
-
-const { getFunctions, getFunctionsAndWatchDirs } = require('./get-functions')
+const { getFunctions, getFunctionsAndWatchDirs } = require('../../../../src/utils/functions/get-functions')
+const { withSiteBuilder } = require('../../../integration/utils/site-builder')
test('should return empty object when an empty string is provided', async (t) => {
const funcs = await getFunctions('')
@@ -87,7 +86,7 @@ test.skip('should return additional watch dirs when functions requires a file ou
path: 'index.js',
// eslint-disable-next-line require-await
handler: async () => {
- // eslint-disable-next-line node/global-require, import/no-unresolved, node/no-missing-require
+ // eslint-disable-next-line node/global-require, import/no-unresolved
const { logHello } = require('../utils')
logHello()
return { statusCode: 200, body: 'Logged Hello!' }
diff --git a/src/utils/get-global-config.test.js b/tests/unit/utils/get-global-config.test.js
similarity index 91%
rename from src/utils/get-global-config.test.js
rename to tests/unit/utils/get-global-config.test.js
index b107c515e35..3e6c5a6114d 100644
--- a/src/utils/get-global-config.test.js
+++ b/tests/unit/utils/get-global-config.test.js
@@ -4,10 +4,9 @@ const path = require('path')
const test = require('ava')
-const { rmdirRecursiveAsync } = require('../lib/fs')
-const { getLegacyPathInHome, getPathInHome } = require('../lib/settings')
-
-const getGlobalConfig = require('./get-global-config')
+const { rmdirRecursiveAsync } = require('../../../src/lib/fs')
+const { getLegacyPathInHome, getPathInHome } = require('../../../src/lib/settings')
+const getGlobalConfig = require('../../../src/utils/get-global-config')
const configPath = getPathInHome(['config.json'])
const legacyConfigPath = getLegacyPathInHome(['config.json'])
diff --git a/src/utils/gh-auth.test.js b/tests/unit/utils/gh-auth.test.js
similarity index 91%
rename from src/utils/gh-auth.test.js
rename to tests/unit/utils/gh-auth.test.js
index 580ea052b05..fceb451a805 100644
--- a/src/utils/gh-auth.test.js
+++ b/tests/unit/utils/gh-auth.test.js
@@ -4,7 +4,7 @@ const fetch = require('node-fetch')
const sinon = require('sinon')
// eslint-disable-next-line import/order
-const openBrowser = require('./open-browser')
+const openBrowser = require('../../../src/utils/open-browser')
// Stub needs to be required before './gh-auth' as this uses the module
/** @type {string} */
let host
@@ -15,7 +15,7 @@ const stubbedModule = sinon.stub(openBrowser, 'openBrowser').callsFake(({ url })
})
// eslint-disable-next-line import/order
-const { authWithNetlify } = require('./gh-auth')
+const { authWithNetlify } = require('../../../src/utils/gh-auth')
test.after(() => {
stubbedModule.restore()
diff --git a/src/utils/headers.test.js b/tests/unit/utils/headers.test.js
similarity index 96%
rename from src/utils/headers.test.js
rename to tests/unit/utils/headers.test.js
index 0b098f3a5fb..42a8eb37d0d 100644
--- a/src/utils/headers.test.js
+++ b/tests/unit/utils/headers.test.js
@@ -2,9 +2,8 @@ const path = require('path')
const test = require('ava')
-const { createSiteBuilder } = require('../../tests/utils/site-builder')
-
-const { headersForPath, parseHeaders } = require('./headers')
+const { headersForPath, parseHeaders } = require('../../../src/utils/headers')
+const { createSiteBuilder } = require('../../integration/utils/site-builder')
const headers = [
{ path: '/', headers: ['X-Frame-Options: SAMEORIGIN'] },
diff --git a/src/utils/init/config-github.test.js b/tests/unit/utils/init/config-github.test.js
similarity index 92%
rename from src/utils/init/config-github.test.js
rename to tests/unit/utils/init/config-github.test.js
index 270322b08ca..5e8001ef76c 100644
--- a/src/utils/init/config-github.test.js
+++ b/tests/unit/utils/init/config-github.test.js
@@ -3,7 +3,8 @@ const octokit = require('@octokit/rest')
const test = require('ava')
const sinon = require('sinon')
-const githubAuth = require('../gh-auth')
+// eslint-disable-next-line import/order
+const githubAuth = require('../../../../src/utils/gh-auth')
let getAuthenticatedResponse
@@ -29,7 +30,7 @@ sinon.stub(githubAuth, 'getGitHubToken').callsFake(() =>
}),
)
-const { getGitHubToken } = require('./config-github')
+const { getGitHubToken } = require('../../../../src/utils/init/config-github')
// mocked configstore
let globalConfig
diff --git a/src/utils/parse-raw-flags.test.js b/tests/unit/utils/parse-raw-flags.test.js
similarity index 89%
rename from src/utils/parse-raw-flags.test.js
rename to tests/unit/utils/parse-raw-flags.test.js
index 0cbc8715fe4..d1dd2c708f5 100644
--- a/src/utils/parse-raw-flags.test.js
+++ b/tests/unit/utils/parse-raw-flags.test.js
@@ -1,6 +1,6 @@
const test = require('ava')
-const { aggressiveJSONParse, parseRawFlags } = require('./parse-raw-flags')
+const { aggressiveJSONParse, parseRawFlags } = require('../../../src/utils/parse-raw-flags')
test.serial('JSONTruthy works with various inputs', (t) => {
const testPairs = [
diff --git a/src/utils/read-repo-url.test.js b/tests/unit/utils/read-repo-url.test.js
similarity index 91%
rename from src/utils/read-repo-url.test.js
rename to tests/unit/utils/read-repo-url.test.js
index da3a0d0f861..ebcf44c23ff 100644
--- a/src/utils/read-repo-url.test.js
+++ b/tests/unit/utils/read-repo-url.test.js
@@ -1,6 +1,6 @@
const test = require('ava')
-const { parseRepoURL } = require('./read-repo-url')
+const { parseRepoURL } = require('../../../src/utils/read-repo-url')
test('parseRepoURL: should parse GitHub URL', (t) => {
const url = new URL('https://github.com/netlify-labs/all-the-functions/tree/master/functions/9-using-middleware')
diff --git a/src/utils/redirects.test.js b/tests/unit/utils/redirects.test.js
similarity index 97%
rename from src/utils/redirects.test.js
rename to tests/unit/utils/redirects.test.js
index 20771ea1d42..4b1266801c9 100644
--- a/src/utils/redirects.test.js
+++ b/tests/unit/utils/redirects.test.js
@@ -1,8 +1,7 @@
const test = require('ava')
-const { withSiteBuilder } = require('../../tests/utils/site-builder')
-
-const { parseRedirects } = require('./redirects')
+const { parseRedirects } = require('../../../src/utils/redirects')
+const { withSiteBuilder } = require('../../integration/utils/site-builder')
const defaultConfig = {
redirects: [
diff --git a/src/utils/rules-proxy.test.js b/tests/unit/utils/rules-proxy.test.js
similarity index 68%
rename from src/utils/rules-proxy.test.js
rename to tests/unit/utils/rules-proxy.test.js
index 86df9c3534e..5b2b84aab0b 100644
--- a/src/utils/rules-proxy.test.js
+++ b/tests/unit/utils/rules-proxy.test.js
@@ -1,6 +1,6 @@
const test = require('ava')
-const { getLanguage } = require('./rules-proxy')
+const { getLanguage } = require('../../../src/utils/rules-proxy')
test('getLanguage', (t) => {
const language = getLanguage({ 'accept-language': 'ur' })
diff --git a/src/utils/telemetry/validation.test.js b/tests/unit/utils/telemetry/validation.test.js
similarity index 96%
rename from src/utils/telemetry/validation.test.js
rename to tests/unit/utils/telemetry/validation.test.js
index 5aca9d04aca..2d3c0d50c1d 100644
--- a/src/utils/telemetry/validation.test.js
+++ b/tests/unit/utils/telemetry/validation.test.js
@@ -1,6 +1,6 @@
const test = require('ava')
-const isValidEventName = require('./validation')
+const isValidEventName = require('../../../../src/utils/telemetry/validation')
const getEventForProject = (projectName, eventName) => `${projectName}:${eventName}`
diff --git a/tests/utils/cli-path.js b/tests/utils/cli-path.js
deleted file mode 100644
index f045b62503a..00000000000
--- a/tests/utils/cli-path.js
+++ /dev/null
@@ -1,5 +0,0 @@
-const path = require('path')
-
-const cliPath = path.resolve(__dirname, '../../bin/run')
-
-module.exports = cliPath
diff --git a/tools/affected-test.js b/tools/affected-test.js
index 13638fb6cb0..40afa4d0920 100755
--- a/tools/affected-test.js
+++ b/tools/affected-test.js
@@ -8,8 +8,6 @@ const { grey } = require('chalk')
const execa = require('execa')
const { sync } = require('fast-glob')
-const { ava } = require('../package.json')
-
const { DependencyGraph, fileVisitor, visitorPlugins } = require('./project-graph')
const getChangedFiles = async (compareTarget = 'origin/main') => {
@@ -28,10 +26,14 @@ const getChangedFiles = async (compareTarget = 'origin/main') => {
const getAffectedFiles = (changedFiles) => {
// glob is using only posix file paths on windows we need the `\`
// by using join the paths are adjusted to the operating system
- const testFiles = sync(ava.files).map((filePath) => join(filePath))
+ const testFiles = sync(['tests/integration/**/*.test.js']).map((filePath) => join(filePath))
// in this case all files are affected
- if (changedFiles.includes('npm-shrinkwrap.json') || changedFiles.includes('package.json')) {
+ if (
+ changedFiles.includes('npm-shrinkwrap.json') ||
+ changedFiles.includes('package.json') ||
+ changedFiles.includes(join('.github', 'workflows', 'main.yml'))
+ ) {
console.log('All files are affected based on the changeset')
return testFiles
}
diff --git a/tools/tests/file-visitor-module.test.js b/tools/tests/file-visitor-module.test.js
index df7f4b0e288..cad74a7c518 100644
--- a/tools/tests/file-visitor-module.test.js
+++ b/tools/tests/file-visitor-module.test.js
@@ -4,7 +4,7 @@ const { format } = require('util')
const test = require('ava')
const mock = require('mock-fs')
-const { normalize } = require('../../tests/utils/snapshots')
+const { normalize } = require('../../tests/integration/utils/snapshots')
const { DependencyGraph, fileVisitor } = require('../project-graph')
const { esModuleMockedFileSystem } = require('./utils/file-systems')
diff --git a/tools/tests/file-visitor.test.js b/tools/tests/file-visitor.test.js
index 31e5347767f..431d338ab96 100644
--- a/tools/tests/file-visitor.test.js
+++ b/tools/tests/file-visitor.test.js
@@ -4,7 +4,7 @@ const { format } = require('util')
const test = require('ava')
const mock = require('mock-fs')
-const { normalize } = require('../../tests/utils/snapshots')
+const { normalize } = require('../../tests/integration/utils/snapshots')
const { DependencyGraph, fileVisitor } = require('../project-graph')
const { simpleMockedFileSystem } = require('./utils/file-systems')