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..887047d7b80 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 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/graph-codegen.test.js b/tests/integration/330.graph-codegen.test.js similarity index 98% rename from tests/graph-codegen.test.js rename to tests/integration/330.graph-codegen.test.js index 06d004645dd..e14e8e80806 100644 --- a/tests/graph-codegen.test.js +++ b/tests/integration/330.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/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/serving-functions.test.js b/tests/integration/530.serving-functions.test.js similarity index 98% rename from tests/serving-functions.test.js rename to tests/integration/530.serving-functions.test.js index 5618aaf7abf..8431317fd0b 100644 --- a/tests/serving-functions.test.js +++ b/tests/integration/530.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/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/330.graph-codegen.test.js.md similarity index 99% rename from tests/snapshots/graph-codegen.test.js.md rename to tests/integration/snapshots/330.graph-codegen.test.js.md index b5843f754aa..46d4aa257b3 100644 --- a/tests/snapshots/graph-codegen.test.js.md +++ b/tests/integration/snapshots/330.graph-codegen.test.js.md @@ -1,6 +1,6 @@ -# Snapshot report for `tests/graph-codegen.test.js` +# Snapshot report for `tests/330.graph-codegen.test.js` -The actual snapshot is saved in `graph-codegen.test.js.snap`. +The actual snapshot is saved in `330.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/330.graph-codegen.test.js.snap similarity index 100% rename from tests/snapshots/graph-codegen.test.js.snap rename to tests/integration/snapshots/330.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')