From 40b1cf9b45e78d07817829323320ea8a90c476e4 Mon Sep 17 00:00:00 2001 From: Lukas Holzer Date: Fri, 28 Jan 2022 13:09:01 +0100 Subject: [PATCH 01/18] chore: migrate e2e tests to esm modules (#4142) --- e2e.config.cjs => e2e.config.mjs | 6 ++-- e2e/{install.e2e.js => install.e2e.mjs} | 23 +++++++-------- e2e/utils.js | 35 ----------------------- e2e/utils.mjs | 35 +++++++++++++++++++++++ package.json | 6 ++-- tools/e2e/{run.js => run.mjs} | 14 +++++----- tools/e2e/{setup.js => setup.mjs} | 37 +++++++++++++------------ 7 files changed, 81 insertions(+), 75 deletions(-) rename e2e.config.cjs => e2e.config.mjs (69%) rename e2e/{install.e2e.js => install.e2e.mjs} (68%) delete mode 100644 e2e/utils.js create mode 100644 e2e/utils.mjs rename tools/e2e/{run.js => run.mjs} (72%) rename tools/e2e/{setup.js => setup.mjs} (82%) diff --git a/e2e.config.cjs b/e2e.config.mjs similarity index 69% rename from e2e.config.cjs rename to e2e.config.mjs index a6b5341e55c..4359f234e69 100644 --- a/e2e.config.cjs +++ b/e2e.config.mjs @@ -1,5 +1,5 @@ -module.exports = { - files: ['e2e/**/*.e2e.js'], +const config = { + files: ['e2e/**/*.e2e.mjs'], cache: true, // eslint-disable-next-line no-magic-numbers concurrency: 5, @@ -8,3 +8,5 @@ module.exports = { tap: false, timeout: '5m', } + +export default config diff --git a/e2e/install.e2e.js b/e2e/install.e2e.mjs similarity index 68% rename from e2e/install.e2e.js rename to e2e/install.e2e.mjs index 9f09f1d5bab..3d81744c3ef 100644 --- a/e2e/install.e2e.js +++ b/e2e/install.e2e.mjs @@ -1,22 +1,23 @@ -const { mkdir } = require('fs').promises -const { existsSync } = require('fs') -const { platform } = require('os') -const { join, resolve } = require('path') -const process = require('process') +import { promises, readFileSync, existsSync } from 'fs' +import { platform } from 'os' +import { join, resolve } from 'path' +import { env } from 'process' +import { fileURLToPath } from 'url' -const test = require('ava') -const execa = require('execa') +import test from 'ava' +import execa from 'execa' -const { version } = require('../package.json') +import { packageManagerConfig, packageManagerExists } from './utils.mjs' -const { packageManagerConfig, packageManagerExists } = require('./utils') +const { version } = JSON.parse(readFileSync(fileURLToPath(new URL('../package.json', import.meta.url)), 'utf-8')) +const { mkdir } = promises /** * Prepares the workspace for the test suite to run * @param {string} folderName */ const prepare = async (folderName) => { - const folder = join(process.env.E2E_TEST_WORKSPACE, folderName) + const folder = join(env.E2E_TEST_WORKSPACE, folderName) await mkdir(folder, { recursive: true }) return folder } @@ -27,7 +28,7 @@ Object.entries(packageManagerConfig).forEach(([packageManager, { install: instal testSuite(`${packageManager} → should install the cli and run the help command`, async (t) => { const cwd = await prepare(`${packageManager}-try-install`) - await execa(...installCmd, { stdio: process.env.DEBUG ? 'inherit' : 'ignore', cwd }) + await execa(...installCmd, { stdio: env.DEBUG ? 'inherit' : 'ignore', cwd }) t.is(existsSync(join(cwd, lockFile)), true, `Generated lock file ${lockFile} does not exists in ${cwd}`) diff --git a/e2e/utils.js b/e2e/utils.js deleted file mode 100644 index e1183450cbd..00000000000 --- a/e2e/utils.js +++ /dev/null @@ -1,35 +0,0 @@ -const { execSync } = require('child_process') -const process = require('process') - -const { version } = require('../package.json') - -/** - * Checks if a package manager exists - * @param {string} packageManager - * @returns {boolean} - */ -const packageManagerExists = (packageManager) => { - try { - execSync(`${packageManager} --version`) - return true - } catch { - return false - } -} - -const packageManagerConfig = { - npm: { - install: ['npm', ['install', 'netlify-cli@testing', `--registry=${process.env.E2E_TEST_REGISTRY}`]], - lockFile: 'package-lock.json' - }, - pnpm: { - install: ['pnpm', ['add', `${process.env.E2E_TEST_REGISTRY}netlify-cli/-/netlify-cli-${version}.tgz`]], - lockFile: 'pnpm-lock.yaml' - }, - yarn: { - install: ['yarn', ['add', 'netlify-cli@testing', `--registry=${process.env.E2E_TEST_REGISTRY}`]], - lockFile: 'yarn.lock' - }, -} - -module.exports = { packageManagerExists, packageManagerConfig } diff --git a/e2e/utils.mjs b/e2e/utils.mjs new file mode 100644 index 00000000000..4e27fea5db0 --- /dev/null +++ b/e2e/utils.mjs @@ -0,0 +1,35 @@ +import { execSync } from 'child_process' +import { readFileSync } from 'fs' +import { env } from 'process' +import { fileURLToPath } from 'url' + +const { version } = JSON.parse(readFileSync(fileURLToPath(new URL('../package.json', import.meta.url)), 'utf-8')) + +/** + * Checks if a package manager exists + * @param {string} packageManager + * @returns {boolean} + */ +export const packageManagerExists = (packageManager) => { + try { + execSync(`${packageManager} --version`) + return true + } catch { + return false + } +} + +export const packageManagerConfig = { + npm: { + install: ['npm', ['install', 'netlify-cli@testing', `--registry=${env.E2E_TEST_REGISTRY}`]], + lockFile: 'package-lock.json', + }, + pnpm: { + install: ['pnpm', ['add', `${env.E2E_TEST_REGISTRY}netlify-cli/-/netlify-cli-${version}.tgz`]], + lockFile: 'pnpm-lock.yaml', + }, + yarn: { + install: ['yarn', ['add', 'netlify-cli@testing', `--registry=${env.E2E_TEST_REGISTRY}`]], + lockFile: 'yarn.lock', + }, +} diff --git a/package.json b/package.json index 0037f9d75e0..95c2ce538f9 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "test:dev:ava": "ava --verbose", "test:ci:ava": "c8 -r json ava", "test:affected": "node ./tools/affected-test.js", - "e2e": "node ./tools/e2e/run.js", + "e2e": "node ./tools/e2e/run.mjs", "docs": "node ./site/scripts/docs.js", "watch": "c8 --reporter=lcov ava --watch", "site:build": "run-s site:build:*", @@ -73,8 +73,8 @@ "postinstall": "node ./scripts/postinstall.js" }, "config": { - "eslint": "--ignore-path .gitignore --cache --format=codeframe --max-warnings=0 \"{src,tools,scripts,site,tests,.github}/**/*.{js,md,html}\" \"*.{js,md,html}\" \".*.{js,md,html}\"", - "prettier": "--ignore-path .gitignore --loglevel=warn \"{src,tools,scripts,site,tests,.github}/**/*.{js,md,yml,json,html}\" \"*.{js,yml,json,html}\" \".*.{js,yml,json,html}\" \"!CHANGELOG.md\" \"!npm-shrinkwrap.json\" \"!.github/**/*.md\"" + "eslint": "--ignore-path .gitignore --cache --format=codeframe --max-warnings=0 \"{src,tools,scripts,site,tests,.github}/**/*.{mjs,cjs,js,md,html}\" \"*.{mjs,cjs,js,md,html}\" \".*.{mjs,cjs,js,md,html}\"", + "prettier": "--ignore-path .gitignore --loglevel=warn \"{src,tools,scripts,site,tests,.github}/**/*.{mjs,cjs,js,md,yml,json,html}\" \"*.{mjs,cjs,js,yml,json,html}\" \".*.{mjs,cjs,js,yml,json,html}\" \"!CHANGELOG.md\" \"!npm-shrinkwrap.json\" \"!.github/**/*.md\"" }, "dependencies": { "@netlify/build": "^26.2.0", diff --git a/tools/e2e/run.js b/tools/e2e/run.mjs similarity index 72% rename from tools/e2e/run.js rename to tools/e2e/run.mjs index ec244558f8a..f3b3b3a914e 100644 --- a/tools/e2e/run.js +++ b/tools/e2e/run.mjs @@ -1,10 +1,10 @@ #!/usr/bin/env node -const { join } = require('path') -const process = require('process') +import { join } from 'path' +import { cwd, exit } from 'process' -const execa = require('execa') +import execa from 'execa' -const { setup } = require('./setup') +import { setup } from './setup.mjs' /** The main test runner function */ const main = async () => { @@ -16,7 +16,7 @@ const main = async () => { try { console.log('Start running ava tests for **/*.e2e.js') - const { exitCode } = await execa('ava', ['**/*.e2e.js', '--config', join(process.cwd(), 'e2e.config.cjs')], { + const { exitCode } = await execa('ava', ['**/*.e2e.mjs', '--config', join(cwd(), 'e2e.config.mjs')], { stdio: 'inherit', env: { E2E_TEST_WORKSPACE: workspace, @@ -30,10 +30,10 @@ const main = async () => { } await cleanup() - process.exit(statusCode) + exit(statusCode) } main().catch((error_) => { console.error(error_ instanceof Error ? error_.message : error_) - process.exit(1) + exit(1) }) diff --git a/tools/e2e/setup.js b/tools/e2e/setup.mjs similarity index 82% rename from tools/e2e/setup.js rename to tools/e2e/setup.mjs index e08ce8bf9bc..4dbb1f01623 100644 --- a/tools/e2e/setup.js +++ b/tools/e2e/setup.mjs @@ -1,14 +1,19 @@ -const { mkdtemp } = require('fs').promises -const { appendFileSync, existsSync, readFileSync, writeFileSync } = require('fs') -const { homedir, tmpdir } = require('os') -const { join, sep } = require('path') -const process = require('process') - -const execa = require('execa') -const getPort = require('get-port') -const startVerdaccio = require('verdaccio').default +import { appendFileSync, existsSync, promises, readFileSync, writeFileSync } from 'fs' +import { homedir, tmpdir } from 'os' +import { join, sep } from 'path' +import { cwd, env } from 'process' + +import del from 'del' +import execa from 'execa' +import getPort from 'get-port' +import verdaccio from 'verdaccio' + +// TODO: remove this once `../../src/lib/fs.js` is an esm module as well +const rmdirRecursiveAsync = async (path) => { + await del(path, { force: true }) +} -const { rmdirRecursiveAsync } = require('../../src/lib/fs') +const { mkdtemp } = promises // eslint-disable-next-line no-magic-numbers const VERDACCIO_TIMEOUT_MILLISECONDS = 60 * 1000 @@ -54,7 +59,7 @@ const getVerdaccioConfig = (storage) => ({ * Start verdaccio registry and store artifacts in a new temporary folder on the os * @returns {Promise<{ url: URL; storage: string; }>} */ -const startRegistry = async () => { +export const startRegistry = async () => { // generate a random starting port to avoid race condition inside the promise when running a large // number in parallel const startPort = Math.floor(Math.random() * END_PORT_RANGE) + START_PORT_RANGE @@ -65,7 +70,7 @@ const startRegistry = async () => { reject(new Error('Starting Verdaccio Timed out')) }, VERDACCIO_TIMEOUT_MILLISECONDS) - startVerdaccio(getVerdaccioConfig(storage), freePort, storage, '1.0.0', 'verdaccio', (webServer, { port }) => { + verdaccio.default(getVerdaccioConfig(storage), freePort, storage, '1.0.0', 'verdaccio', (webServer, { port }) => { webServer.listen(port, 'localhost', () => { resolve({ url: new URL(`http://localhost:${port}/`), storage }) }) @@ -82,7 +87,7 @@ const startRegistry = async () => { * cleanup: () => Promise * } */ -const setup = async () => { +export const setup = async () => { const { storage, url } = await startRegistry() const workspace = await mkdtemp(`${tmpdir()}${sep}e2e-test-`) @@ -112,8 +117,8 @@ const setup = async () => { } // publish the CLI package to our registry - await execa('npm', ['publish', `--registry=${url}`, '--tag=testing', process.cwd()], { - stdio: process.env.DEBUG ? 'inherit' : 'ignore', + await execa('npm', ['publish', `--registry=${url}`, '--tag=testing', cwd()], { + stdio: env.DEBUG ? 'inherit' : 'ignore', }) console.log(`------------------------------------------ @@ -139,5 +144,3 @@ ${error_ instanceof Error ? error_.message : error_}`, cleanup, } } - -module.exports = { startRegistry, setup } From 1c8968d5ebb50f7fc781b92e37fc3aa3e29f3b33 Mon Sep 17 00:00:00 2001 From: Netlify Team Account 1 <90322326+netlify-team-account-1@users.noreply.github.com> Date: Fri, 28 Jan 2022 17:14:27 +0100 Subject: [PATCH 02/18] feat: add local dev experience for scheduled functions (#3689) * feat: add local dev experience for scheduled functions * feat: use synchronous function's timeout * chore: rename to scheduled.js * feat: return 400 when trying to invoke scheduled function via http * chore: use testMatrix pattern * fix: config-based schedule without esbuild * feat: add support for ISC-declared flags * fix: node v12 doesn't understand optional chaining * fix: allow esbuild to read mock files by writing them to disk * fix: wrong import * feat: use listFunction to detect ISC schedule * feat: remove unused feature flag * chore: update zisi * fix: enable parseISC hook * feat: give full command * refactor: move clockwork simulation to calling side * chore: remove one changed line * refactor: extract clockwork useragent into constants * feat: improve error message co-authored-by: Eduardo Boucas * feat: print friendly error screen * chore: trim down diff to npm-shrinkwrap * chore: remove mock-require (not used anymore) * fix: optional chaining doesnt exist * chore: add test for helpful tips and tricks * fix: correct tests * fix: add missing property to test * Update src/lib/functions/runtimes/js/builders/zisi.js Co-authored-by: Lukas Holzer * refactor: extract help response into its own testable function * chore: add some unit tests * fix: replaceAll not available on node v12 co-authored-by: Lukas Holzer * fix: remove unneeded level field * refactor: remove unused test matrix * fix: increase file change delay for macOS and windows Co-authored-by: Netlify Team Account 1 Co-authored-by: Eduardo Boucas Co-authored-by: Lukas Holzer Co-authored-by: Simon Knott --- npm-shrinkwrap.json | 77 +++-------- package.json | 3 +- src/commands/functions/functions-invoke.js | 19 ++- src/lib/functions/netlify-function.js | 10 +- .../functions/runtimes/js/builders/zisi.js | 34 ++++- src/lib/functions/runtimes/js/index.js | 3 +- src/lib/functions/scheduled.js | 98 ++++++++++++++ src/lib/functions/scheduled.test.js | 58 ++++++++ src/lib/functions/server.js | 10 ++ src/lib/functions/server.test.js | 41 ++---- src/utils/functions/constants.js | 5 + src/utils/functions/get-functions.js | 8 +- src/utils/functions/get-functions.test.js | 3 + src/utils/functions/index.js | 2 + tests/command.functions.test.js | 124 ++++++++++++++++++ 15 files changed, 391 insertions(+), 104 deletions(-) create mode 100644 src/lib/functions/scheduled.js create mode 100644 src/lib/functions/scheduled.test.js create mode 100644 src/utils/functions/constants.js diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index b843f3aca4d..853cd1185ec 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -22,6 +22,7 @@ "@sindresorhus/slugify": "^1.1.0", "ansi-escapes": "^5.0.0", "ansi-styles": "^5.0.0", + "ansi2html": "^0.0.1", "ascii-table": "0.0.9", "backoff": "^2.5.0", "better-opn": "^3.0.0", @@ -36,6 +37,7 @@ "content-type": "^1.0.4", "cookie": "^0.4.0", "copy-template-dir": "^1.4.0", + "cron-parser": "^4.2.1", "debug": "^4.1.1", "decache": "^4.6.0", "del": "^6.0.0", @@ -134,7 +136,6 @@ "ini": "^2.0.0", "jsonwebtoken": "^8.5.1", "mock-fs": "^5.1.2", - "mock-require": "^3.0.3", "p-timeout": "^4.0.0", "proxyquire": "^2.1.3", "seedrandom": "^3.0.5", @@ -4426,6 +4427,17 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ansi2html": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/ansi2html/-/ansi2html-0.0.1.tgz", + "integrity": "sha1-u4gARhtECvALkb89c2al4LhHO6g=", + "bin": { + "ansi2html": "bin/ansi2html" + }, + "engines": { + "node": ">0.4" + } + }, "node_modules/any-observable": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz", @@ -10887,12 +10899,6 @@ "node": ">=6.0" } }, - "node_modules/get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true - }, "node_modules/get-intrinsic": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", @@ -14770,31 +14776,6 @@ "node": ">=12.0.0" } }, - "node_modules/mock-require": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/mock-require/-/mock-require-3.0.3.tgz", - "integrity": "sha512-lLzfLHcyc10MKQnNUCv7dMcoY/2Qxd6wJfbqCcVk3LDb8An4hF6ohk5AztrvgKhJCqj36uyzi/p5se+tvyD+Wg==", - "dev": true, - "dependencies": { - "get-caller-file": "^1.0.2", - "normalize-path": "^2.1.1" - }, - "engines": { - "node": ">=4.3.0" - } - }, - "node_modules/mock-require/node_modules/normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "dependencies": { - "remove-trailing-separator": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/module-definition": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/module-definition/-/module-definition-3.3.1.tgz", @@ -24813,6 +24794,11 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==" }, + "ansi2html": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/ansi2html/-/ansi2html-0.0.1.tgz", + "integrity": "sha1-u4gARhtECvALkb89c2al4LhHO6g=" + }, "any-observable": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz", @@ -29735,12 +29721,6 @@ "node-source-walk": "^4.0.0" } }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true - }, "get-intrinsic": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", @@ -32621,27 +32601,6 @@ "integrity": "sha512-YkjQkdLulFrz0vD4BfNQdQRVmgycXTV7ykuHMlyv+C8WCHazpkiQRDthwa02kSyo8wKnY9wRptHfQLgmf0eR+A==", "dev": true }, - "mock-require": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/mock-require/-/mock-require-3.0.3.tgz", - "integrity": "sha512-lLzfLHcyc10MKQnNUCv7dMcoY/2Qxd6wJfbqCcVk3LDb8An4hF6ohk5AztrvgKhJCqj36uyzi/p5se+tvyD+Wg==", - "dev": true, - "requires": { - "get-caller-file": "^1.0.2", - "normalize-path": "^2.1.1" - }, - "dependencies": { - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - } - } - }, "module-definition": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/module-definition/-/module-definition-3.3.1.tgz", diff --git a/package.json b/package.json index 95c2ce538f9..3fec8304dbe 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "@sindresorhus/slugify": "^1.1.0", "ansi-escapes": "^5.0.0", "ansi-styles": "^5.0.0", + "ansi2html": "^0.0.1", "ascii-table": "0.0.9", "backoff": "^2.5.0", "better-opn": "^3.0.0", @@ -103,6 +104,7 @@ "content-type": "^1.0.4", "cookie": "^0.4.0", "copy-template-dir": "^1.4.0", + "cron-parser": "^4.2.1", "debug": "^4.1.1", "decache": "^4.6.0", "del": "^6.0.0", @@ -197,7 +199,6 @@ "ini": "^2.0.0", "jsonwebtoken": "^8.5.1", "mock-fs": "^5.1.2", - "mock-require": "^3.0.3", "p-timeout": "^4.0.0", "proxyquire": "^2.1.3", "seedrandom": "^3.0.5", diff --git a/src/commands/functions/functions-invoke.js b/src/commands/functions/functions-invoke.js index 56ef8370fa6..0ec808b89b3 100644 --- a/src/commands/functions/functions-invoke.js +++ b/src/commands/functions/functions-invoke.js @@ -3,10 +3,11 @@ const fs = require('fs') const path = require('path') const process = require('process') +const CronParser = require('cron-parser') const inquirer = require('inquirer') const fetch = require('node-fetch') -const { BACKGROUND, NETLIFYDEVWARN, chalk, error, exit, getFunctions } = require('../../utils') +const { BACKGROUND, CLOCKWORK_USERAGENT, NETLIFYDEVWARN, chalk, error, exit, getFunctions } = require('../../utils') // https://www.netlify.com/docs/functions/#event-triggered-functions const events = [ @@ -130,6 +131,13 @@ const getFunctionToTrigger = function (options, argumentName) { return argumentName } +const getNextRun = function (schedule) { + const cron = CronParser.parseExpression(schedule, { + tz: 'Etc/UTC', + }) + return cron.next().toDate() +} + /** * The functions:invoke command * @param {string} nameArgument @@ -150,11 +158,18 @@ const functionsInvoke = async (nameArgument, options, command) => { const functions = await getFunctions(functionsDir) const functionToTrigger = await getNameFromArgs(functions, options, nameArgument) + const functionObj = functions.find((func) => func.name === functionToTrigger) let headers = {} let body = {} - if (eventTriggeredFunctions.has(functionToTrigger)) { + if (functionObj.schedule) { + body.next_run = getNextRun(functionObj.schedule) + headers = { + 'user-agent': CLOCKWORK_USERAGENT, + 'X-NF-Event': 'schedule', + } + } else if (eventTriggeredFunctions.has(functionToTrigger)) { /** handle event triggered fns */ // https://www.netlify.com/docs/functions/#event-triggered-functions const [name, event] = functionToTrigger.split('-') diff --git a/src/lib/functions/netlify-function.js b/src/lib/functions/netlify-function.js index 12787321d08..e7fbe9c9f5f 100644 --- a/src/lib/functions/netlify-function.js +++ b/src/lib/functions/netlify-function.js @@ -32,6 +32,7 @@ class NetlifyFunction { // Determines whether this is a background function based on the function // name. this.isBackground = name.endsWith(BACKGROUND_SUFFIX) + this.schedule = null // List of the function's source files. This starts out as an empty set // and will get populated on every build. @@ -44,6 +45,12 @@ class NetlifyFunction { return /^[A-Za-z0-9_-]+$/.test(this.name) } + async isScheduled() { + await this.buildQueue + + return Boolean(this.schedule) + } + // The `build` method transforms source files into invocable functions. Its // return value is an object with: // @@ -61,12 +68,13 @@ class NetlifyFunction { this.buildQueue = buildFunction({ cache }) try { - const { srcFiles, ...buildData } = await this.buildQueue + const { schedule, srcFiles, ...buildData } = await this.buildQueue const srcFilesSet = new Set(srcFiles) const srcFilesDiff = this.getSrcFilesDiff(srcFilesSet) this.buildData = buildData this.srcFiles = srcFilesSet + this.schedule = schedule return { srcFilesDiff } } catch (error) { diff --git a/src/lib/functions/runtimes/js/builders/zisi.js b/src/lib/functions/runtimes/js/builders/zisi.js index c5ddfa96e00..dd92c94bf9b 100644 --- a/src/lib/functions/runtimes/js/builders/zisi.js +++ b/src/lib/functions/runtimes/js/builders/zisi.js @@ -1,7 +1,7 @@ const { mkdir, writeFile } = require('fs').promises const path = require('path') -const { zipFunction } = require('@netlify/zip-it-and-ship-it') +const { listFunction, zipFunction } = require('@netlify/zip-it-and-ship-it') const decache = require('decache') const readPkgUp = require('read-pkg-up') const sourceMapSupport = require('source-map-support') @@ -35,7 +35,11 @@ const buildFunction = async ({ cache, config, directory, func, hasTypeModule, pr // root of the functions directory (e.g. `functions/my-func.js`). In // this case, we use `mainFile` as the function path of `zipFunction`. const entryPath = functionDirectory === directory ? func.mainFile : functionDirectory - const { inputs, path: functionPath } = await memoizedBuild({ + const { + inputs, + path: functionPath, + schedule, + } = await memoizedBuild({ cache, cacheKey: `zisi-${entryPath}`, command: () => zipFunction(entryPath, targetDirectory, zipOptions), @@ -56,7 +60,22 @@ const buildFunction = async ({ cache, config, directory, func, hasTypeModule, pr clearFunctionsCache(targetDirectory) - return { buildPath, srcFiles } + return { buildPath, srcFiles, schedule } +} + +/** + * @param {object} params + * @param {unknown} params.config + * @param {string} params.mainFile + * @param {string} params.projectRoot + */ +const parseForSchedule = async ({ config, mainFile, projectRoot }) => { + const listedFunction = await listFunction(mainFile, { + config: netlifyConfigToZisiConfig({ config, projectRoot }), + parseISC: true, + }) + + return listedFunction && listedFunction.schedule } // Clears the cache for any files inside the directory from which functions are @@ -79,10 +98,11 @@ const getTargetDirectory = async ({ errorExit }) => { return targetDirectory } +const netlifyConfigToZisiConfig = ({ config, projectRoot }) => + addFunctionsConfigDefaults(normalizeFunctionsConfig({ functionsConfig: config.functions, projectRoot })) + module.exports = async ({ config, directory, errorExit, func, projectRoot }) => { - const functionsConfig = addFunctionsConfigDefaults( - normalizeFunctionsConfig({ functionsConfig: config.functions, projectRoot }), - ) + const functionsConfig = netlifyConfigToZisiConfig({ config, projectRoot }) const packageJson = await readPkgUp(func.mainFile) const hasTypeModule = packageJson && packageJson.packageJson.type === 'module' @@ -115,3 +135,5 @@ module.exports = async ({ config, directory, errorExit, func, projectRoot }) => target: targetDirectory, } } + +module.exports.parseForSchedule = parseForSchedule diff --git a/src/lib/functions/runtimes/js/index.js b/src/lib/functions/runtimes/js/index.js index cfd41242c9c..5a0786ac900 100644 --- a/src/lib/functions/runtimes/js/index.js +++ b/src/lib/functions/runtimes/js/index.js @@ -46,8 +46,9 @@ const getBuildFunction = async ({ config, directory, errorExit, func, projectRoo // main file otherwise. const functionDirectory = dirname(func.mainFile) const srcFiles = functionDirectory === directory ? [func.mainFile] : [functionDirectory] + const schedule = await detectZisiBuilder.parseForSchedule({ mainFile: func.mainFile, config, projectRoot }) - return () => ({ srcFiles }) + return () => ({ schedule, srcFiles }) } const invokeFunction = async ({ context, event, func, timeout }) => { diff --git a/src/lib/functions/scheduled.js b/src/lib/functions/scheduled.js new file mode 100644 index 00000000000..7c78aa1ae0e --- /dev/null +++ b/src/lib/functions/scheduled.js @@ -0,0 +1,98 @@ +const ansi2html = require('ansi2html') + +const { CLOCKWORK_USERAGENT } = require('../../utils') + +const { formatLambdaError } = require('./utils') + +const buildHelpResponse = ({ error, headers, path, result }) => { + const acceptsHtml = headers.accept && headers.accept.includes('text/html') + + const paragraph = (text) => { + text = text.trim() + + if (acceptsHtml) { + return ansi2html(`

${text}

`) + } + + text = text + .replace(/
/gm, '```\n')
+      .replace(/<\/code><\/pre>/gm, '\n```')
+      .replace(//gm, '`')
+      .replace(/<\/code>/gm, '`')
+
+    return `${text}\n\n`
+  }
+
+  const isSimulatedRequest = headers['user-agent'] === CLOCKWORK_USERAGENT
+
+  let message = ''
+
+  if (!isSimulatedRequest) {
+    message += paragraph(`
+You performed an HTTP request to ${path}, which is a scheduled function.
+You can do this to test your functions locally, but it won't work in production.
+    `)
+  }
+
+  if (error) {
+    message += paragraph(`
+There was an error during execution of your scheduled function:
+
+
${formatLambdaError(error)}
`) + } + + if (result) { + // lambda emulator adds level field, which isn't user-provided + const returnValue = { ...result } + delete returnValue.level + + const { statusCode } = returnValue + if (statusCode >= 500) { + message += paragraph(` +Your function returned a status code of ${statusCode}. +At the moment, Netlify does nothing about that. In the future, there might be a retry mechanism based on this. +`) + } + + const allowedKeys = new Set(['statusCode']) + const returnedKeys = Object.keys(returnValue) + const ignoredKeys = returnedKeys.filter((key) => !allowedKeys.has(key)) + + if (ignoredKeys.length !== 0) { + message += paragraph( + `Your function returned ${ignoredKeys + .map((key) => `${key}`) + .join(', ')}. Is this an accident? It won't be interpreted by Netlify.`, + ) + } + } + + const statusCode = error ? 500 : 200 + return acceptsHtml + ? { + statusCode, + contentType: 'text/html', + message: `\n + ${message}`, + } + : { + statusCode, + contentType: 'text/plain', + message, + } +} + +const handleScheduledFunction = ({ error, request, response, result }) => { + const { contentType, message, statusCode } = buildHelpResponse({ + error, + headers: request.headers, + path: request.path, + result, + }) + + response.status(statusCode) + response.set('Content-Type', contentType) + response.send(message) +} + +module.exports = { handleScheduledFunction, buildHelpResponse } diff --git a/src/lib/functions/scheduled.test.js b/src/lib/functions/scheduled.test.js new file mode 100644 index 00000000000..24013abea86 --- /dev/null +++ b/src/lib/functions/scheduled.test.js @@ -0,0 +1,58 @@ +const test = require('ava') + +const { buildHelpResponse } = require('./scheduled') + +const withAccept = (accept) => + buildHelpResponse({ + error: undefined, + headers: { + accept, + }, + path: '/', + result: { + statusCode: 200, + }, + }) + +test('buildHelpResponse does content negotiation', (t) => { + const html = withAccept('text/html') + t.is(html.contentType, 'text/html') + t.true(html.message.includes('')) + + const plain = withAccept('text/plain') + t.is(plain.contentType, 'text/plain') + t.false(plain.message.includes('')) +}) + +test('buildHelpResponse prints errors', (t) => { + const response = buildHelpResponse({ + error: new Error('test'), + headers: {}, + path: '/', + result: { + statusCode: 200, + }, + }) + + t.true(response.message.includes('There was an error')) +}) + +const withUserAgent = (userAgent) => + buildHelpResponse({ + error: new Error('test'), + headers: { + accept: 'text/plain', + 'user-agent': userAgent, + }, + path: '/', + result: { + statusCode: 200, + }, + }) + +test('buildHelpResponse conditionally prints notice about HTTP x scheduled functions', (t) => { + t.true(withUserAgent('').message.includes("it won't work in production")) + t.false(withUserAgent('Netlify Clockwork').message.includes("it won't work in production")) +}) diff --git a/src/lib/functions/server.js b/src/lib/functions/server.js index 48e8eeb928a..bbe03d3bfb8 100644 --- a/src/lib/functions/server.js +++ b/src/lib/functions/server.js @@ -6,6 +6,7 @@ const { NETLIFYDEVERR, NETLIFYDEVLOG, error: errorExit, getInternalFunctionsDir, const { handleBackgroundFunction, handleBackgroundFunctionResult } = require('./background') const { createFormSubmissionHandler } = require('./form-submissions-handler') const { FunctionsRegistry } = require('./registry') +const { handleScheduledFunction } = require('./scheduled') const { handleSynchronousFunction } = require('./synchronous') const { shouldBase64Encode } = require('./utils') @@ -105,6 +106,15 @@ const createHandler = function ({ functionsRegistry }) { const { error } = await func.invoke(event, clientContext) handleBackgroundFunctionResult(functionName, error) + } else if (await func.isScheduled()) { + const { error, result } = await func.invoke(event, clientContext) + + handleScheduledFunction({ + error, + result, + request, + response, + }) } else { const { error, result } = await func.invoke(event, clientContext) diff --git a/src/lib/functions/server.test.js b/src/lib/functions/server.test.js index 17f4b1d34e4..1e7906cc3e6 100644 --- a/src/lib/functions/server.test.js +++ b/src/lib/functions/server.test.js @@ -1,42 +1,25 @@ -const fs = require('fs') -const { platform } = require('os') +const { mkdirSync, mkdtempSync, writeFileSync } = require('fs') +const { tmpdir } = require('os') const { join } = require('path') -const zisi = require('@netlify/zip-it-and-ship-it') const test = require('ava') const express = require('express') -const mockRequire = require('mock-require') -const sinon = require('sinon') const request = require('supertest') -const projectRoot = platform() === 'win32' ? 'C:\\my-functions' : `/my-functions` -const functionsPath = `functions` - -// mock mkdir for the functions folder -sinon.stub(fs.promises, 'mkdir').withArgs(join(projectRoot, functionsPath)).returns(Promise.resolve()) - const { FunctionsRegistry } = require('./registry') const { createHandler } = require('./server') /** @type { express.Express} */ let app -test.before(async (t) => { - const mainFile = join(projectRoot, functionsPath, 'hello.js') - t.context.zisiStub = sinon.stub(zisi, 'listFunctions').returns( - Promise.resolve([ - { - name: 'hello', - mainFile, - runtime: 'js', - extension: '.js', - }, - ]), - ) +test.before(async () => { + const projectRoot = mkdtempSync(join(tmpdir(), 'functions-server-project-root')) + const functionsDirectory = join(projectRoot, 'functions') + mkdirSync(functionsDirectory) + + const mainFile = join(functionsDirectory, 'hello.js') + writeFileSync(mainFile, `exports.handler = (event) => ({ statusCode: 200, body: event.rawUrl })`) - mockRequire(mainFile, { - handler: (event) => ({ statusCode: 200, body: event.rawUrl }), - }) const functionsRegistry = new FunctionsRegistry({ projectRoot, config: {}, @@ -44,15 +27,11 @@ test.before(async (t) => { // eslint-disable-next-line no-magic-numbers settings: { port: 8888 }, }) - await functionsRegistry.scan([functionsPath]) + await functionsRegistry.scan([functionsDirectory]) app = express() app.all('*', createHandler({ functionsRegistry })) }) -test.after((t) => { - t.context.zisiStub.restore() -}) - test('should get the url as the `rawUrl` inside the function', async (t) => { await request(app) .get('/hello') diff --git a/src/utils/functions/constants.js b/src/utils/functions/constants.js new file mode 100644 index 00000000000..061d31aca74 --- /dev/null +++ b/src/utils/functions/constants.js @@ -0,0 +1,5 @@ +const CLOCKWORK_USERAGENT = 'Netlify Clockwork' + +module.exports = { + CLOCKWORK_USERAGENT, +} diff --git a/src/utils/functions/get-functions.js b/src/utils/functions/get-functions.js index 9f9c4de8a8d..9f8cef2e8fe 100644 --- a/src/utils/functions/get-functions.js +++ b/src/utils/functions/get-functions.js @@ -5,10 +5,10 @@ const getUrlPath = (functionName) => `/.netlify/functions/${functionName}` const BACKGROUND = '-background' -const addFunctionProps = ({ mainFile, name, runtime }) => { +const addFunctionProps = ({ mainFile, name, runtime, schedule }) => { const urlPath = getUrlPath(name) const isBackground = name.endsWith(BACKGROUND) - return { mainFile, name, runtime, urlPath, isBackground } + return { mainFile, name, runtime, urlPath, isBackground, schedule } } const JS = 'js' @@ -21,7 +21,9 @@ const getFunctions = async (functionsSrcDir) => { // performance optimization, load '@netlify/zip-it-and-ship-it' on demand // eslint-disable-next-line node/global-require const { listFunctions } = require('@netlify/zip-it-and-ship-it') - const functions = await listFunctions(functionsSrcDir) + const functions = await listFunctions(functionsSrcDir, { + parseISC: true, + }) const functionsWithProps = functions.filter(({ runtime }) => runtime === JS).map((func) => addFunctionProps(func)) return functionsWithProps } diff --git a/src/utils/functions/get-functions.test.js b/src/utils/functions/get-functions.test.js index 407f3848f15..ff79f34397f 100644 --- a/src/utils/functions/get-functions.test.js +++ b/src/utils/functions/get-functions.test.js @@ -37,6 +37,7 @@ test('should return object with function details for a directory with js files', mainFile: path.join(builder.directory, 'functions', 'index.js'), isBackground: false, runtime: 'js', + schedule: undefined, urlPath: '/.netlify/functions/index', }, ]) @@ -64,6 +65,7 @@ test('should mark background functions based on filenames', async (t) => { mainFile: path.join(builder.directory, 'functions', 'bar-background', 'bar-background.js'), isBackground: true, runtime: 'js', + schedule: undefined, urlPath: '/.netlify/functions/bar-background', }, { @@ -71,6 +73,7 @@ test('should mark background functions based on filenames', async (t) => { mainFile: path.join(builder.directory, 'functions', 'foo-background.js'), isBackground: true, runtime: 'js', + schedule: undefined, urlPath: '/.netlify/functions/foo-background', }, ]) diff --git a/src/utils/functions/index.js b/src/utils/functions/index.js index 8606ce08894..b5952f47014 100644 --- a/src/utils/functions/index.js +++ b/src/utils/functions/index.js @@ -1,8 +1,10 @@ +const constants = require('./constants') const edgeHandlers = require('./edge-handlers') const functions = require('./functions') const getFunctions = require('./get-functions') module.exports = { + ...constants, ...functions, ...edgeHandlers, ...getFunctions, diff --git a/tests/command.functions.test.js b/tests/command.functions.test.js index a3542b44eb2..74963e88447 100644 --- a/tests/command.functions.test.js +++ b/tests/command.functions.test.js @@ -13,6 +13,7 @@ const { withDevServer } = require('./utils/dev-server') const got = require('./utils/got') const { CONFIRM, DOWN, answerWithValue, handleQuestions } = require('./utils/handle-questions') const { withMockApi } = require('./utils/mock-api') +const { pause } = require('./utils/pause') const { killProcess } = require('./utils/process') const { withSiteBuilder } = require('./utils/site-builder') @@ -582,6 +583,129 @@ test('should trigger background function from event', async (t) => { }) }) +test('should serve helpful tips and tricks', async (t) => { + await withSiteBuilder('site-with-isc-ping-function', async (builder) => { + await builder + .withNetlifyToml({ + config: { functions: { directory: 'functions' } }, + }) + // mocking until https://github.com/netlify/functions/pull/226 landed + .withContentFile({ + path: 'node_modules/@netlify/functions/package.json', + content: `{}`, + }) + .withContentFile({ + path: 'node_modules/@netlify/functions/index.js', + content: ` + module.exports.schedule = (schedule, handler) => handler + `, + }) + .withContentFile({ + path: 'functions/hello-world.js', + content: ` + const { schedule } = require('@netlify/functions') + + module.exports.handler = schedule('@daily', () => { + return { + statusCode: 200, + body: "hello world" + } + }) + `.trim(), + }) + .buildAsync() + + await withDevServer({ cwd: builder.directory }, async (server) => { + const plainTextResponse = await got(`http://localhost:${server.port}/.netlify/functions/hello-world`, { + throwHttpErrors: false, + retry: null, + }) + const youReturnedBodyRegex = /.*Your function returned `body`. Is this an accident\?.*/ + t.regex(plainTextResponse.body, youReturnedBodyRegex) + t.regex(plainTextResponse.body, /.*You performed an HTTP request.*/) + t.is(plainTextResponse.statusCode, 200) + + const htmlResponse = await got(`http://localhost:${server.port}/.netlify/functions/hello-world`, { + throwHttpErrors: false, + retry: null, + headers: { + accept: 'text/html', + }, + }) + t.regex(htmlResponse.body, /.* { + await withSiteBuilder('site-with-isc-ping-function', async (builder) => { + await builder + .withNetlifyToml({ + config: { functions: { directory: 'functions' } }, + }) + // mocking until https://github.com/netlify/functions/pull/226 landed + .withContentFile({ + path: 'node_modules/@netlify/functions/package.json', + content: `{}`, + }) + .withContentFile({ + path: 'node_modules/@netlify/functions/index.js', + content: ` + module.exports.schedule = (schedule, handler) => handler + `, + }) + .withContentFile({ + path: 'functions/hello-world.js', + content: ` + module.exports.handler = () => { + return { + statusCode: 200 + } + } + `.trim(), + }) + .buildAsync() + + await withDevServer({ cwd: builder.directory }, async (server) => { + const helloWorldBody = () => + got(`http://localhost:${server.port}/.netlify/functions/hello-world`, { + throwHttpErrors: false, + retry: null, + }).then((response) => response.body) + + t.is(await helloWorldBody(), '') + + await builder + .withContentFile({ + path: 'functions/hello-world.js', + content: ` + const { schedule } = require('@netlify/functions') + + module.exports.handler = schedule("@daily", () => { + return { + statusCode: 200, + body: "test" + } + }) + `.trim(), + }) + .buildAsync() + + const DETECT_FILE_CHANGE_DELAY = 500 + await pause(DETECT_FILE_CHANGE_DELAY) + + const warningMessage = await helloWorldBody() + t.true(warningMessage.includes('Your function returned `body`')) + }) + }) +}) + test('should inject env variables', async (t) => { await withSiteBuilder('site-with-env-function', async (builder) => { await builder From 7d6566953ae6fb3c04992fcd84e204ffe452750e Mon Sep 17 00:00:00 2001 From: "token-generator-app[bot]" <82042599+token-generator-app[bot]@users.noreply.github.com> Date: Fri, 28 Jan 2022 18:48:14 +0100 Subject: [PATCH 03/18] chore(main): release 8.15.0 (#4144) Co-authored-by: token-generator-app[bot] <82042599+token-generator-app[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ npm-shrinkwrap.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7171f93ff24..d58cb6df4b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +## [8.15.0](https://github.com/netlify/cli/compare/v8.14.1...v8.15.0) (2022-01-28) + + +### Features + +* add local dev experience for scheduled functions ([#3689](https://github.com/netlify/cli/issues/3689)) ([1c8968d](https://github.com/netlify/cli/commit/1c8968d5ebb50f7fc781b92e37fc3aa3e29f3b33)) + ### [8.14.1](https://github.com/netlify/cli/compare/v8.14.0...v8.14.1) (2022-01-25) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 853cd1185ec..41f45e3af8d 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "netlify-cli", - "version": "8.14.1", + "version": "8.15.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "netlify-cli", - "version": "8.14.1", + "version": "8.15.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 3fec8304dbe..67ce03bb9dc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "netlify-cli", "description": "Netlify command line tool", - "version": "8.14.1", + "version": "8.15.0", "author": "Netlify Inc.", "contributors": [ "Mathias Biilmann (https://twitter.com/biilmann)", From b2b0faa4a66cdc0d6bcbe51ef5ebed1dd8936efd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 31 Jan 2022 00:41:10 +0000 Subject: [PATCH 04/18] fix(deps): update dependency stripe to v8.201.0 (#4149) Co-authored-by: Renovate Bot --- .../javascript/stripe-charge/package-lock.json | 14 +++++++------- .../stripe-subscription/package-lock.json | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/functions-templates/javascript/stripe-charge/package-lock.json b/src/functions-templates/javascript/stripe-charge/package-lock.json index d2c757f45c2..dafb945a379 100644 --- a/src/functions-templates/javascript/stripe-charge/package-lock.json +++ b/src/functions-templates/javascript/stripe-charge/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "stripe": "^8.199.0" + "stripe": "^8.201.0" } }, "node_modules/@types/node": { @@ -105,9 +105,9 @@ } }, "node_modules/stripe": { - "version": "8.199.0", - "resolved": "https://registry.npmjs.org/stripe/-/stripe-8.199.0.tgz", - "integrity": "sha512-Bc5Zfp6eOOCdde9x5NPrAczeGSKuNwemzjsfGJXWtpbUfQXgJujzTGgkhx2YuzamqakDYJkTgf9w7Ry2uY8QNA==", + "version": "8.201.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-8.201.0.tgz", + "integrity": "sha512-pF0F1DdE9zt0U6Cb0XN+REpdFkUmaqp6C7OEVOCeUpTAafjjJqrdV/WmZd7Y5MwT8XvDAxB5/v3CAXwxAp0XNg==", "dependencies": { "@types/node": ">=8.1.0", "qs": "^6.6.0" @@ -184,9 +184,9 @@ } }, "stripe": { - "version": "8.199.0", - "resolved": "https://registry.npmjs.org/stripe/-/stripe-8.199.0.tgz", - "integrity": "sha512-Bc5Zfp6eOOCdde9x5NPrAczeGSKuNwemzjsfGJXWtpbUfQXgJujzTGgkhx2YuzamqakDYJkTgf9w7Ry2uY8QNA==", + "version": "8.201.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-8.201.0.tgz", + "integrity": "sha512-pF0F1DdE9zt0U6Cb0XN+REpdFkUmaqp6C7OEVOCeUpTAafjjJqrdV/WmZd7Y5MwT8XvDAxB5/v3CAXwxAp0XNg==", "requires": { "@types/node": ">=8.1.0", "qs": "^6.6.0" diff --git a/src/functions-templates/javascript/stripe-subscription/package-lock.json b/src/functions-templates/javascript/stripe-subscription/package-lock.json index 66c7cae9357..5f5a9cb2323 100644 --- a/src/functions-templates/javascript/stripe-subscription/package-lock.json +++ b/src/functions-templates/javascript/stripe-subscription/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "stripe": "^8.199.0" + "stripe": "^8.201.0" } }, "node_modules/@types/node": { @@ -105,9 +105,9 @@ } }, "node_modules/stripe": { - "version": "8.199.0", - "resolved": "https://registry.npmjs.org/stripe/-/stripe-8.199.0.tgz", - "integrity": "sha512-Bc5Zfp6eOOCdde9x5NPrAczeGSKuNwemzjsfGJXWtpbUfQXgJujzTGgkhx2YuzamqakDYJkTgf9w7Ry2uY8QNA==", + "version": "8.201.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-8.201.0.tgz", + "integrity": "sha512-pF0F1DdE9zt0U6Cb0XN+REpdFkUmaqp6C7OEVOCeUpTAafjjJqrdV/WmZd7Y5MwT8XvDAxB5/v3CAXwxAp0XNg==", "dependencies": { "@types/node": ">=8.1.0", "qs": "^6.6.0" @@ -184,9 +184,9 @@ } }, "stripe": { - "version": "8.199.0", - "resolved": "https://registry.npmjs.org/stripe/-/stripe-8.199.0.tgz", - "integrity": "sha512-Bc5Zfp6eOOCdde9x5NPrAczeGSKuNwemzjsfGJXWtpbUfQXgJujzTGgkhx2YuzamqakDYJkTgf9w7Ry2uY8QNA==", + "version": "8.201.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-8.201.0.tgz", + "integrity": "sha512-pF0F1DdE9zt0U6Cb0XN+REpdFkUmaqp6C7OEVOCeUpTAafjjJqrdV/WmZd7Y5MwT8XvDAxB5/v3CAXwxAp0XNg==", "requires": { "@types/node": ">=8.1.0", "qs": "^6.6.0" From a41323f4f8827c228664de9ea38607b4c80efc09 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 31 Jan 2022 01:09:36 +0000 Subject: [PATCH 05/18] fix(deps): update dependency winston to v3.5.0 (#4150) Co-authored-by: Renovate Bot Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- npm-shrinkwrap.json | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 41f45e3af8d..a38470ebecd 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -115,7 +115,7 @@ "update-notifier": "^5.0.0", "uuid": "^8.0.0", "wait-port": "^0.2.2", - "winston": "^3.2.1", + "winston": "^3.5.0", "write-file-atomic": "^3.0.0" }, "bin": { @@ -21450,9 +21450,9 @@ } }, "node_modules/winston": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.4.0.tgz", - "integrity": "sha512-FqilVj+5HKwCfIHQzMxrrd5tBIH10JTS3koFGbLVWBODjiIYq7zir08rFyBT4rrTYG/eaTqDcfSIbcjSM78YSw==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.5.0.tgz", + "integrity": "sha512-OQMbmLsIdVHvm2hSurrYZs+iZNIImXneYJ6pX7LseSMEq20HdTETXiNnNX3FDwN4LB/xDRZLF6JYOY+AI112Kw==", "dependencies": { "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", @@ -21460,6 +21460,7 @@ "logform": "^2.3.2", "one-time": "^1.0.0", "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.4.2" @@ -21481,6 +21482,14 @@ "node": ">= 6.4.0" } }, + "node_modules/winston/node_modules/safe-stable-stringify": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz", + "integrity": "sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==", + "engines": { + "node": ">=10" + } + }, "node_modules/word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", @@ -37730,9 +37739,9 @@ } }, "winston": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.4.0.tgz", - "integrity": "sha512-FqilVj+5HKwCfIHQzMxrrd5tBIH10JTS3koFGbLVWBODjiIYq7zir08rFyBT4rrTYG/eaTqDcfSIbcjSM78YSw==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.5.0.tgz", + "integrity": "sha512-OQMbmLsIdVHvm2hSurrYZs+iZNIImXneYJ6pX7LseSMEq20HdTETXiNnNX3FDwN4LB/xDRZLF6JYOY+AI112Kw==", "requires": { "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", @@ -37740,9 +37749,17 @@ "logform": "^2.3.2", "one-time": "^1.0.0", "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.4.2" + }, + "dependencies": { + "safe-stable-stringify": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz", + "integrity": "sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==" + } } }, "winston-transport": { From df33dd0c14c14dce32b34c6b9bc5b5897970075d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 31 Jan 2022 01:35:53 +0000 Subject: [PATCH 06/18] chore(deps): update dependency verdaccio to v5.5.2 (#4147) Co-authored-by: Renovate Bot --- npm-shrinkwrap.json | 96 +++++++++++++++++---------------------------- 1 file changed, 36 insertions(+), 60 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index a38470ebecd..6056ac031d1 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -115,7 +115,7 @@ "update-notifier": "^5.0.0", "uuid": "^8.0.0", "wait-port": "^0.2.2", - "winston": "^3.5.0", + "winston": "^3.2.1", "write-file-atomic": "^3.0.0" }, "bin": { @@ -148,7 +148,7 @@ "tomlify-j0.4": "^3.0.0", "tree-kill": "^1.2.2", "typescript": "^4.4.4", - "verdaccio": "^5.4.0" + "verdaccio": "^5.5.2" }, "engines": { "node": "^12.20.0 || ^14.14.0 || >=16.0.0" @@ -4053,18 +4053,6 @@ "url": "https://opencollective.com/verdaccio" } }, - "node_modules/@verdaccio/readme/node_modules/marked": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.10.tgz", - "integrity": "sha512-+QvuFj0nGgO970fySghXGmuw+Fd0gD2x3+MqCWLIPf5oxdv1Ka6b2q+z9RP01P/IaKPMEramy+7cNy/Lw8c3hw==", - "dev": true, - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 12" - } - }, "node_modules/@verdaccio/streams": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/@verdaccio/streams/-/streams-10.1.0.tgz", @@ -4080,14 +4068,10 @@ } }, "node_modules/@verdaccio/ui-theme": { - "version": "6.0.0-6-next.15", - "resolved": "https://registry.npmjs.org/@verdaccio/ui-theme/-/ui-theme-6.0.0-6-next.15.tgz", - "integrity": "sha512-qKqsk3OUIG1VJh2cgkA8H1nSyB1ybMw9VAiWkBarv8uriRkwG+cIcEMUXZuG1pnr6ieNLsYtLRGKbnlBhIpSaA==", - "dev": true, - "engines": { - "node": ">=14", - "npm": ">=6" - } + "version": "6.0.0-6-next.16", + "resolved": "https://registry.npmjs.org/@verdaccio/ui-theme/-/ui-theme-6.0.0-6-next.16.tgz", + "integrity": "sha512-FbYl3273qaA0/fRwrvE876/HuvU81zjsnR70rCEojBelDuddl3xbY1LVdvthCjUGuIj2SUNpTzGhyROdqHJUCg==", + "dev": true }, "node_modules/abab": { "version": "2.0.5", @@ -14276,15 +14260,15 @@ } }, "node_modules/marked": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/marked/-/marked-2.1.3.tgz", - "integrity": "sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA==", + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.10.tgz", + "integrity": "sha512-+QvuFj0nGgO970fySghXGmuw+Fd0gD2x3+MqCWLIPf5oxdv1Ka6b2q+z9RP01P/IaKPMEramy+7cNy/Lw8c3hw==", "dev": true, "bin": { - "marked": "bin/marked" + "marked": "bin/marked.js" }, "engines": { - "node": ">= 10" + "node": ">= 12" } }, "node_modules/matcher": { @@ -21037,16 +21021,16 @@ } }, "node_modules/verdaccio": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/verdaccio/-/verdaccio-5.5.0.tgz", - "integrity": "sha512-isHIHRpjoT0cUXQyH1wAAHnO0E5Ky+pMVaaYThrzsjlkQHS2rp04xj7VPQrVHTJFIbv2VTIHRjWriw0J2Ilt8g==", + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/verdaccio/-/verdaccio-5.5.2.tgz", + "integrity": "sha512-SU107gfxE8FLrwp5GezhmWE0otWTAb7xIcz/m931vqOPjeH2MwbMf0+WuCDQ5O80XxdwurlK/JFNRYzDzqUrMA==", "dev": true, "dependencies": { "@verdaccio/commons-api": "10.1.0", "@verdaccio/local-storage": "10.1.1", "@verdaccio/readme": "10.2.1", "@verdaccio/streams": "10.1.0", - "@verdaccio/ui-theme": "6.0.0-6-next.15", + "@verdaccio/ui-theme": "6.0.0-6-next.16", "async": "3.2.3", "body-parser": "1.19.1", "clipanion": "3.1.0", @@ -21069,9 +21053,9 @@ "lodash": "4.17.21", "lru-cache": "6.0.0", "lunr-mutable-indexes": "2.3.2", - "marked": "2.1.3", + "marked": "4.0.10", "memoizee": "0.4.15", - "mime": "2.6.0", + "mime": "3.0.0", "minimatch": "3.0.4", "mkdirp": "1.0.4", "mv": "2.1.1", @@ -21162,15 +21146,15 @@ } }, "node_modules/verdaccio/node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", "dev": true, "bin": { "mime": "cli.js" }, "engines": { - "node": ">=4.0.0" + "node": ">=10.0.0" } }, "node_modules/verdaccio/node_modules/mkdirp": { @@ -24530,14 +24514,6 @@ "dompurify": "^2.2.6", "jsdom": "15.2.1", "marked": "4.0.10" - }, - "dependencies": { - "marked": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.10.tgz", - "integrity": "sha512-+QvuFj0nGgO970fySghXGmuw+Fd0gD2x3+MqCWLIPf5oxdv1Ka6b2q+z9RP01P/IaKPMEramy+7cNy/Lw8c3hw==", - "dev": true - } } }, "@verdaccio/streams": { @@ -24547,9 +24523,9 @@ "dev": true }, "@verdaccio/ui-theme": { - "version": "6.0.0-6-next.15", - "resolved": "https://registry.npmjs.org/@verdaccio/ui-theme/-/ui-theme-6.0.0-6-next.15.tgz", - "integrity": "sha512-qKqsk3OUIG1VJh2cgkA8H1nSyB1ybMw9VAiWkBarv8uriRkwG+cIcEMUXZuG1pnr6ieNLsYtLRGKbnlBhIpSaA==", + "version": "6.0.0-6-next.16", + "resolved": "https://registry.npmjs.org/@verdaccio/ui-theme/-/ui-theme-6.0.0-6-next.16.tgz", + "integrity": "sha512-FbYl3273qaA0/fRwrvE876/HuvU81zjsnR70rCEojBelDuddl3xbY1LVdvthCjUGuIj2SUNpTzGhyROdqHJUCg==", "dev": true }, "abab": { @@ -32228,9 +32204,9 @@ } }, "marked": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/marked/-/marked-2.1.3.tgz", - "integrity": "sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA==", + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.10.tgz", + "integrity": "sha512-+QvuFj0nGgO970fySghXGmuw+Fd0gD2x3+MqCWLIPf5oxdv1Ka6b2q+z9RP01P/IaKPMEramy+7cNy/Lw8c3hw==", "dev": true }, "matcher": { @@ -37421,16 +37397,16 @@ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, "verdaccio": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/verdaccio/-/verdaccio-5.5.0.tgz", - "integrity": "sha512-isHIHRpjoT0cUXQyH1wAAHnO0E5Ky+pMVaaYThrzsjlkQHS2rp04xj7VPQrVHTJFIbv2VTIHRjWriw0J2Ilt8g==", + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/verdaccio/-/verdaccio-5.5.2.tgz", + "integrity": "sha512-SU107gfxE8FLrwp5GezhmWE0otWTAb7xIcz/m931vqOPjeH2MwbMf0+WuCDQ5O80XxdwurlK/JFNRYzDzqUrMA==", "dev": true, "requires": { "@verdaccio/commons-api": "10.1.0", "@verdaccio/local-storage": "10.1.1", "@verdaccio/readme": "10.2.1", "@verdaccio/streams": "10.1.0", - "@verdaccio/ui-theme": "6.0.0-6-next.15", + "@verdaccio/ui-theme": "6.0.0-6-next.16", "async": "3.2.3", "body-parser": "1.19.1", "clipanion": "3.1.0", @@ -37453,9 +37429,9 @@ "lodash": "4.17.21", "lru-cache": "6.0.0", "lunr-mutable-indexes": "2.3.2", - "marked": "2.1.3", + "marked": "4.0.10", "memoizee": "0.4.15", - "mime": "2.6.0", + "mime": "3.0.0", "minimatch": "3.0.4", "mkdirp": "1.0.4", "mv": "2.1.1", @@ -37490,9 +37466,9 @@ } }, "mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", "dev": true }, "mkdirp": { From 50b3483efa03203859ff49583e3c91c90b1999f2 Mon Sep 17 00:00:00 2001 From: "token-generator-app[bot]" <82042599+token-generator-app[bot]@users.noreply.github.com> Date: Mon, 31 Jan 2022 08:51:33 +0000 Subject: [PATCH 07/18] chore(main): release 8.15.1 (#4153) Co-authored-by: token-generator-app[bot] <82042599+token-generator-app[bot]@users.noreply.github.com> Co-authored-by: Lukas Holzer --- CHANGELOG.md | 8 ++++++++ npm-shrinkwrap.json | 4 ++-- package.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d58cb6df4b0..c095b170bed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +### [8.15.1](https://github.com/netlify/cli/compare/v8.15.0...v8.15.1) (2022-01-31) + + +### Bug Fixes + +* **deps:** update dependency stripe to v8.201.0 ([#4149](https://github.com/netlify/cli/issues/4149)) ([b2b0faa](https://github.com/netlify/cli/commit/b2b0faa4a66cdc0d6bcbe51ef5ebed1dd8936efd)) +* **deps:** update dependency winston to v3.5.0 ([#4150](https://github.com/netlify/cli/issues/4150)) ([a41323f](https://github.com/netlify/cli/commit/a41323f4f8827c228664de9ea38607b4c80efc09)) + ## [8.15.0](https://github.com/netlify/cli/compare/v8.14.1...v8.15.0) (2022-01-28) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 6056ac031d1..895c6c0d210 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "netlify-cli", - "version": "8.15.0", + "version": "8.15.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "netlify-cli", - "version": "8.15.0", + "version": "8.15.1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 67ce03bb9dc..6dc10f06a09 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "netlify-cli", "description": "Netlify command line tool", - "version": "8.15.0", + "version": "8.15.1", "author": "Netlify Inc.", "contributors": [ "Mathias Biilmann (https://twitter.com/biilmann)", From f1c4d1017acba380048c06efa98b51b967f763d2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 31 Jan 2022 09:07:39 +0000 Subject: [PATCH 08/18] fix(deps): update rust crate tokio to 1.16.1 (#4151) Co-authored-by: Renovate Bot Co-authored-by: Lukas Holzer --- src/functions-templates/rust/hello-world/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/functions-templates/rust/hello-world/Cargo.toml b/src/functions-templates/rust/hello-world/Cargo.toml index 330a6919233..8d585b013ae 100644 --- a/src/functions-templates/rust/hello-world/Cargo.toml +++ b/src/functions-templates/rust/hello-world/Cargo.toml @@ -11,4 +11,4 @@ http = "0.2.6" lambda_runtime = "0.4.1" log = "0.4.14" simple_logger = "1.16.0" -tokio = "1.15.0" +tokio = "1.16.1" From 0550e604582265caa5f949c9f0fa96b984ead581 Mon Sep 17 00:00:00 2001 From: Lukas Holzer Date: Mon, 31 Jan 2022 17:13:58 +0100 Subject: [PATCH 09/18] chore: remove package as it is a restricted word (#4161) --- src/utils/init/utils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/init/utils.js b/src/utils/init/utils.js index 432feedf80b..4f7aa39fe76 100644 --- a/src/utils/init/utils.js +++ b/src/utils/init/utils.js @@ -123,8 +123,8 @@ const getPromptInputs = async ({ .map(({ name }) => `${name} plugin`) .map(formatTitle) .join(', ')}${EOL}➡️ OK to install??`, - choices: infos.map(({ name, package }) => ({ name: `${name} plugin`, value: package })), - default: infos.map(({ package }) => package), + choices: infos.map((info) => ({ name: `${info.name} plugin`, value: info.package })), + default: infos.map((info) => info.package), }, ] } From c89d81a5d64cdd73e84d7ad8c8cdb9abed990d40 Mon Sep 17 00:00:00 2001 From: Netlify Team Account 1 <90322326+netlify-team-account-1@users.noreply.github.com> Date: Mon, 31 Jan 2022 22:33:53 +0100 Subject: [PATCH 10/18] fix: dont show warning message for toml-defined scheduled functions (#4162) * chore: add reproduction test co-authored-by: Alexander Karagulamos * fix: respect netlify.toml-configured schedule co-authored-by: Alexander Karagulamos * refactor: prevent user-provided netlify-toml from being injected into ZISI * fix: test Co-authored-by: Netlify Team Account 1 Co-authored-by: Alexander Karagulamos --- src/commands/functions/functions-invoke.js | 2 +- src/utils/functions/get-functions.js | 10 +++++++++- tests/command.functions.test.js | 23 ++++++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/commands/functions/functions-invoke.js b/src/commands/functions/functions-invoke.js index 0ec808b89b3..a008be58e86 100644 --- a/src/commands/functions/functions-invoke.js +++ b/src/commands/functions/functions-invoke.js @@ -156,7 +156,7 @@ const functionsInvoke = async (nameArgument, options, command) => { console.warn(`${NETLIFYDEVWARN} "port" flag was not specified. Attempting to connect to localhost:8888 by default`) const port = options.port || DEFAULT_PORT - const functions = await getFunctions(functionsDir) + const functions = await getFunctions(functionsDir, config) const functionToTrigger = await getNameFromArgs(functions, options, nameArgument) const functionObj = functions.find((func) => func.name === functionToTrigger) diff --git a/src/utils/functions/get-functions.js b/src/utils/functions/get-functions.js index 9f8cef2e8fe..06c032eb864 100644 --- a/src/utils/functions/get-functions.js +++ b/src/utils/functions/get-functions.js @@ -13,7 +13,14 @@ const addFunctionProps = ({ mainFile, name, runtime, schedule }) => { const JS = 'js' -const getFunctions = async (functionsSrcDir) => { +/** + * @param {Record} functionConfigRecord + * @returns {Record} + */ +const extractSchedule = (functionConfigRecord) => + Object.fromEntries(Object.entries(functionConfigRecord).map(([name, { schedule }]) => [name, { schedule }])) + +const getFunctions = async (functionsSrcDir, config = {}) => { if (!(await fileExistsAsync(functionsSrcDir))) { return [] } @@ -22,6 +29,7 @@ const getFunctions = async (functionsSrcDir) => { // eslint-disable-next-line node/global-require const { listFunctions } = require('@netlify/zip-it-and-ship-it') const functions = await listFunctions(functionsSrcDir, { + config: config.functions ? extractSchedule(config.functions) : undefined, parseISC: true, }) const functionsWithProps = functions.filter(({ runtime }) => runtime === JS).map((func) => addFunctionProps(func)) diff --git a/tests/command.functions.test.js b/tests/command.functions.test.js index 74963e88447..3ee6e553ba4 100644 --- a/tests/command.functions.test.js +++ b/tests/command.functions.test.js @@ -643,6 +643,29 @@ test('should serve helpful tips and tricks', async (t) => { }) }) +test('should detect netlify-toml defined scheduled functions', async (t) => { + await withSiteBuilder('site-with-netlify-toml-ping-function', async (builder) => { + await builder + .withNetlifyToml({ + config: { functions: { directory: 'functions', 'test-1': { schedule: '@daily' } } }, + }) + .withFunction({ + path: 'test-1.js', + handler: async () => ({ + statusCode: 200, + }), + }) + .buildAsync() + + await withDevServer({ cwd: builder.directory }, async (server) => { + const stdout = await callCli(['functions:invoke', 'test-1', `--port=${server.port}`], { + cwd: builder.directory, + }) + t.is(stdout, '') + }) + }) +}) + test('should detect file changes to scheduled function', async (t) => { await withSiteBuilder('site-with-isc-ping-function', async (builder) => { await builder From 434cdb79e98994ec0e273959c9b1d8809dd86971 Mon Sep 17 00:00:00 2001 From: "token-generator-app[bot]" <82042599+token-generator-app[bot]@users.noreply.github.com> Date: Tue, 1 Feb 2022 10:26:16 +0100 Subject: [PATCH 11/18] chore(main): release 8.15.2 (#4157) Co-authored-by: token-generator-app[bot] <82042599+token-generator-app[bot]@users.noreply.github.com> --- CHANGELOG.md | 8 ++++++++ npm-shrinkwrap.json | 4 ++-- package.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c095b170bed..47a9da2f586 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +### [8.15.2](https://github.com/netlify/cli/compare/v8.15.1...v8.15.2) (2022-01-31) + + +### Bug Fixes + +* **deps:** update rust crate tokio to 1.16.1 ([#4151](https://github.com/netlify/cli/issues/4151)) ([f1c4d10](https://github.com/netlify/cli/commit/f1c4d1017acba380048c06efa98b51b967f763d2)) +* dont show warning message for toml-defined scheduled functions ([#4162](https://github.com/netlify/cli/issues/4162)) ([c89d81a](https://github.com/netlify/cli/commit/c89d81a5d64cdd73e84d7ad8c8cdb9abed990d40)) + ### [8.15.1](https://github.com/netlify/cli/compare/v8.15.0...v8.15.1) (2022-01-31) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 895c6c0d210..a278938c0af 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "netlify-cli", - "version": "8.15.1", + "version": "8.15.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "netlify-cli", - "version": "8.15.1", + "version": "8.15.2", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 6dc10f06a09..edf88c1dbdd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "netlify-cli", "description": "Netlify command line tool", - "version": "8.15.1", + "version": "8.15.2", "author": "Netlify Inc.", "contributors": [ "Mathias Biilmann (https://twitter.com/biilmann)", From 19815158e0f4ce32bf73990c53fa6bd6dda34e39 Mon Sep 17 00:00:00 2001 From: Netlify Team Account 1 <90322326+netlify-team-account-1@users.noreply.github.com> Date: Tue, 1 Feb 2022 15:33:21 +0100 Subject: [PATCH 12/18] fix: supply next_run for all scheduled functions calls (#4163) * chore: add reproduction test co-authored-by: Alexander Karagulamos * fix: supply next_run for all calls co-authored-by: Alexander Karagulamos * fix: linting Co-authored-by: Netlify Team Account 1 Co-authored-by: Alexander Karagulamos Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- src/commands/functions/functions-invoke.js | 10 ------ src/lib/functions/netlify-function.js | 17 +++++++++ src/lib/functions/server.js | 25 +++++++++++-- tests/command.functions.test.js | 42 ++++++++++++++++++++++ 4 files changed, 82 insertions(+), 12 deletions(-) diff --git a/src/commands/functions/functions-invoke.js b/src/commands/functions/functions-invoke.js index a008be58e86..ba956a0370c 100644 --- a/src/commands/functions/functions-invoke.js +++ b/src/commands/functions/functions-invoke.js @@ -3,7 +3,6 @@ const fs = require('fs') const path = require('path') const process = require('process') -const CronParser = require('cron-parser') const inquirer = require('inquirer') const fetch = require('node-fetch') @@ -131,13 +130,6 @@ const getFunctionToTrigger = function (options, argumentName) { return argumentName } -const getNextRun = function (schedule) { - const cron = CronParser.parseExpression(schedule, { - tz: 'Etc/UTC', - }) - return cron.next().toDate() -} - /** * The functions:invoke command * @param {string} nameArgument @@ -164,10 +156,8 @@ const functionsInvoke = async (nameArgument, options, command) => { let body = {} if (functionObj.schedule) { - body.next_run = getNextRun(functionObj.schedule) headers = { 'user-agent': CLOCKWORK_USERAGENT, - 'X-NF-Event': 'schedule', } } else if (eventTriggeredFunctions.has(functionToTrigger)) { /** handle event triggered fns */ diff --git a/src/lib/functions/netlify-function.js b/src/lib/functions/netlify-function.js index e7fbe9c9f5f..9e6956b5ae7 100644 --- a/src/lib/functions/netlify-function.js +++ b/src/lib/functions/netlify-function.js @@ -1,4 +1,6 @@ // @ts-check +const CronParser = require('cron-parser') + const { error: errorExit } = require('../../utils/command-helpers') const BACKGROUND_SUFFIX = '-background' @@ -6,6 +8,13 @@ const BACKGROUND_SUFFIX = '-background' // Returns a new set with all elements of `setA` that don't exist in `setB`. const difference = (setA, setB) => new Set([...setA].filter((item) => !setB.has(item))) +const getNextRun = function (schedule) { + const cron = CronParser.parseExpression(schedule, { + tz: 'Etc/UTC', + }) + return cron.next().toDate() +} + class NetlifyFunction { constructor({ config, @@ -51,6 +60,14 @@ class NetlifyFunction { return Boolean(this.schedule) } + async getNextRun() { + if (!(await this.isScheduled())) { + return null + } + + return getNextRun(this.schedule) + } + // The `build` method transforms source files into invocable functions. Its // return value is an object with: // diff --git a/src/lib/functions/server.js b/src/lib/functions/server.js index bbe03d3bfb8..e82f9300dd6 100644 --- a/src/lib/functions/server.js +++ b/src/lib/functions/server.js @@ -1,7 +1,14 @@ // @ts-check const jwtDecode = require('jwt-decode') -const { NETLIFYDEVERR, NETLIFYDEVLOG, error: errorExit, getInternalFunctionsDir, log } = require('../../utils') +const { + CLOCKWORK_USERAGENT, + NETLIFYDEVERR, + NETLIFYDEVLOG, + error: errorExit, + getInternalFunctionsDir, + log, +} = require('../../utils') const { handleBackgroundFunction, handleBackgroundFunctionResult } = require('./background') const { createFormSubmissionHandler } = require('./form-submissions-handler') @@ -107,7 +114,21 @@ const createHandler = function ({ functionsRegistry }) { handleBackgroundFunctionResult(functionName, error) } else if (await func.isScheduled()) { - const { error, result } = await func.invoke(event, clientContext) + const { error, result } = await func.invoke( + { + ...event, + body: JSON.stringify({ + next_run: await func.getNextRun(), + }), + isBase64Encoded: false, + headers: { + ...event.headers, + 'user-agent': CLOCKWORK_USERAGENT, + 'X-NF-Event': 'schedule', + }, + }, + clientContext, + ) handleScheduledFunction({ error, diff --git a/tests/command.functions.test.js b/tests/command.functions.test.js index 3ee6e553ba4..a740eaa4e88 100644 --- a/tests/command.functions.test.js +++ b/tests/command.functions.test.js @@ -643,6 +643,48 @@ test('should serve helpful tips and tricks', async (t) => { }) }) +test('should emulate next_run for scheduled functions', async (t) => { + await withSiteBuilder('site-with-isc-ping-function', async (builder) => { + await builder + .withNetlifyToml({ + config: { functions: { directory: 'functions' } }, + }) + // mocking until https://github.com/netlify/functions/pull/226 landed + .withContentFile({ + path: 'node_modules/@netlify/functions/package.json', + content: `{}`, + }) + .withContentFile({ + path: 'node_modules/@netlify/functions/index.js', + content: ` + module.exports.schedule = (schedule, handler) => handler + `, + }) + .withContentFile({ + path: 'functions/hello-world.js', + content: ` + const { schedule } = require('@netlify/functions') + module.exports.handler = schedule("@daily", (event) => { + const { next_run } = JSON.parse(event.body) + return { + statusCode: !!next_run ? 200 : 400, + } + }) + `.trim(), + }) + .buildAsync() + + await withDevServer({ cwd: builder.directory }, async (server) => { + const response = await got(`http://localhost:${server.port}/.netlify/functions/hello-world`, { + throwHttpErrors: false, + retry: null, + }) + + t.is(response.statusCode, 200) + }) + }) +}) + test('should detect netlify-toml defined scheduled functions', async (t) => { await withSiteBuilder('site-with-netlify-toml-ping-function', async (builder) => { await builder From 54040d846342d4c9f774cfcc74862fbdc94dad49 Mon Sep 17 00:00:00 2001 From: "token-generator-app[bot]" <82042599+token-generator-app[bot]@users.noreply.github.com> Date: Tue, 1 Feb 2022 14:46:16 +0000 Subject: [PATCH 13/18] chore(main): release 8.15.3 (#4166) Co-authored-by: token-generator-app[bot] <82042599+token-generator-app[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ npm-shrinkwrap.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47a9da2f586..4a28af0f209 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +### [8.15.3](https://github.com/netlify/cli/compare/v8.15.2...v8.15.3) (2022-02-01) + + +### Bug Fixes + +* supply next_run for all scheduled functions calls ([#4163](https://github.com/netlify/cli/issues/4163)) ([1981515](https://github.com/netlify/cli/commit/19815158e0f4ce32bf73990c53fa6bd6dda34e39)) + ### [8.15.2](https://github.com/netlify/cli/compare/v8.15.1...v8.15.2) (2022-01-31) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index a278938c0af..eeb47f8437c 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "netlify-cli", - "version": "8.15.2", + "version": "8.15.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "netlify-cli", - "version": "8.15.2", + "version": "8.15.3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index edf88c1dbdd..b275001f2f9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "netlify-cli", "description": "Netlify command line tool", - "version": "8.15.2", + "version": "8.15.3", "author": "Netlify Inc.", "contributors": [ "Mathias Biilmann (https://twitter.com/biilmann)", From c8281ec992c958a053d297b21894a0b8f137cd2b Mon Sep 17 00:00:00 2001 From: Erez Rokah Date: Thu, 3 Feb 2022 16:36:03 +0100 Subject: [PATCH 14/18] ci(test): split into multiple machines (#4164) * ci(test): split tests into multiple machines * test: add --no-worker-threads --- .github/workflows/main.yml | 68 +- .gitignore | 6 +- package.json | 12 +- src/utils/rules-proxy.js | 8 + tests/command.dev.test.js | 1913 ----------------- tests/integration/0.command.dev.test.js | 297 +++ .../10.command.addons.test.js} | 0 tests/integration/100.command.dev.test.js | 376 ++++ .../110.command.build.test.js} | 0 .../120.command.status.test.js} | 0 .../130.eleventy.test.js} | 0 .../20.command.functions.test.js} | 14 +- tests/integration/200.command.dev.test.js | 414 ++++ .../210.command.deploy.test.js} | 4 +- .../220.command.graph.test.js} | 0 .../230.rules-proxy.test.js} | 10 +- .../240.telemetry.test.js} | 2 +- .../30.command.lm.test.js} | 2 +- tests/integration/300.command.dev.test.js | 262 +++ .../310.command.dev.exec.test.js} | 0 .../320.command.help.test.js} | 0 .../330.serving-functions.test.js} | 10 +- tests/integration/400.command.dev.test.js | 662 ++++++ .../410.command.dev.trace.test.js} | 0 .../420.command.init.test.js} | 0 tests/integration/500.command.dev.test.js | 374 ++++ .../510.hugo.test.js} | 0 .../520.command.link.test.js} | 2 +- .../530.graph-codegen.test.js} | 2 +- .../600.framework-detection.test.js} | 6 +- .../610.command.env.test.js} | 0 .../620.serving-functions-rust.test.js} | 0 .../630.serving-functions-go.test.js} | 0 .../assets/bundled-function-1.zip | Bin tests/{ => integration}/assets/cert.pem | 0 tests/{ => integration}/assets/key.pem | 0 .../netlifyGraphOperationsLibrary.graphql | 0 .../assets/netlifyGraphSchema.graphql | 0 .../eleventy-site/.eleventy.js | 0 .../eleventy-site/.gitignore | 0 .../eleventy-site/_redirects | 0 .../eleventy-site/force.html | 0 .../eleventy-site/functions/echo.js | 0 .../eleventy-site/index.html | 0 .../eleventy-site/netlify.toml | 0 .../eleventy-site/otherthing.html | 0 .../eleventy-site/package-lock.json | 0 .../eleventy-site/package.json | 0 .../{ => integration}/eleventy-site/test.html | 0 tests/{ => integration}/hugo-site/config.toml | 0 .../hugo-site/content/_index.html | 0 .../hugo-site/layouts/_default/list.html | 0 .../{ => integration}/hugo-site/netlify.toml | 0 .../hugo-site/package-lock.json | 0 .../{ => integration}/hugo-site/package.json | 0 .../hugo-site/static/_redirects | 0 .../snapshots/220.command.graph.test.js.md} | 4 +- .../snapshots/220.command.graph.test.js.snap} | Bin .../snapshots/320.command.help.test.js.md} | 4 +- .../snapshots/320.command.help.test.js.snap} | Bin .../snapshots/530.graph-codegen.test.js.md} | 4 +- .../snapshots/530.graph-codegen.test.js.snap} | Bin .../600.framework-detection.test.js.md} | 4 +- .../600.framework-detection.test.js.snap} | Bin .../snapshots/610.command.env.test.js.md} | 4 +- .../snapshots/610.command.env.test.js.snap} | Bin tests/{ => integration}/utils/call-cli.js | 0 tests/integration/utils/cli-path.js | 5 + .../utils/create-live-test-site.js | 0 tests/{ => integration}/utils/curl.js | 0 tests/{ => integration}/utils/dev-server.js | 0 .../utils/external-server.js | 0 tests/{ => integration}/utils/got.js | 0 .../utils/handle-questions.js | 0 tests/{ => integration}/utils/mock-api.js | 0 tests/{ => integration}/utils/mock-execa.js | 2 +- tests/{ => integration}/utils/pause.js | 0 tests/{ => integration}/utils/process.js | 0 tests/{ => integration}/utils/site-builder.js | 2 +- tests/{ => integration}/utils/snapshots.js | 0 .../unit/lib/completion}/completion.test.js | 6 +- .../snapshots/completion.test.js.md | 0 .../snapshots/completion.test.js.snap | Bin {src => tests/unit}/lib/exec-fetcher.test.js | 2 +- .../js/builders}/netlify-lambda.test.js | 2 +- .../unit}/lib/functions/scheduled.test.js | 2 +- .../unit}/lib/functions/server.test.js | 4 +- {src => tests/unit}/lib/http-agent.test.js | 2 +- .../unit}/utils/deploy/hash-files.test.js | 7 +- .../unit}/utils/deploy/hash-fns.test.js | 7 +- {src => tests/unit}/utils/deploy/util.test.js | 2 +- {src => tests/unit}/utils/dot-env.test.js | 5 +- .../utils/functions/get-functions.test.js | 7 +- .../unit}/utils/get-global-config.test.js | 7 +- {src => tests/unit}/utils/gh-auth.test.js | 4 +- {src => tests/unit}/utils/headers.test.js | 5 +- .../unit}/utils/init/config-github.test.js | 5 +- .../unit}/utils/parse-raw-flags.test.js | 2 +- .../unit}/utils/read-repo-url.test.js | 2 +- {src => tests/unit}/utils/redirects.test.js | 5 +- {src => tests/unit}/utils/rules-proxy.test.js | 2 +- .../unit}/utils/telemetry/validation.test.js | 2 +- tests/utils/cli-path.js | 5 - tools/affected-test.js | 10 +- tools/tests/file-visitor-module.test.js | 2 +- tools/tests/file-visitor.test.js | 2 +- 106 files changed, 2553 insertions(+), 2019 deletions(-) delete mode 100644 tests/command.dev.test.js create mode 100644 tests/integration/0.command.dev.test.js rename tests/{command.addons.test.js => integration/10.command.addons.test.js} (100%) create mode 100644 tests/integration/100.command.dev.test.js rename tests/{command.build.test.js => integration/110.command.build.test.js} (100%) rename tests/{command.status.test.js => integration/120.command.status.test.js} (100%) rename tests/{eleventy.test.js => integration/130.eleventy.test.js} (100%) rename tests/{command.functions.test.js => integration/20.command.functions.test.js} (98%) create mode 100644 tests/integration/200.command.dev.test.js rename tests/{command.deploy.test.js => integration/210.command.deploy.test.js} (99%) rename tests/{command.graph.test.js => integration/220.command.graph.test.js} (100%) rename tests/{rules-proxy.test.js => integration/230.rules-proxy.test.js} (84%) rename tests/{telemetry.test.js => integration/240.telemetry.test.js} (96%) rename tests/{command.lm.test.js => integration/30.command.lm.test.js} (98%) create mode 100644 tests/integration/300.command.dev.test.js rename tests/{command.dev.exec.test.js => integration/310.command.dev.exec.test.js} (100%) rename tests/{command.help.test.js => integration/320.command.help.test.js} (100%) rename tests/{serving-functions.test.js => integration/330.serving-functions.test.js} (98%) create mode 100644 tests/integration/400.command.dev.test.js rename tests/{command.dev.trace.test.js => integration/410.command.dev.trace.test.js} (100%) rename tests/{command.init.test.js => integration/420.command.init.test.js} (100%) create mode 100644 tests/integration/500.command.dev.test.js rename tests/{hugo.test.js => integration/510.hugo.test.js} (100%) rename tests/{command.link.test.js => integration/520.command.link.test.js} (96%) rename tests/{graph-codegen.test.js => integration/530.graph-codegen.test.js} (98%) rename tests/{framework-detection.test.js => integration/600.framework-detection.test.js} (98%) rename tests/{command.env.test.js => integration/610.command.env.test.js} (100%) rename tests/{serving-functions-rust.test.js => integration/620.serving-functions-rust.test.js} (100%) rename tests/{serving-functions-go.test.js => integration/630.serving-functions-go.test.js} (100%) rename tests/{ => integration}/assets/bundled-function-1.zip (100%) rename tests/{ => integration}/assets/cert.pem (100%) rename tests/{ => integration}/assets/key.pem (100%) rename tests/{ => integration}/assets/netlifyGraphOperationsLibrary.graphql (100%) rename tests/{ => integration}/assets/netlifyGraphSchema.graphql (100%) rename tests/{ => integration}/eleventy-site/.eleventy.js (100%) rename tests/{ => integration}/eleventy-site/.gitignore (100%) rename tests/{ => integration}/eleventy-site/_redirects (100%) rename tests/{ => integration}/eleventy-site/force.html (100%) rename tests/{ => integration}/eleventy-site/functions/echo.js (100%) rename tests/{ => integration}/eleventy-site/index.html (100%) rename tests/{ => integration}/eleventy-site/netlify.toml (100%) rename tests/{ => integration}/eleventy-site/otherthing.html (100%) rename tests/{ => integration}/eleventy-site/package-lock.json (100%) rename tests/{ => integration}/eleventy-site/package.json (100%) rename tests/{ => integration}/eleventy-site/test.html (100%) rename tests/{ => integration}/hugo-site/config.toml (100%) rename tests/{ => integration}/hugo-site/content/_index.html (100%) rename tests/{ => integration}/hugo-site/layouts/_default/list.html (100%) rename tests/{ => integration}/hugo-site/netlify.toml (100%) rename tests/{ => integration}/hugo-site/package-lock.json (100%) rename tests/{ => integration}/hugo-site/package.json (100%) rename tests/{ => integration}/hugo-site/static/_redirects (100%) rename tests/{snapshots/command.graph.test.js.md => integration/snapshots/220.command.graph.test.js.md} (93%) rename tests/{snapshots/command.graph.test.js.snap => integration/snapshots/220.command.graph.test.js.snap} (100%) rename tests/{snapshots/command.help.test.js.md => integration/snapshots/320.command.help.test.js.md} (95%) rename tests/{snapshots/command.help.test.js.snap => integration/snapshots/320.command.help.test.js.snap} (100%) rename tests/{snapshots/graph-codegen.test.js.md => integration/snapshots/530.graph-codegen.test.js.md} (98%) rename tests/{snapshots/graph-codegen.test.js.snap => integration/snapshots/530.graph-codegen.test.js.snap} (100%) rename tests/{snapshots/framework-detection.test.js.md => integration/snapshots/600.framework-detection.test.js.md} (98%) rename tests/{snapshots/framework-detection.test.js.snap => integration/snapshots/600.framework-detection.test.js.snap} (100%) rename tests/{snapshots/command.env.test.js.md => integration/snapshots/610.command.env.test.js.md} (96%) rename tests/{snapshots/command.env.test.js.snap => integration/snapshots/610.command.env.test.js.snap} (100%) rename tests/{ => integration}/utils/call-cli.js (100%) create mode 100644 tests/integration/utils/cli-path.js rename tests/{ => integration}/utils/create-live-test-site.js (100%) rename tests/{ => integration}/utils/curl.js (100%) rename tests/{ => integration}/utils/dev-server.js (100%) rename tests/{ => integration}/utils/external-server.js (100%) rename tests/{ => integration}/utils/got.js (100%) rename tests/{ => integration}/utils/handle-questions.js (100%) rename tests/{ => integration}/utils/mock-api.js (100%) rename tests/{ => integration}/utils/mock-execa.js (91%) rename tests/{ => integration}/utils/pause.js (100%) rename tests/{ => integration}/utils/process.js (100%) rename tests/{ => integration}/utils/site-builder.js (98%) rename tests/{ => integration}/utils/snapshots.js (100%) rename {src/lib/completion/tests => tests/unit/lib/completion}/completion.test.js (94%) rename {src/lib/completion/tests => tests/unit/lib/completion}/snapshots/completion.test.js.md (100%) rename {src/lib/completion/tests => tests/unit/lib/completion}/snapshots/completion.test.js.snap (100%) rename {src => tests/unit}/lib/exec-fetcher.test.js (98%) rename {src/lib/functions/runtimes/js/builders/tests => tests/unit/lib/functions/runtimes/js/builders}/netlify-lambda.test.js (97%) rename {src => tests/unit}/lib/functions/scheduled.test.js (94%) rename {src => tests/unit}/lib/functions/server.test.js (92%) rename {src => tests/unit}/lib/http-agent.test.js (95%) rename {src => tests/unit}/utils/deploy/hash-files.test.js (82%) rename {src => tests/unit}/utils/deploy/hash-fns.test.js (86%) rename {src => tests/unit}/utils/deploy/util.test.js (86%) rename {src => tests/unit}/utils/dot-env.test.js (94%) rename {src => tests/unit}/utils/functions/get-functions.test.js (94%) rename {src => tests/unit}/utils/get-global-config.test.js (91%) rename {src => tests/unit}/utils/gh-auth.test.js (91%) rename {src => tests/unit}/utils/headers.test.js (96%) rename {src => tests/unit}/utils/init/config-github.test.js (92%) rename {src => tests/unit}/utils/parse-raw-flags.test.js (89%) rename {src => tests/unit}/utils/read-repo-url.test.js (91%) rename {src => tests/unit}/utils/redirects.test.js (97%) rename {src => tests/unit}/utils/rules-proxy.test.js (68%) rename {src => tests/unit}/utils/telemetry/validation.test.js (96%) delete mode 100644 tests/utils/cli-path.js diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f6971aa4fa5..cffb4c8cc4e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,19 +8,55 @@ on: jobs: build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macOS-latest, windows-latest] + node-version: ['*'] + steps: + # Sets an output parameter if this is a release PR + - name: Check for release + id: release-check + run: echo "::set-output name=IS_RELEASE::true" + if: "${{ startsWith(github.head_ref, 'release-') }}" + - name: Git checkout + uses: actions/checkout@v2 + if: '${{!steps.release-check.outputs.IS_RELEASE}}' + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + cache-dependency-path: 'npm-shrinkwrap.json' + check-latest: true + if: '${{!steps.release-check.outputs.IS_RELEASE}}' + - name: Install core dependencies + run: npm ci --no-audit + if: '${{!steps.release-check.outputs.IS_RELEASE}}' + - name: Install site dependencies + run: npm run site:build:install + if: '${{!steps.release-check.outputs.IS_RELEASE}}' + - name: Linting + run: npm run format:ci + if: '${{!steps.release-check.outputs.IS_RELEASE}}' + - name: Run unit tests + run: npm run test:ci:ava:unit + if: '${{!steps.release-check.outputs.IS_RELEASE}}' + test: runs-on: ${{ matrix.os }} timeout-minutes: 30 strategy: matrix: os: [ubuntu-latest, macOS-latest, windows-latest] node-version: [12.x, '*'] + machine: ['0', '1', '2', '3', '4', '5', '6'] + exclude: - os: macOS-latest node-version: '12.x' - os: windows-latest node-version: '12.x' fail-fast: false - steps: # Sets an output parameter if this is a release PR - name: Check for release @@ -32,11 +68,12 @@ jobs: run: | REG ADD HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\TCPIP\Parameters /v MaxUserPort /t REG_DWORD /d 32768 /f REG ADD HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\TCPIP\Parameters /v TcpTimedWaitDelay /t REG_DWORD /d 30 /f - if: "${{ matrix.os == 'windows-latest' }}" + if: "${{ matrix.os == 'windows-latest' && !steps.release-check.outputs.IS_RELEASE }}" - name: Git checkout uses: actions/checkout@v2 with: fetch-depth: 0 + if: '${{!steps.release-check.outputs.IS_RELEASE}}' - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2 with: @@ -44,24 +81,22 @@ jobs: cache: 'npm' cache-dependency-path: 'npm-shrinkwrap.json' check-latest: true + if: '${{!steps.release-check.outputs.IS_RELEASE}}' - name: Install core dependencies run: npm ci --no-audit - - name: Install site dependencies - run: npm run site:build:install - - name: Linting - run: npm run format:ci - if: "${{ matrix.node-version == '*' && !steps.release-check.outputs.IS_RELEASE}}" + if: '${{!steps.release-check.outputs.IS_RELEASE}}' - name: Determine Test Command uses: haya14busa/action-cond@v1 id: testCommand with: cond: ${{ github.event_name == 'pull_request' }} if_true: 'npm run test:affected ${{ github.event.pull_request.base.sha }}' # on pull requests test with the project graph only the affected tests - if_false: 'npm run test:ci' # on the base branch run all the tests as security measure + if_false: 'npm run test:ci:ava:integration' # on the base branch run all the tests as security measure + if: '${{ !steps.release-check.outputs.IS_RELEASE }}' - name: Prepare tests run: npm run test:init - - name: Tests if: '${{ !steps.release-check.outputs.IS_RELEASE }}' + - name: Tests run: ${{ steps.testCommand.outputs.value }} env: # GitHub secrets are not available when running on PR from forks @@ -74,8 +109,12 @@ jobs: # Changes the polling interval used by the file watcher CHOKIDAR_INTERVAL: 20 CHOKIDAR_USEPOLLING: 1 - - name: Get test coverage flags + + # split tests across multiple machines + CI_NODE_INDEX: ${{ matrix.machine }} + CI_NODE_TOTAL: 7 if: '${{ !steps.release-check.outputs.IS_RELEASE }}' + - name: Get test coverage flags id: test-coverage-flags run: |- os=${{ matrix.os }} @@ -83,9 +122,16 @@ jobs: echo "::set-output name=os::${os/-latest/}" echo "::set-output name=node::node_${node//[.*]/}" shell: bash - - uses: codecov/codecov-action@v2 if: '${{ !steps.release-check.outputs.IS_RELEASE }}' + - uses: codecov/codecov-action@v2 continue-on-error: true with: file: coverage/coverage-final.json flags: ${{ steps.test-coverage-flags.outputs.os }},${{ steps.test-coverage-flags.outputs.node }} + if: '${{ !steps.release-check.outputs.IS_RELEASE }}' + all: + needs: [build, test] + runs-on: ubuntu-latest + steps: + - name: Log success + run: echo "Finished running all tests" diff --git a/.gitignore b/.gitignore index 01f99a450d1..c26234151c9 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,6 @@ site/src/**/*.md # tests .eslintcache -tests/hugo-site/resources -tests/hugo-site/out -tests/hugo-site/.hugo_build.lock +tests/integration/hugo-site/resources +tests/integration/hugo-site/out +tests/integration/hugo-site/.hugo_build.lock diff --git a/package.json b/package.json index b275001f2f9..49d4e73219e 100644 --- a/package.json +++ b/package.json @@ -55,14 +55,14 @@ "format:check:prettier": "cross-env-shell prettier --check $npm_package_config_prettier", "format:fix:prettier": "cross-env-shell prettier --write $npm_package_config_prettier", "test:dev": "run-s test:init:* test:dev:*", - "test:ci": "run-s test:ci:*", "test:init": "run-s test:init:*", "test:init:cli-version": "npm run start -- --version", "test:init:cli-help": "npm run start -- --help", - "test:init:eleventy-deps": "npm ci --prefix tests/eleventy-site --no-audit", - "test:init:hugo-deps": "npm ci --prefix tests/hugo-site --no-audit", + "test:init:eleventy-deps": "npm ci --prefix tests/integration/eleventy-site --no-audit", + "test:init:hugo-deps": "npm ci --prefix tests/integration/hugo-site --no-audit", "test:dev:ava": "ava --verbose", - "test:ci:ava": "c8 -r json ava", + "test:ci:ava:unit": "c8 -r json ava --no-worker-threads tests/unit/**/*.test.js tools/**/*.test.js", + "test:ci:ava:integration": "c8 -r json ava --concurrency 1 --no-worker-threads tests/integration/**/*.test.js", "test:affected": "node ./tools/affected-test.js", "e2e": "node ./tools/e2e/run.mjs", "docs": "node ./site/scripts/docs.js", @@ -215,10 +215,8 @@ }, "ava": { "files": [ - "site/**/*.test.js", - "src/**/*.test.js", "tools/**/*.test.js", - "tests/*.test.js" + "tests/**/*.test.js" ], "cache": true, "concurrency": 5, diff --git a/src/utils/rules-proxy.js b/src/utils/rules-proxy.js index 932bd294b9a..cb9ac8013d8 100644 --- a/src/utils/rules-proxy.js +++ b/src/utils/rules-proxy.js @@ -11,14 +11,21 @@ const { fileExistsAsync } = require('../lib/fs') const { NETLIFYDEVLOG } = require('./command-helpers') const { parseRedirects } = require('./redirects') +const watchers = [] + const onChanges = function (files, listener) { files.forEach((file) => { const watcher = chokidar.watch(file) watcher.on('change', listener) watcher.on('unlink', listener) + watchers.push(watcher) }) } +const getWatchers = function () { + return watchers +} + const getLanguage = function (headers) { if (headers['accept-language']) { return headers['accept-language'].split(',')[0].slice(0, 2) @@ -97,4 +104,5 @@ module.exports = { onChanges, getLanguage, createRewriter, + getWatchers, } diff --git a/tests/command.dev.test.js b/tests/command.dev.test.js deleted file mode 100644 index 9647409bf7b..00000000000 --- a/tests/command.dev.test.js +++ /dev/null @@ -1,1913 +0,0 @@ -// Handlers are meant to be async outside tests -/* eslint-disable require-await */ -const { copyFile } = require('fs').promises -const http = require('http') -const os = require('os') -const path = require('path') -const process = require('process') - -// eslint-disable-next-line ava/use-test -const avaTest = require('ava') -const { isCI } = require('ci-info') -const dotProp = require('dot-prop') -const FormData = require('form-data') -const jwt = require('jsonwebtoken') - -const { curl } = require('./utils/curl') -const { withDevServer } = require('./utils/dev-server') -const { startExternalServer } = require('./utils/external-server') -const got = require('./utils/got') -const { withMockApi } = require('./utils/mock-api') -const { withSiteBuilder } = require('./utils/site-builder') - -const test = isCI ? avaTest.serial.bind(avaTest) : avaTest - -const testMatrix = [ - { args: [] }, - - // some tests are still failing with this enabled - // { args: ['--edgeHandlers'] } -] - -const testName = (title, args) => (args.length <= 0 ? title : `${title} - ${args.join(' ')}`) - -const JWT_EXPIRY = 1_893_456_000 -const getToken = ({ jwtRolePath = 'app_metadata.authorization.roles', jwtSecret = 'secret', roles }) => { - const payload = { - exp: JWT_EXPIRY, - sub: '12345678', - } - return jwt.sign(dotProp.set(payload, jwtRolePath, roles), jwtSecret) -} - -const setupRoleBasedRedirectsSite = (builder) => { - builder - .withContentFiles([ - { - path: 'index.html', - content: 'index', - }, - { - path: 'admin/foo.html', - content: 'foo', - }, - ]) - .withRedirectsFile({ - redirects: [{ from: `/admin/*`, to: ``, status: '200!', condition: 'Role=admin' }], - }) - return builder -} - -const validateRoleBasedRedirectsSite = async ({ args, builder, jwtRolePath, jwtSecret, t }) => { - const adminToken = getToken({ jwtSecret, jwtRolePath, roles: ['admin'] }) - const editorToken = getToken({ jwtSecret, jwtRolePath, roles: ['editor'] }) - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const unauthenticatedResponse = await got(`${server.url}/admin`, { throwHttpErrors: false }) - t.is(unauthenticatedResponse.statusCode, 404) - t.is(unauthenticatedResponse.body, 'Not Found') - - const authenticatedResponse = await got(`${server.url}/admin/foo`, { - headers: { - cookie: `nf_jwt=${adminToken}`, - }, - }) - t.is(authenticatedResponse.statusCode, 200) - t.is(authenticatedResponse.body, 'foo') - - const wrongRoleResponse = await got(`${server.url}/admin/foo`, { - headers: { - cookie: `nf_jwt=${editorToken}`, - }, - throwHttpErrors: false, - }) - t.is(wrongRoleResponse.statusCode, 404) - t.is(wrongRoleResponse.body, 'Not Found') - }) -} - -testMatrix.forEach(({ args }) => { - test(testName('should return index file when / is accessed', args), async (t) => { - await withSiteBuilder('site-with-index-file', async (builder) => { - builder.withContentFile({ - path: 'index.html', - content: '

⊂◉‿◉つ

', - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got(server.url).text() - t.is(response, '

⊂◉‿◉つ

') - }) - }) - }) - - test(testName('should return user defined headers when / is accessed', args), async (t) => { - await withSiteBuilder('site-with-headers-on-root', async (builder) => { - builder.withContentFile({ - path: 'index.html', - content: '

⊂◉‿◉つ

', - }) - - const headerName = 'X-Frame-Options' - const headerValue = 'SAMEORIGIN' - builder.withHeadersFile({ headers: [{ path: '/*', headers: [`${headerName}: ${headerValue}`] }] }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const { headers } = await got(server.url) - t.is(headers[headerName.toLowerCase()], headerValue) - }) - }) - }) - - test(testName('should return user defined headers when non-root path is accessed', args), async (t) => { - await withSiteBuilder('site-with-headers-on-non-root', async (builder) => { - builder.withContentFile({ - path: 'foo/index.html', - content: '

⊂◉‿◉つ

', - }) - - const headerName = 'X-Frame-Options' - const headerValue = 'SAMEORIGIN' - builder.withHeadersFile({ headers: [{ path: '/*', headers: [`${headerName}: ${headerValue}`] }] }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const { headers } = await got(`${server.url}/foo`) - t.is(headers[headerName.toLowerCase()], headerValue) - }) - }) - }) - - test(testName('should return response from a function with setTimeout', args), async (t) => { - await withSiteBuilder('site-with-set-timeout-function', async (builder) => { - builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({ - path: 'timeout.js', - handler: async () => { - console.log('ding') - // Wait for 4 seconds - const FUNCTION_TIMEOUT = 4e3 - await new Promise((resolve) => { - setTimeout(resolve, FUNCTION_TIMEOUT) - }) - return { - statusCode: 200, - body: 'ping', - metadata: { builder_function: true }, - } - }, - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got(`${server.url}/.netlify/functions/timeout`).text() - t.is(response, 'ping') - const builderResponse = await got(`${server.url}/.netlify/builders/timeout`).text() - t.is(builderResponse, 'ping') - }) - }) - }) - - test(testName('should fail when no metadata is set for builder function', args), async (t) => { - await withSiteBuilder('site-with-misconfigured-builder-function', async (builder) => { - builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({ - path: 'builder.js', - handler: async () => ({ - statusCode: 200, - body: 'ping', - }), - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got(`${server.url}/.netlify/functions/builder`) - t.is(response.body, 'ping') - t.is(response.statusCode, 200) - const builderResponse = await got(`${server.url}/.netlify/builders/builder`, { - throwHttpErrors: false, - }) - t.is( - builderResponse.body, - `{"message":"Function is not an on-demand builder. See https://ntl.fyi/create-builder for how to convert a function to a builder."}`, - ) - t.is(builderResponse.statusCode, 400) - }) - }) - }) - - test(testName('should serve function from a subdirectory', args), async (t) => { - await withSiteBuilder('site-with-from-subdirectory', async (builder) => { - builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({ - path: path.join('echo', 'echo.js'), - handler: async () => ({ - statusCode: 200, - body: 'ping', - metadata: { builder_function: true }, - }), - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got(`${server.url}/.netlify/functions/echo`).text() - t.is(response, 'ping') - const builderResponse = await got(`${server.url}/.netlify/builders/echo`).text() - t.is(builderResponse, 'ping') - }) - }) - }) - - test(testName('should pass .env.development vars to function', args), async (t) => { - await withSiteBuilder('site-with-env-development', async (builder) => { - builder - .withNetlifyToml({ config: { functions: { directory: 'functions' } } }) - .withEnvFile({ path: '.env.development', env: { TEST: 'FROM_DEV_FILE' } }) - .withFunction({ - path: 'env.js', - handler: async () => ({ - statusCode: 200, - body: `${process.env.TEST}`, - metadata: { builder_function: true }, - }), - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got(`${server.url}/.netlify/functions/env`).text() - t.is(response, 'FROM_DEV_FILE') - const builderResponse = await got(`${server.url}/.netlify/builders/env`).text() - t.is(builderResponse, 'FROM_DEV_FILE') - }) - }) - }) - - test(testName('should pass process env vars to function', args), async (t) => { - await withSiteBuilder('site-with-process-env', async (builder) => { - builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({ - path: 'env.js', - handler: async () => ({ - statusCode: 200, - body: `${process.env.TEST}`, - metadata: { builder_function: true }, - }), - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, env: { TEST: 'FROM_PROCESS_ENV' }, args }, async (server) => { - const response = await got(`${server.url}/.netlify/functions/env`).text() - t.is(response, 'FROM_PROCESS_ENV') - const builderResponse = await got(`${server.url}/.netlify/builders/env`).text() - t.is(builderResponse, 'FROM_PROCESS_ENV') - }) - }) - }) - - test(testName('should pass [build.environment] env vars to function', args), async (t) => { - await withSiteBuilder('site-with-build-environment', async (builder) => { - builder - .withNetlifyToml({ - config: { build: { environment: { TEST: 'FROM_CONFIG_FILE' } }, functions: { directory: 'functions' } }, - }) - .withFunction({ - path: 'env.js', - handler: async () => ({ - statusCode: 200, - body: `${process.env.TEST}`, - metadata: { builder_function: true }, - }), - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got(`${server.url}/.netlify/functions/env`).text() - t.is(response, 'FROM_CONFIG_FILE') - const builderResponse = await got(`${server.url}/.netlify/builders/env`).text() - t.is(builderResponse, 'FROM_CONFIG_FILE') - }) - }) - }) - - test(testName('[context.dev.environment] should override [build.environment]', args), async (t) => { - await withSiteBuilder('site-with-build-environment', async (builder) => { - builder - .withNetlifyToml({ - config: { - build: { environment: { TEST: 'DEFAULT_CONTEXT' } }, - context: { dev: { environment: { TEST: 'DEV_CONTEXT' } } }, - functions: { directory: 'functions' }, - }, - }) - .withFunction({ - path: 'env.js', - handler: async () => ({ - statusCode: 200, - body: `${process.env.TEST}`, - }), - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got(`${server.url}/.netlify/functions/env`).text() - t.is(response, 'DEV_CONTEXT') - }) - }) - }) - - test(testName('should use [build.environment] and not [context.production.environment]', args), async (t) => { - await withSiteBuilder('site-with-build-environment', async (builder) => { - builder - .withNetlifyToml({ - config: { - build: { environment: { TEST: 'DEFAULT_CONTEXT' } }, - context: { production: { environment: { TEST: 'PRODUCTION_CONTEXT' } } }, - functions: { directory: 'functions' }, - }, - }) - .withFunction({ - path: 'env.js', - handler: async () => ({ - statusCode: 200, - body: `${process.env.TEST}`, - }), - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got(`${server.url}/.netlify/functions/env`).text() - t.is(response, 'DEFAULT_CONTEXT') - }) - }) - }) - - test(testName('should override .env.development with process env', args), async (t) => { - await withSiteBuilder('site-with-override', async (builder) => { - builder - .withNetlifyToml({ config: { functions: { directory: 'functions' } } }) - .withEnvFile({ path: '.env.development', env: { TEST: 'FROM_DEV_FILE' } }) - .withFunction({ - path: 'env.js', - handler: async () => ({ - statusCode: 200, - body: `${process.env.TEST}`, - }), - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, env: { TEST: 'FROM_PROCESS_ENV' }, args }, async (server) => { - const response = await got(`${server.url}/.netlify/functions/env`).text() - t.is(response, 'FROM_PROCESS_ENV') - }) - }) - }) - - test(testName('should override [build.environment] with process env', args), async (t) => { - await withSiteBuilder('site-with-build-environment-override', async (builder) => { - builder - .withNetlifyToml({ - config: { build: { environment: { TEST: 'FROM_CONFIG_FILE' } }, functions: { directory: 'functions' } }, - }) - .withFunction({ - path: 'env.js', - handler: async () => ({ - statusCode: 200, - body: `${process.env.TEST}`, - }), - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, env: { TEST: 'FROM_PROCESS_ENV' }, args }, async (server) => { - const response = await got(`${server.url}/.netlify/functions/env`).text() - t.is(response, 'FROM_PROCESS_ENV') - }) - }) - }) - - test(testName('should override value of the NETLIFY_DEV env variable', args), async (t) => { - await withSiteBuilder('site-with-netlify-dev-override', async (builder) => { - builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({ - path: 'env.js', - handler: async () => ({ - statusCode: 200, - body: `${process.env.NETLIFY_DEV}`, - }), - }) - - await builder.buildAsync() - - await withDevServer( - { cwd: builder.directory, env: { NETLIFY_DEV: 'FROM_PROCESS_ENV' }, args }, - async (server) => { - const response = await got(`${server.url}/.netlify/functions/env`).text() - t.is(response, 'true') - }, - ) - }) - }) - - test(testName('should set value of the CONTEXT env variable', args), async (t) => { - await withSiteBuilder('site-with-context-override', async (builder) => { - builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({ - path: 'env.js', - handler: async () => ({ - statusCode: 200, - body: `${process.env.CONTEXT}`, - }), - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got(`${server.url}/.netlify/functions/env`).text() - t.is(response, 'dev') - }) - }) - }) - - test(testName('should redirect using a wildcard when set in netlify.toml', args), async (t) => { - await withSiteBuilder('site-with-redirect-function', async (builder) => { - builder - .withNetlifyToml({ - config: { - functions: { directory: 'functions' }, - redirects: [{ from: '/api/*', to: '/.netlify/functions/:splat', status: 200 }], - }, - }) - .withFunction({ - path: 'ping.js', - handler: async () => ({ - statusCode: 200, - body: 'ping', - }), - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got(`${server.url}/api/ping`).text() - t.is(response, 'ping') - }) - }) - }) - - test(testName('should pass undefined body to functions event for GET requests when redirecting', args), async (t) => { - await withSiteBuilder('site-with-get-echo-function', async (builder) => { - builder - .withNetlifyToml({ - config: { - functions: { directory: 'functions' }, - redirects: [{ from: '/api/*', to: '/.netlify/functions/:splat', status: 200 }], - }, - }) - .withFunction({ - path: 'echo.js', - handler: async (event) => ({ - statusCode: 200, - body: JSON.stringify(event), - }), - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got(`${server.url}/api/echo?ding=dong`).json() - t.is(response.body, undefined) - t.is(response.headers.host, `${server.host}:${server.port}`) - t.is(response.httpMethod, 'GET') - t.is(response.isBase64Encoded, true) - t.is(response.path, '/api/echo') - t.deepEqual(response.queryStringParameters, { ding: 'dong' }) - }) - }) - }) - - test(testName('should pass body to functions event for POST requests when redirecting', args), async (t) => { - await withSiteBuilder('site-with-post-echo-function', async (builder) => { - builder - .withNetlifyToml({ - config: { - functions: { directory: 'functions' }, - redirects: [{ from: '/api/*', to: '/.netlify/functions/:splat', status: 200 }], - }, - }) - .withFunction({ - path: 'echo.js', - handler: async (event) => ({ - statusCode: 200, - body: JSON.stringify(event), - }), - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got - .post(`${server.url}/api/echo?ding=dong`, { - headers: { - 'content-type': 'application/x-www-form-urlencoded', - }, - body: 'some=thing', - }) - .json() - - t.is(response.body, 'some=thing') - t.is(response.headers.host, `${server.host}:${server.port}`) - t.is(response.headers['content-type'], 'application/x-www-form-urlencoded') - t.is(response.headers['content-length'], '10') - t.is(response.httpMethod, 'POST') - t.is(response.isBase64Encoded, false) - t.is(response.path, '/api/echo') - t.deepEqual(response.queryStringParameters, { ding: 'dong' }) - }) - }) - }) - - test(testName('should return an empty body for a function with no body when redirecting', args), async (t) => { - await withSiteBuilder('site-with-no-body-function', async (builder) => { - builder - .withNetlifyToml({ - config: { - functions: { directory: 'functions' }, - redirects: [{ from: '/api/*', to: '/.netlify/functions/:splat', status: 200 }], - }, - }) - .withFunction({ - path: 'echo.js', - handler: async () => ({ - statusCode: 200, - }), - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got.post(`${server.url}/api/echo?ding=dong`, { - headers: { - 'content-type': 'application/x-www-form-urlencoded', - }, - body: 'some=thing', - }) - - t.is(response.body, '') - t.is(response.statusCode, 200) - }) - }) - }) - - test(testName('should handle multipart form data when redirecting', args), async (t) => { - await withSiteBuilder('site-with-multi-part-function', async (builder) => { - builder - .withNetlifyToml({ - config: { - functions: { directory: 'functions' }, - redirects: [{ from: '/api/*', to: '/.netlify/functions/:splat', status: 200 }], - }, - }) - .withFunction({ - path: 'echo.js', - handler: async (event) => ({ - statusCode: 200, - body: JSON.stringify(event), - }), - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const form = new FormData() - form.append('some', 'thing') - - const expectedBoundary = form.getBoundary() - const expectedResponseBody = form.getBuffer().toString('base64') - - const response = await got - .post(`${server.url}/api/echo?ding=dong`, { - body: form, - }) - .json() - - t.is(response.headers.host, `${server.host}:${server.port}`) - t.is(response.headers['content-type'], `multipart/form-data; boundary=${expectedBoundary}`) - t.is(response.headers['content-length'], '164') - t.is(response.httpMethod, 'POST') - t.is(response.isBase64Encoded, true) - t.is(response.path, '/api/echo') - t.deepEqual(response.queryStringParameters, { ding: 'dong' }) - t.is(response.body, expectedResponseBody) - }) - }) - }) - - test(testName('should return 404 when redirecting to a non existing function', args), async (t) => { - await withSiteBuilder('site-with-missing-function', async (builder) => { - builder.withNetlifyToml({ - config: { - functions: { directory: 'functions' }, - redirects: [{ from: '/api/*', to: '/.netlify/functions/:splat', status: 200 }], - }, - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got - .post(`${server.url}/api/none`, { - body: 'nothing', - }) - .catch((error) => error.response) - - t.is(response.statusCode, 404) - }) - }) - }) - - test(testName('should parse function query parameters using simple parsing', args), async (t) => { - await withSiteBuilder('site-with-multi-part-function', async (builder) => { - builder - .withNetlifyToml({ - config: { - functions: { directory: 'functions' }, - }, - }) - .withFunction({ - path: 'echo.js', - handler: async (event) => ({ - statusCode: 200, - body: JSON.stringify(event), - }), - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response1 = await got(`${server.url}/.netlify/functions/echo?category[SOMETHING][]=something`).json() - const response2 = await got(`${server.url}/.netlify/functions/echo?category=one&category=two`).json() - - t.deepEqual(response1.queryStringParameters, { 'category[SOMETHING][]': 'something' }) - t.deepEqual(response2.queryStringParameters, { category: 'one, two' }) - }) - }) - }) - - test(testName('should handle form submission', args), async (t) => { - await withSiteBuilder('site-with-form', async (builder) => { - builder - .withContentFile({ - path: 'index.html', - content: '

⊂◉‿◉つ

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

⊂◉‿◉つ

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

⊂◉‿◉つ

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

foo') - t.is(response2, '

foo') - t.is(response3, '

not-foo') - }) - }) - }) - - test(testName('should return 404.html if exists for non existing routes', args), async (t) => { - await withSiteBuilder('site-with-shadowing-404', async (builder) => { - builder.withContentFile({ - path: '404.html', - content: '

404 - Page not found

', - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got(`${server.url}/non-existent`, { throwHttpErrors: false }) - t.is(response.body, '

404 - Page not found

') - }) - }) - }) - - test(testName('should return 404.html from publish folder if exists for non existing routes', args), async (t) => { - await withSiteBuilder('site-with-shadowing-404-in-publish-folder', async (builder) => { - builder - .withContentFile({ - path: 'public/404.html', - content: '

404 - My Custom 404 Page

', - }) - .withNetlifyToml({ - config: { - build: { - publish: 'public/', - }, - }, - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got(`${server.url}/non-existent`, { throwHttpErrors: false }) - t.is(response.statusCode, 404) - t.is(response.body, '

404 - My Custom 404 Page

') - }) - }) - }) - - test(testName('should return 404 for redirect', args), async (t) => { - await withSiteBuilder('site-with-shadowing-404-redirect', async (builder) => { - builder - .withContentFile({ - path: 'foo.html', - content: '

foo', - }) - .withNetlifyToml({ - config: { - redirects: [{ from: '/test-404', to: '/foo', status: 404 }], - }, - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got(`${server.url}/test-404`, { throwHttpErrors: false }) - t.is(response.statusCode, 404) - t.is(response.body, '

foo') - }) - }) - }) - - test(testName('should ignore 404 redirect for existing file', args), async (t) => { - await withSiteBuilder('site-with-shadowing-404-redirect-existing', async (builder) => { - builder - .withContentFile({ - path: 'foo.html', - content: '

foo', - }) - .withContentFile({ - path: 'test-404.html', - content: '

This page actually exists', - }) - .withNetlifyToml({ - config: { - redirects: [{ from: '/test-404', to: '/foo', status: 404 }], - }, - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got(`${server.url}/test-404`) - - t.is(response.statusCode, 200) - t.is(response.body, '

This page actually exists') - }) - }) - }) - - test(testName('should follow 404 redirect even with existing file when force=true', args), async (t) => { - await withSiteBuilder('site-with-shadowing-404-redirect-force', async (builder) => { - builder - .withContentFile({ - path: 'foo.html', - content: '

foo', - }) - .withContentFile({ - path: 'test-404.html', - content: '

This page actually exists', - }) - .withNetlifyToml({ - config: { - redirects: [{ from: '/test-404', to: '/foo', status: 404, force: true }], - }, - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got(`${server.url}/test-404`, { throwHttpErrors: false }) - - t.is(response.statusCode, 404) - t.is(response.body, '

foo') - }) - }) - }) - - test(testName('should source redirects file from publish directory', args), async (t) => { - await withSiteBuilder('site-redirects-file-inside-publish', async (builder) => { - builder - .withContentFile({ - path: 'public/index.html', - content: 'index', - }) - .withRedirectsFile({ - pathPrefix: 'public', - redirects: [{ from: '/*', to: `/index.html`, status: 200 }], - }) - .withNetlifyToml({ - config: { - build: { publish: 'public' }, - }, - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got(`${server.url}/test`) - - t.is(response.statusCode, 200) - t.is(response.body, 'index') - }) - }) - }) - - test(testName('should redirect requests to an external server', args), async (t) => { - await withSiteBuilder('site-redirects-file-to-external', async (builder) => { - const externalServer = startExternalServer() - const { port } = externalServer.address() - builder.withRedirectsFile({ - redirects: [{ from: '/api/*', to: `http://localhost:${port}/:splat`, status: 200 }], - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const getResponse = await got(`${server.url}/api/ping`).json() - t.deepEqual(getResponse, { body: {}, method: 'GET', url: '/ping' }) - - const postResponse = await got - .post(`${server.url}/api/ping`, { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: 'param=value', - }) - .json() - t.deepEqual(postResponse, { body: { param: 'value' }, method: 'POST', url: '/ping' }) - }) - - externalServer.close() - }) - }) - - test(testName('should redirect POST request if content-type is missing', args), async (t) => { - await withSiteBuilder('site-with-post-no-content-type', async (builder) => { - builder.withNetlifyToml({ - config: { - functions: { directory: 'functions' }, - redirects: [{ from: '/api/*', to: '/other/:splat', status: 200 }], - }, - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const options = { - host: server.host, - port: server.port, - path: '/api/echo', - method: 'POST', - } - let data = '' - await new Promise((resolve) => { - const callback = (response) => { - response.on('data', (chunk) => { - data += chunk - }) - response.on('end', resolve) - } - const req = http.request(options, callback) - req.write('param=value') - req.end() - }) - - // we're testing Netlify Dev didn't crash - t.is(data, 'Method Not Allowed') - }) - }) - }) - - test(testName('should return .html file when file and folder have the same name', args), async (t) => { - await withSiteBuilder('site-with-same-name-for-file-and-folder', async (builder) => { - builder - .withContentFile({ - path: 'foo.html', - content: '

foo', - }) - .withContentFile({ - path: 'foo/file.html', - content: '

file in folder', - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got(`${server.url}/foo`) - - t.is(response.statusCode, 200) - t.is(response.body, '

foo') - }) - }) - }) - - test(testName('should not shadow an existing file that has unsafe URL characters', args), async (t) => { - await withSiteBuilder('site-with-unsafe-url-file-names', async (builder) => { - builder - .withContentFile({ - path: 'public/index.html', - content: 'index', - }) - .withContentFile({ - path: 'public/files/file with spaces.html', - content: 'file with spaces', - }) - .withContentFile({ - path: 'public/files/[file_with_brackets].html', - content: 'file with brackets', - }) - .withNetlifyToml({ - config: { - build: { publish: 'public' }, - redirects: [{ from: '/*', to: '/index.html', status: 200 }], - }, - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const [spaces, brackets] = await Promise.all([ - got(`${server.url}/files/file with spaces`).text(), - got(`${server.url}/files/[file_with_brackets]`).text(), - ]) - - t.is(spaces, 'file with spaces') - t.is(brackets, 'file with brackets') - }) - }) - }) - - test(testName('should follow redirect for fully qualified rule', args), async (t) => { - await withSiteBuilder('site-with-fully-qualified-redirect-rule', async (builder) => { - const publicDir = 'public' - builder - .withNetlifyToml({ - config: { - build: { publish: publicDir }, - }, - }) - .withContentFiles([ - { - path: path.join(publicDir, 'index.html'), - content: 'index', - }, - { - path: path.join(publicDir, 'local-hello.html'), - content: 'hello', - }, - ]) - .withRedirectsFile({ - redirects: [{ from: `http://localhost/hello-world`, to: `/local-hello`, status: 200 }], - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got(`${server.url}/hello-world`) - - t.is(response.statusCode, 200) - t.is(response.body, 'hello') - }) - }) - }) - - test(testName('should return 202 ok and empty response for background function', args), async (t) => { - await withSiteBuilder('site-with-background-function', async (builder) => { - builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({ - path: 'hello-background.js', - handler: () => { - console.log("Look at me I'm a background task") - }, - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got(`${server.url}/.netlify/functions/hello-background`) - t.is(response.statusCode, 202) - t.is(response.body, '') - }) - }) - }) - - test(testName('should enforce role based redirects with default secret and role path', args), async (t) => { - await withSiteBuilder('site-with-default-role-based-redirects', async (builder) => { - setupRoleBasedRedirectsSite(builder) - await builder.buildAsync() - await validateRoleBasedRedirectsSite({ builder, args, t }) - }) - }) - - test(testName('should enforce role based redirects with custom secret and role path', args), async (t) => { - await withSiteBuilder('site-with-custom-role-based-redirects', async (builder) => { - const jwtSecret = 'custom' - const jwtRolePath = 'roles' - setupRoleBasedRedirectsSite(builder).withNetlifyToml({ - config: { - dev: { - jwtSecret, - jwtRolePath, - }, - }, - }) - await builder.buildAsync() - await validateRoleBasedRedirectsSite({ builder, args, t, jwtSecret, jwtRolePath }) - }) - }) - - test(testName('routing-local-proxy serves edge handlers with --edgeHandlers flag', args), async (t) => { - await withSiteBuilder('site-with-fully-qualified-redirect-rule', async (builder) => { - const publicDir = 'public' - builder - .withNetlifyToml({ - config: { - build: { - publish: publicDir, - edge_handlers: 'netlify/edge-handlers', - }, - 'edge-handlers': [ - { - handler: 'smoke', - path: '/edge-handler', - }, - ], - }, - }) - .withContentFiles([ - { - path: path.join(publicDir, 'index.html'), - content: 'index', - }, - ]) - .withEdgeHandlers({ - fileName: 'smoke.js', - handlers: { - onRequest: (event) => { - event.replaceResponse( - // eslint-disable-next-line no-undef - new Response(null, { - headers: { - Location: 'https://google.com/', - }, - status: 301, - }), - ) - }, - }, - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args: [...args, '--edgeHandlers'] }, async (server) => { - const response = await got(`${server.url}/edge-handler`, { - followRedirect: false, - }) - - t.is(response.statusCode, 301) - t.is(response.headers.location, 'https://google.com/') - }) - }) - }) - - test(testName('routing-local-proxy serves edge handlers with deprecated --trafficMesh flag', args), async (t) => { - await withSiteBuilder('site-with-fully-qualified-redirect-rule', async (builder) => { - const publicDir = 'public' - builder - .withNetlifyToml({ - config: { - build: { - publish: publicDir, - edge_handlers: 'netlify/edge-handlers', - }, - 'edge-handlers': [ - { - handler: 'smoke', - path: '/edge-handler', - }, - ], - }, - }) - .withContentFiles([ - { - path: path.join(publicDir, 'index.html'), - content: 'index', - }, - ]) - .withEdgeHandlers({ - fileName: 'smoke.js', - handlers: { - onRequest: (event) => { - event.replaceResponse( - // eslint-disable-next-line no-undef - new Response(null, { - headers: { - Location: 'https://google.com/', - }, - status: 301, - }), - ) - }, - }, - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args: [...args, '--trafficMesh'] }, async (server) => { - const response = await got(`${server.url}/edge-handler`, { - followRedirect: false, - }) - - t.is(response.statusCode, 301) - t.is(response.headers.location, 'https://google.com/') - }) - }) - }) - - test(testName('routing-local-proxy builds projects w/o edge handlers', args), async (t) => { - await withSiteBuilder('site-with-fully-qualified-redirect-rule', async (builder) => { - const publicDir = 'public' - builder - .withNetlifyToml({ - config: { - build: { publish: publicDir }, - }, - }) - .withContentFiles([ - { - path: path.join(publicDir, 'index.html'), - content: 'index', - }, - ]) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args: [...args, '--edgeHandlers'] }, async (server) => { - const response = await got(`${server.url}/index.html`) - - t.is(response.statusCode, 200) - }) - }) - }) - - test(testName('redirect with country cookie', args), async (t) => { - await withSiteBuilder('site-with-country-cookie', async (builder) => { - builder - .withContentFiles([ - { - path: 'index.html', - content: 'index', - }, - { - path: 'index-es.html', - content: 'index in spanish', - }, - ]) - .withRedirectsFile({ - redirects: [{ from: `/`, to: `/index-es.html`, status: '200!', condition: 'Country=ES' }], - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got(`${server.url}/`, { - headers: { - cookie: `nf_country=ES`, - }, - }) - t.is(response.statusCode, 200) - t.is(response.body, 'index in spanish') - }) - }) - }) - - test(testName(`doesn't hang when sending a application/json POST request to function server`, args), async (t) => { - await withSiteBuilder('site-with-functions', async (builder) => { - const functionsPort = 6666 - await builder - .withNetlifyToml({ config: { functions: { directory: 'functions' }, dev: { functionsPort } } }) - .buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async ({ port, url }) => { - const response = await got(`${url.replace(port, functionsPort)}/test`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: '{}', - throwHttpErrors: false, - }) - t.is(response.statusCode, 404) - t.is(response.body, 'Function not found...') - }) - }) - }) - - test(testName(`catches invalid function names`, args), async (t) => { - await withSiteBuilder('site-with-functions', async (builder) => { - const functionsPort = 6667 - await builder - .withNetlifyToml({ config: { functions: { directory: 'functions' }, dev: { functionsPort } } }) - .withFunction({ - path: 'exclamat!on.js', - handler: async (event) => ({ - statusCode: 200, - body: JSON.stringify(event), - }), - }) - .buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async ({ port, url }) => { - const response = await got(`${url.replace(port, functionsPort)}/exclamat!on`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: '{}', - throwHttpErrors: false, - }) - t.is(response.statusCode, 400) - t.is(response.body, 'Function name should consist only of alphanumeric characters, hyphen & underscores.') - }) - }) - }) - - test(testName('should handle query params in redirects', args), async (t) => { - await withSiteBuilder('site-with-query-redirects', async (builder) => { - await builder - .withContentFile({ - path: 'public/index.html', - content: 'home', - }) - .withNetlifyToml({ - config: { - build: { publish: 'public' }, - functions: { directory: 'functions' }, - }, - }) - .withRedirectsFile({ - redirects: [ - { from: `/api/*`, to: `/.netlify/functions/echo?a=1&a=2`, status: '200' }, - { from: `/foo`, to: `/`, status: '302' }, - { from: `/bar`, to: `/?a=1&a=2`, status: '302' }, - { from: `/test id=:id`, to: `/?param=:id` }, - ], - }) - .withFunction({ - path: 'echo.js', - handler: async (event) => ({ - statusCode: 200, - body: JSON.stringify(event), - }), - }) - .buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const [fromFunction, queryPassthrough, queryInRedirect, withParamMatching] = await Promise.all([ - got(`${server.url}/api/test?foo=1&foo=2&bar=1&bar=2`).json(), - got(`${server.url}/foo?foo=1&foo=2&bar=1&bar=2`, { followRedirect: false }), - got(`${server.url}/bar?foo=1&foo=2&bar=1&bar=2`, { followRedirect: false }), - got(`${server.url}/test?id=1`, { followRedirect: false }), - ]) - - // query params should be taken from the request - t.deepEqual(fromFunction.multiValueQueryStringParameters, { foo: ['1', '2'], bar: ['1', '2'] }) - - // query params should be passed through from the request - t.is(queryPassthrough.headers.location, '/?foo=1&foo=2&bar=1&bar=2') - - // query params should be taken from the redirect rule - t.is(queryInRedirect.headers.location, '/?a=1&a=2') - - // query params should be taken from the redirect rule - t.is(withParamMatching.headers.location, '/?param=1') - }) - }) - }) - - test(testName('Should not use the ZISI function bundler if not using esbuild', args), async (t) => { - await withSiteBuilder('site-with-esm-function', async (builder) => { - builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withContentFile({ - path: path.join('functions', 'esm-function', 'esm-function.js'), - content: ` -export async function handler(event, context) { - return { - statusCode: 200, - body: 'esm', - }; -} - `, - }) - - await builder.buildAsync() - - await t.throwsAsync(() => - withDevServer({ cwd: builder.directory, args }, async (server) => - got(`${server.url}/.netlify/functions/esm-function`).text(), - ), - ) - }) - }) - - test(testName('Should use the ZISI function bundler and serve ESM functions if using esbuild', args), async (t) => { - await withSiteBuilder('site-with-esm-function', async (builder) => { - builder - .withNetlifyToml({ config: { functions: { directory: 'functions', node_bundler: 'esbuild' } } }) - .withContentFile({ - path: path.join('functions', 'esm-function', 'esm-function.js'), - content: ` -export async function handler(event, context) { - return { - statusCode: 200, - body: 'esm', - }; -} - `, - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got(`${server.url}/.netlify/functions/esm-function`).text() - t.is(response, 'esm') - }) - }) - }) - - test( - testName('Should use the ZISI function bundler and serve TypeScript functions if using esbuild', args), - async (t) => { - await withSiteBuilder('site-with-ts-function', async (builder) => { - builder - .withNetlifyToml({ config: { functions: { directory: 'functions', node_bundler: 'esbuild' } } }) - .withContentFile({ - path: path.join('functions', 'ts-function', 'ts-function.ts'), - content: ` -type CustomResponse = string; - -export const handler = async function () { - const response: CustomResponse = "ts"; - - return { - statusCode: 200, - body: response, - }; -}; - - `, - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got(`${server.url}/.netlify/functions/ts-function`).text() - t.is(response, 'ts') - }) - }) - }, - ) - - test( - testName('Should use the ZISI function bundler and serve TypeScript functions if not using esbuild', args), - async (t) => { - await withSiteBuilder('site-with-ts-function', async (builder) => { - builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withContentFile({ - path: path.join('functions', 'ts-function', 'ts-function.ts'), - content: ` -type CustomResponse = string; - -export const handler = async function () { - const response: CustomResponse = "ts"; - - return { - statusCode: 200, - body: response, - }; -}; - - `, - }) - - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got(`${server.url}/.netlify/functions/ts-function`).text() - t.is(response, 'ts') - }) - }) - }, - ) - - test(testName(`should start https server when https dev block is configured`, args), async (t) => { - await withSiteBuilder('sites-with-https-certificate', async (builder) => { - await builder - .withNetlifyToml({ - config: { - build: { publish: 'public' }, - functions: { directory: 'functions' }, - dev: { https: { certFile: 'cert.pem', keyFile: 'key.pem' } }, - }, - }) - .withContentFile({ - path: 'public/index.html', - content: 'index', - }) - .withRedirectsFile({ - redirects: [{ from: `/api/*`, to: `/.netlify/functions/:splat`, status: '200' }], - }) - .withFunction({ - path: 'hello.js', - handler: async () => ({ - statusCode: 200, - body: 'Hello World', - }), - }) - .buildAsync() - - await Promise.all([ - copyFile(`${__dirname}/assets/cert.pem`, `${builder.directory}/cert.pem`), - copyFile(`${__dirname}/assets/key.pem`, `${builder.directory}/key.pem`), - ]) - await withDevServer({ cwd: builder.directory, args }, async ({ port }) => { - const options = { https: { rejectUnauthorized: false } } - t.is(await got(`https://localhost:${port}`, options).text(), 'index') - t.is(await got(`https://localhost:${port}/api/hello`, options).text(), 'Hello World') - }) - }) - }) - - test(testName(`should use custom functions timeouts`, args), async (t) => { - await withSiteBuilder('site-with-custom-functions-timeout', async (builder) => { - await builder - .withNetlifyToml({ - config: { - build: { publish: 'public' }, - functions: { directory: 'functions' }, - }, - }) - .withFunction({ - path: 'hello.js', - handler: async () => { - await new Promise((resolve) => { - const SLEEP_TIME = 2000 - setTimeout(resolve, SLEEP_TIME) - }) - return { - statusCode: 200, - body: 'Hello World', - } - }, - }) - .buildAsync() - - const siteInfo = { - account_slug: 'test-account', - id: 'site_id', - name: 'site-name', - functions_config: { timeout: 1 }, - } - - const routes = [ - { path: 'sites/site_id', response: siteInfo }, - - { path: 'sites/site_id/service-instances', response: [] }, - { - path: 'accounts', - response: [{ slug: siteInfo.account_slug }], - }, - ] - - await withMockApi(routes, async ({ apiUrl }) => { - await withDevServer( - { - cwd: builder.directory, - offline: false, - env: { - NETLIFY_API_URL: apiUrl, - NETLIFY_SITE_ID: 'site_id', - NETLIFY_AUTH_TOKEN: 'fake-token', - }, - }, - async ({ url }) => { - const error = await t.throwsAsync(() => got(`${url}/.netlify/functions/hello`)) - t.true(error.response.body.includes('TimeoutError: Task timed out after 1.00 seconds')) - }, - ) - }) - }) - }) - - // we need curl to reproduce this issue - if (os.platform() !== 'win32') { - test(testName(`don't hang on 'Expect: 100-continue' header`, args), async () => { - await withSiteBuilder('site-with-expect-header', async (builder) => { - await builder - .withNetlifyToml({ - config: { - functions: { directory: 'functions' }, - }, - }) - .withFunction({ - path: 'hello.js', - handler: async () => ({ statusCode: 200, body: 'Hello' }), - }) - .buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - await curl(`${server.url}/.netlify/functions/hello`, [ - '-i', - '-v', - '-d', - '{"somefield":"somevalue"}', - '-H', - 'Content-Type: application/json', - '-H', - `Expect: 100-continue' header`, - ]) - }) - }) - }) - } - - test(testName(`serves non ascii static files correctly`, args), async (t) => { - await withSiteBuilder('site-with-non-ascii-files', async (builder) => { - await builder - .withContentFile({ - path: 'public/范.txt', - content: 'success', - }) - .withNetlifyToml({ - config: { - build: { publish: 'public' }, - redirects: [{ from: '/*', to: '/index.html', status: 200 }], - }, - }) - .buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got(`${server.url}/${encodeURIComponent('范.txt')}`) - t.is(response.body, 'success') - }) - }) - }) - - test(testName(`returns headers set by function`, args), async (t) => { - await withSiteBuilder('site-with-function-with-custom-headers', async (builder) => { - await builder - .withFunction({ - pathPrefix: 'netlify/functions', - path: 'custom-headers.js', - handler: async () => ({ - statusCode: 200, - body: '', - headers: { 'single-value-header': 'custom-value' }, - multiValueHeaders: { 'multi-value-header': ['custom-value1', 'custom-value2'] }, - metadata: { builder_function: true }, - }), - }) - .buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const response = await got(`${server.url}/.netlify/functions/custom-headers`) - t.is(response.headers['single-value-header'], 'custom-value') - t.is(response.headers['multi-value-header'], 'custom-value1, custom-value2') - const builderResponse = await got(`${server.url}/.netlify/builders/custom-headers`) - t.is(builderResponse.headers['single-value-header'], 'custom-value') - t.is(builderResponse.headers['multi-value-header'], 'custom-value1, custom-value2') - }) - }) - }) - - test(testName('should match redirect when path is URL encoded', args), async (t) => { - await withSiteBuilder('site-with-encoded-redirect', async (builder) => { - await builder - .withContentFile({ path: 'static/special[test].txt', content: `special` }) - .withRedirectsFile({ redirects: [{ from: '/_next/static/*', to: '/static/:splat', status: 200 }] }) - .buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - const [response1, response2] = await Promise.all([ - got(`${server.url}/_next/static/special[test].txt`).text(), - got(`${server.url}/_next/static/special%5Btest%5D.txt`).text(), - ]) - t.is(response1, 'special') - t.is(response2, 'special') - }) - }) - }) - - test(testName(`should not redirect POST request to functions server when it doesn't exists`, args), async (t) => { - await withSiteBuilder('site-with-post-request', async (builder) => { - await builder.buildAsync() - - await withDevServer({ cwd: builder.directory, args }, async (server) => { - // an error is expected since we're sending a POST request to a static server - // the important thing is that it's not proxied to the functions server - const error = await t.throwsAsync(() => - got.post(`${server.url}/api/test`, { - headers: { - 'content-type': 'application/x-www-form-urlencoded', - }, - body: 'some=thing', - }), - ) - - t.is(error.message, 'Response code 405 (Method Not Allowed)') - }) - }) - }) -}) -/* eslint-enable require-await */ diff --git a/tests/integration/0.command.dev.test.js b/tests/integration/0.command.dev.test.js new file mode 100644 index 00000000000..37238b4dee0 --- /dev/null +++ b/tests/integration/0.command.dev.test.js @@ -0,0 +1,297 @@ +// Handlers are meant to be async outside tests +const http = require('http') + +// eslint-disable-next-line ava/use-test +const avaTest = require('ava') +const { isCI } = require('ci-info') + +const { withDevServer } = require('./utils/dev-server') +const { startExternalServer } = require('./utils/external-server') +const got = require('./utils/got') +const { withSiteBuilder } = require('./utils/site-builder') + +const test = isCI ? avaTest.serial.bind(avaTest) : avaTest + +const testMatrix = [ + { args: [] }, + + // some tests are still failing with this enabled + // { args: ['--edgeHandlers'] } +] + +const testName = (title, args) => (args.length <= 0 ? title : `${title} - ${args.join(' ')}`) + +testMatrix.forEach(({ args }) => { + test(testName('should return 404.html if exists for non existing routes', args), async (t) => { + await withSiteBuilder('site-with-shadowing-404', async (builder) => { + builder.withContentFile({ + path: '404.html', + content: '

404 - Page not found

', + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got(`${server.url}/non-existent`, { throwHttpErrors: false }) + t.is(response.body, '

404 - Page not found

') + }) + }) + }) + + test(testName('should return 404.html from publish folder if exists for non existing routes', args), async (t) => { + await withSiteBuilder('site-with-shadowing-404-in-publish-folder', async (builder) => { + builder + .withContentFile({ + path: 'public/404.html', + content: '

404 - My Custom 404 Page

', + }) + .withNetlifyToml({ + config: { + build: { + publish: 'public/', + }, + }, + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got(`${server.url}/non-existent`, { throwHttpErrors: false }) + t.is(response.statusCode, 404) + t.is(response.body, '

404 - My Custom 404 Page

') + }) + }) + }) + + test(testName('should return 404 for redirect', args), async (t) => { + await withSiteBuilder('site-with-shadowing-404-redirect', async (builder) => { + builder + .withContentFile({ + path: 'foo.html', + content: '

foo', + }) + .withNetlifyToml({ + config: { + redirects: [{ from: '/test-404', to: '/foo', status: 404 }], + }, + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got(`${server.url}/test-404`, { throwHttpErrors: false }) + t.is(response.statusCode, 404) + t.is(response.body, '

foo') + }) + }) + }) + + test(testName('should ignore 404 redirect for existing file', args), async (t) => { + await withSiteBuilder('site-with-shadowing-404-redirect-existing', async (builder) => { + builder + .withContentFile({ + path: 'foo.html', + content: '

foo', + }) + .withContentFile({ + path: 'test-404.html', + content: '

This page actually exists', + }) + .withNetlifyToml({ + config: { + redirects: [{ from: '/test-404', to: '/foo', status: 404 }], + }, + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got(`${server.url}/test-404`) + + t.is(response.statusCode, 200) + t.is(response.body, '

This page actually exists') + }) + }) + }) + + test(testName('should follow 404 redirect even with existing file when force=true', args), async (t) => { + await withSiteBuilder('site-with-shadowing-404-redirect-force', async (builder) => { + builder + .withContentFile({ + path: 'foo.html', + content: '

foo', + }) + .withContentFile({ + path: 'test-404.html', + content: '

This page actually exists', + }) + .withNetlifyToml({ + config: { + redirects: [{ from: '/test-404', to: '/foo', status: 404, force: true }], + }, + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got(`${server.url}/test-404`, { throwHttpErrors: false }) + + t.is(response.statusCode, 404) + t.is(response.body, '

foo') + }) + }) + }) + + test(testName('should source redirects file from publish directory', args), async (t) => { + await withSiteBuilder('site-redirects-file-inside-publish', async (builder) => { + builder + .withContentFile({ + path: 'public/index.html', + content: 'index', + }) + .withRedirectsFile({ + pathPrefix: 'public', + redirects: [{ from: '/*', to: `/index.html`, status: 200 }], + }) + .withNetlifyToml({ + config: { + build: { publish: 'public' }, + }, + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got(`${server.url}/test`) + + t.is(response.statusCode, 200) + t.is(response.body, 'index') + }) + }) + }) + + test(testName('should redirect requests to an external server', args), async (t) => { + await withSiteBuilder('site-redirects-file-to-external', async (builder) => { + const externalServer = startExternalServer() + const { port } = externalServer.address() + builder.withRedirectsFile({ + redirects: [{ from: '/api/*', to: `http://localhost:${port}/:splat`, status: 200 }], + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const getResponse = await got(`${server.url}/api/ping`).json() + t.deepEqual(getResponse, { body: {}, method: 'GET', url: '/ping' }) + + const postResponse = await got + .post(`${server.url}/api/ping`, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: 'param=value', + }) + .json() + t.deepEqual(postResponse, { body: { param: 'value' }, method: 'POST', url: '/ping' }) + }) + + externalServer.close() + }) + }) + + test(testName('should redirect POST request if content-type is missing', args), async (t) => { + await withSiteBuilder('site-with-post-no-content-type', async (builder) => { + builder.withNetlifyToml({ + config: { + functions: { directory: 'functions' }, + redirects: [{ from: '/api/*', to: '/other/:splat', status: 200 }], + }, + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const options = { + host: server.host, + port: server.port, + path: '/api/echo', + method: 'POST', + } + let data = '' + await new Promise((resolve) => { + const callback = (response) => { + response.on('data', (chunk) => { + data += chunk + }) + response.on('end', resolve) + } + const req = http.request(options, callback) + req.write('param=value') + req.end() + }) + + // we're testing Netlify Dev didn't crash + t.is(data, 'Method Not Allowed') + }) + }) + }) + + test(testName('should return .html file when file and folder have the same name', args), async (t) => { + await withSiteBuilder('site-with-same-name-for-file-and-folder', async (builder) => { + builder + .withContentFile({ + path: 'foo.html', + content: '

foo', + }) + .withContentFile({ + path: 'foo/file.html', + content: '

file in folder', + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got(`${server.url}/foo`) + + t.is(response.statusCode, 200) + t.is(response.body, '

foo') + }) + }) + }) + + test(testName('should not shadow an existing file that has unsafe URL characters', args), async (t) => { + await withSiteBuilder('site-with-unsafe-url-file-names', async (builder) => { + builder + .withContentFile({ + path: 'public/index.html', + content: 'index', + }) + .withContentFile({ + path: 'public/files/file with spaces.html', + content: 'file with spaces', + }) + .withContentFile({ + path: 'public/files/[file_with_brackets].html', + content: 'file with brackets', + }) + .withNetlifyToml({ + config: { + build: { publish: 'public' }, + redirects: [{ from: '/*', to: '/index.html', status: 200 }], + }, + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const [spaces, brackets] = await Promise.all([ + got(`${server.url}/files/file with spaces`).text(), + got(`${server.url}/files/[file_with_brackets]`).text(), + ]) + + t.is(spaces, 'file with spaces') + t.is(brackets, 'file with brackets') + }) + }) + }) +}) diff --git a/tests/command.addons.test.js b/tests/integration/10.command.addons.test.js similarity index 100% rename from tests/command.addons.test.js rename to tests/integration/10.command.addons.test.js diff --git a/tests/integration/100.command.dev.test.js b/tests/integration/100.command.dev.test.js new file mode 100644 index 00000000000..38c9593f6f9 --- /dev/null +++ b/tests/integration/100.command.dev.test.js @@ -0,0 +1,376 @@ +// Handlers are meant to be async outside tests +/* eslint-disable require-await */ +const path = require('path') + +// eslint-disable-next-line ava/use-test +const avaTest = require('ava') +const { isCI } = require('ci-info') +const dotProp = require('dot-prop') +const jwt = require('jsonwebtoken') + +const { withDevServer } = require('./utils/dev-server') +const got = require('./utils/got') +const { withSiteBuilder } = require('./utils/site-builder') + +const test = isCI ? avaTest.serial.bind(avaTest) : avaTest + +const testMatrix = [ + { args: [] }, + + // some tests are still failing with this enabled + // { args: ['--edgeHandlers'] } +] + +const testName = (title, args) => (args.length <= 0 ? title : `${title} - ${args.join(' ')}`) + +const JWT_EXPIRY = 1_893_456_000 +const getToken = ({ jwtRolePath = 'app_metadata.authorization.roles', jwtSecret = 'secret', roles }) => { + const payload = { + exp: JWT_EXPIRY, + sub: '12345678', + } + return jwt.sign(dotProp.set(payload, jwtRolePath, roles), jwtSecret) +} + +const setupRoleBasedRedirectsSite = (builder) => { + builder + .withContentFiles([ + { + path: 'index.html', + content: 'index', + }, + { + path: 'admin/foo.html', + content: 'foo', + }, + ]) + .withRedirectsFile({ + redirects: [{ from: `/admin/*`, to: ``, status: '200!', condition: 'Role=admin' }], + }) + return builder +} + +const validateRoleBasedRedirectsSite = async ({ args, builder, jwtRolePath, jwtSecret, t }) => { + const adminToken = getToken({ jwtSecret, jwtRolePath, roles: ['admin'] }) + const editorToken = getToken({ jwtSecret, jwtRolePath, roles: ['editor'] }) + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const unauthenticatedResponse = await got(`${server.url}/admin`, { throwHttpErrors: false }) + t.is(unauthenticatedResponse.statusCode, 404) + t.is(unauthenticatedResponse.body, 'Not Found') + + const authenticatedResponse = await got(`${server.url}/admin/foo`, { + headers: { + cookie: `nf_jwt=${adminToken}`, + }, + }) + t.is(authenticatedResponse.statusCode, 200) + t.is(authenticatedResponse.body, 'foo') + + const wrongRoleResponse = await got(`${server.url}/admin/foo`, { + headers: { + cookie: `nf_jwt=${editorToken}`, + }, + throwHttpErrors: false, + }) + t.is(wrongRoleResponse.statusCode, 404) + t.is(wrongRoleResponse.body, 'Not Found') + }) +} + +testMatrix.forEach(({ args }) => { + test(testName('should follow redirect for fully qualified rule', args), async (t) => { + await withSiteBuilder('site-with-fully-qualified-redirect-rule', async (builder) => { + const publicDir = 'public' + builder + .withNetlifyToml({ + config: { + build: { publish: publicDir }, + }, + }) + .withContentFiles([ + { + path: path.join(publicDir, 'index.html'), + content: 'index', + }, + { + path: path.join(publicDir, 'local-hello.html'), + content: 'hello', + }, + ]) + .withRedirectsFile({ + redirects: [{ from: `http://localhost/hello-world`, to: `/local-hello`, status: 200 }], + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got(`${server.url}/hello-world`) + + t.is(response.statusCode, 200) + t.is(response.body, 'hello') + }) + }) + }) + + test(testName('should return 202 ok and empty response for background function', args), async (t) => { + await withSiteBuilder('site-with-background-function', async (builder) => { + builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({ + path: 'hello-background.js', + handler: () => { + console.log("Look at me I'm a background task") + }, + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got(`${server.url}/.netlify/functions/hello-background`) + t.is(response.statusCode, 202) + t.is(response.body, '') + }) + }) + }) + + test(testName('should enforce role based redirects with default secret and role path', args), async (t) => { + await withSiteBuilder('site-with-default-role-based-redirects', async (builder) => { + setupRoleBasedRedirectsSite(builder) + await builder.buildAsync() + await validateRoleBasedRedirectsSite({ builder, args, t }) + }) + }) + + test(testName('should enforce role based redirects with custom secret and role path', args), async (t) => { + await withSiteBuilder('site-with-custom-role-based-redirects', async (builder) => { + const jwtSecret = 'custom' + const jwtRolePath = 'roles' + setupRoleBasedRedirectsSite(builder).withNetlifyToml({ + config: { + dev: { + jwtSecret, + jwtRolePath, + }, + }, + }) + await builder.buildAsync() + await validateRoleBasedRedirectsSite({ builder, args, t, jwtSecret, jwtRolePath }) + }) + }) + + test(testName('routing-local-proxy serves edge handlers with --edgeHandlers flag', args), async (t) => { + await withSiteBuilder('site-with-fully-qualified-redirect-rule', async (builder) => { + const publicDir = 'public' + builder + .withNetlifyToml({ + config: { + build: { + publish: publicDir, + edge_handlers: 'netlify/edge-handlers', + }, + 'edge-handlers': [ + { + handler: 'smoke', + path: '/edge-handler', + }, + ], + }, + }) + .withContentFiles([ + { + path: path.join(publicDir, 'index.html'), + content: 'index', + }, + ]) + .withEdgeHandlers({ + fileName: 'smoke.js', + handlers: { + onRequest: (event) => { + event.replaceResponse( + // eslint-disable-next-line no-undef + new Response(null, { + headers: { + Location: 'https://google.com/', + }, + status: 301, + }), + ) + }, + }, + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args: [...args, '--edgeHandlers'] }, async (server) => { + const response = await got(`${server.url}/edge-handler`, { + followRedirect: false, + }) + + t.is(response.statusCode, 301) + t.is(response.headers.location, 'https://google.com/') + }) + }) + }) + + test(testName('routing-local-proxy serves edge handlers with deprecated --trafficMesh flag', args), async (t) => { + await withSiteBuilder('site-with-fully-qualified-redirect-rule', async (builder) => { + const publicDir = 'public' + builder + .withNetlifyToml({ + config: { + build: { + publish: publicDir, + edge_handlers: 'netlify/edge-handlers', + }, + 'edge-handlers': [ + { + handler: 'smoke', + path: '/edge-handler', + }, + ], + }, + }) + .withContentFiles([ + { + path: path.join(publicDir, 'index.html'), + content: 'index', + }, + ]) + .withEdgeHandlers({ + fileName: 'smoke.js', + handlers: { + onRequest: (event) => { + event.replaceResponse( + // eslint-disable-next-line no-undef + new Response(null, { + headers: { + Location: 'https://google.com/', + }, + status: 301, + }), + ) + }, + }, + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args: [...args, '--trafficMesh'] }, async (server) => { + const response = await got(`${server.url}/edge-handler`, { + followRedirect: false, + }) + + t.is(response.statusCode, 301) + t.is(response.headers.location, 'https://google.com/') + }) + }) + }) + + test(testName('routing-local-proxy builds projects w/o edge handlers', args), async (t) => { + await withSiteBuilder('site-with-fully-qualified-redirect-rule', async (builder) => { + const publicDir = 'public' + builder + .withNetlifyToml({ + config: { + build: { publish: publicDir }, + }, + }) + .withContentFiles([ + { + path: path.join(publicDir, 'index.html'), + content: 'index', + }, + ]) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args: [...args, '--edgeHandlers'] }, async (server) => { + const response = await got(`${server.url}/index.html`) + + t.is(response.statusCode, 200) + }) + }) + }) + + test(testName('redirect with country cookie', args), async (t) => { + await withSiteBuilder('site-with-country-cookie', async (builder) => { + builder + .withContentFiles([ + { + path: 'index.html', + content: 'index', + }, + { + path: 'index-es.html', + content: 'index in spanish', + }, + ]) + .withRedirectsFile({ + redirects: [{ from: `/`, to: `/index-es.html`, status: '200!', condition: 'Country=ES' }], + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got(`${server.url}/`, { + headers: { + cookie: `nf_country=ES`, + }, + }) + t.is(response.statusCode, 200) + t.is(response.body, 'index in spanish') + }) + }) + }) + + test(testName(`doesn't hang when sending a application/json POST request to function server`, args), async (t) => { + await withSiteBuilder('site-with-functions', async (builder) => { + const functionsPort = 6666 + await builder + .withNetlifyToml({ config: { functions: { directory: 'functions' }, dev: { functionsPort } } }) + .buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async ({ port, url }) => { + const response = await got(`${url.replace(port, functionsPort)}/test`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: '{}', + throwHttpErrors: false, + }) + t.is(response.statusCode, 404) + t.is(response.body, 'Function not found...') + }) + }) + }) + + test(testName(`catches invalid function names`, args), async (t) => { + await withSiteBuilder('site-with-functions', async (builder) => { + const functionsPort = 6667 + await builder + .withNetlifyToml({ config: { functions: { directory: 'functions' }, dev: { functionsPort } } }) + .withFunction({ + path: 'exclamat!on.js', + handler: async (event) => ({ + statusCode: 200, + body: JSON.stringify(event), + }), + }) + .buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async ({ port, url }) => { + const response = await got(`${url.replace(port, functionsPort)}/exclamat!on`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: '{}', + throwHttpErrors: false, + }) + t.is(response.statusCode, 400) + t.is(response.body, 'Function name should consist only of alphanumeric characters, hyphen & underscores.') + }) + }) + }) +}) +/* eslint-enable require-await */ diff --git a/tests/command.build.test.js b/tests/integration/110.command.build.test.js similarity index 100% rename from tests/command.build.test.js rename to tests/integration/110.command.build.test.js diff --git a/tests/command.status.test.js b/tests/integration/120.command.status.test.js similarity index 100% rename from tests/command.status.test.js rename to tests/integration/120.command.status.test.js diff --git a/tests/eleventy.test.js b/tests/integration/130.eleventy.test.js similarity index 100% rename from tests/eleventy.test.js rename to tests/integration/130.eleventy.test.js diff --git a/tests/command.functions.test.js b/tests/integration/20.command.functions.test.js similarity index 98% rename from tests/command.functions.test.js rename to tests/integration/20.command.functions.test.js index a740eaa4e88..23638ed48f2 100644 --- a/tests/command.functions.test.js +++ b/tests/integration/20.command.functions.test.js @@ -1,11 +1,13 @@ // Handlers are meant to be async outside tests /* eslint-disable require-await */ -const test = require('ava') +// eslint-disable-next-line ava/use-test +const avaTest = require('ava') +const { isCI } = require('ci-info') const execa = require('execa') const getPort = require('get-port') const waitPort = require('wait-port') -const fs = require('../src/lib/fs') +const fs = require('../../src/lib/fs') const callCli = require('./utils/call-cli') const cliPath = require('./utils/cli-path') @@ -17,6 +19,8 @@ const { pause } = require('./utils/pause') const { killProcess } = require('./utils/process') const { withSiteBuilder } = require('./utils/site-builder') +const test = isCI ? avaTest.serial.bind(avaTest) : avaTest + test('should return function response when invoked with no identity argument', async (t) => { await withSiteBuilder('function-invoke-with-no-identity-argument', async (builder) => { builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({ @@ -492,7 +496,7 @@ const withFunctionsServer = async ({ builder, args = [], port = DEFAULT_PORT }, } } -test.skip('should serve functions on default port', async (t) => { +test('should serve functions on default port', async (t) => { await withSiteBuilder('site-with-ping-function', async (builder) => { await builder .withNetlifyToml({ config: { functions: { directory: 'functions' } } }) @@ -512,7 +516,7 @@ test.skip('should serve functions on default port', async (t) => { }) }) -test.skip('should serve functions on custom port', async (t) => { +test('should serve functions on custom port', async (t) => { await withSiteBuilder('site-with-ping-function', async (builder) => { await builder .withNetlifyToml({ config: { functions: { directory: 'functions' } } }) @@ -533,7 +537,7 @@ test.skip('should serve functions on custom port', async (t) => { }) }) -test.skip('should use settings from netlify.toml dev', async (t) => { +test('should use settings from netlify.toml dev', async (t) => { await withSiteBuilder('site-with-ping-function', async (builder) => { const port = await getPort() await builder diff --git a/tests/integration/200.command.dev.test.js b/tests/integration/200.command.dev.test.js new file mode 100644 index 00000000000..fd884f03eb5 --- /dev/null +++ b/tests/integration/200.command.dev.test.js @@ -0,0 +1,414 @@ +// Handlers are meant to be async outside tests +/* eslint-disable require-await */ +const { copyFile } = require('fs').promises +const os = require('os') +const path = require('path') + +// eslint-disable-next-line ava/use-test +const avaTest = require('ava') +const { isCI } = require('ci-info') + +const { curl } = require('./utils/curl') +const { withDevServer } = require('./utils/dev-server') +const got = require('./utils/got') +const { withMockApi } = require('./utils/mock-api') +const { withSiteBuilder } = require('./utils/site-builder') + +const test = isCI ? avaTest.serial.bind(avaTest) : avaTest + +const testMatrix = [ + { args: [] }, + + // some tests are still failing with this enabled + // { args: ['--edgeHandlers'] } +] + +const testName = (title, args) => (args.length <= 0 ? title : `${title} - ${args.join(' ')}`) + +testMatrix.forEach(({ args }) => { + test(testName('should handle query params in redirects', args), async (t) => { + await withSiteBuilder('site-with-query-redirects', async (builder) => { + await builder + .withContentFile({ + path: 'public/index.html', + content: 'home', + }) + .withNetlifyToml({ + config: { + build: { publish: 'public' }, + functions: { directory: 'functions' }, + }, + }) + .withRedirectsFile({ + redirects: [ + { from: `/api/*`, to: `/.netlify/functions/echo?a=1&a=2`, status: '200' }, + { from: `/foo`, to: `/`, status: '302' }, + { from: `/bar`, to: `/?a=1&a=2`, status: '302' }, + { from: `/test id=:id`, to: `/?param=:id` }, + ], + }) + .withFunction({ + path: 'echo.js', + handler: async (event) => ({ + statusCode: 200, + body: JSON.stringify(event), + }), + }) + .buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const [fromFunction, queryPassthrough, queryInRedirect, withParamMatching] = await Promise.all([ + got(`${server.url}/api/test?foo=1&foo=2&bar=1&bar=2`).json(), + got(`${server.url}/foo?foo=1&foo=2&bar=1&bar=2`, { followRedirect: false }), + got(`${server.url}/bar?foo=1&foo=2&bar=1&bar=2`, { followRedirect: false }), + got(`${server.url}/test?id=1`, { followRedirect: false }), + ]) + + // query params should be taken from the request + t.deepEqual(fromFunction.multiValueQueryStringParameters, { foo: ['1', '2'], bar: ['1', '2'] }) + + // query params should be passed through from the request + t.is(queryPassthrough.headers.location, '/?foo=1&foo=2&bar=1&bar=2') + + // query params should be taken from the redirect rule + t.is(queryInRedirect.headers.location, '/?a=1&a=2') + + // query params should be taken from the redirect rule + t.is(withParamMatching.headers.location, '/?param=1') + }) + }) + }) + + test(testName('Should not use the ZISI function bundler if not using esbuild', args), async (t) => { + await withSiteBuilder('site-with-esm-function', async (builder) => { + builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withContentFile({ + path: path.join('functions', 'esm-function', 'esm-function.js'), + content: ` +export async function handler(event, context) { + return { + statusCode: 200, + body: 'esm', + }; +} + `, + }) + + await builder.buildAsync() + + await t.throwsAsync(() => + withDevServer({ cwd: builder.directory, args }, async (server) => + got(`${server.url}/.netlify/functions/esm-function`).text(), + ), + ) + }) + }) + + test(testName('Should use the ZISI function bundler and serve ESM functions if using esbuild', args), async (t) => { + await withSiteBuilder('site-with-esm-function', async (builder) => { + builder + .withNetlifyToml({ config: { functions: { directory: 'functions', node_bundler: 'esbuild' } } }) + .withContentFile({ + path: path.join('functions', 'esm-function', 'esm-function.js'), + content: ` +export async function handler(event, context) { + return { + statusCode: 200, + body: 'esm', + }; +} + `, + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got(`${server.url}/.netlify/functions/esm-function`).text() + t.is(response, 'esm') + }) + }) + }) + + test( + testName('Should use the ZISI function bundler and serve TypeScript functions if using esbuild', args), + async (t) => { + await withSiteBuilder('site-with-ts-function', async (builder) => { + builder + .withNetlifyToml({ config: { functions: { directory: 'functions', node_bundler: 'esbuild' } } }) + .withContentFile({ + path: path.join('functions', 'ts-function', 'ts-function.ts'), + content: ` +type CustomResponse = string; + +export const handler = async function () { + const response: CustomResponse = "ts"; + + return { + statusCode: 200, + body: response, + }; +}; + + `, + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got(`${server.url}/.netlify/functions/ts-function`).text() + t.is(response, 'ts') + }) + }) + }, + ) + + test( + testName('Should use the ZISI function bundler and serve TypeScript functions if not using esbuild', args), + async (t) => { + await withSiteBuilder('site-with-ts-function', async (builder) => { + builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withContentFile({ + path: path.join('functions', 'ts-function', 'ts-function.ts'), + content: ` +type CustomResponse = string; + +export const handler = async function () { + const response: CustomResponse = "ts"; + + return { + statusCode: 200, + body: response, + }; +}; + + `, + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got(`${server.url}/.netlify/functions/ts-function`).text() + t.is(response, 'ts') + }) + }) + }, + ) + + test(testName(`should start https server when https dev block is configured`, args), async (t) => { + await withSiteBuilder('sites-with-https-certificate', async (builder) => { + await builder + .withNetlifyToml({ + config: { + build: { publish: 'public' }, + functions: { directory: 'functions' }, + dev: { https: { certFile: 'cert.pem', keyFile: 'key.pem' } }, + }, + }) + .withContentFile({ + path: 'public/index.html', + content: 'index', + }) + .withRedirectsFile({ + redirects: [{ from: `/api/*`, to: `/.netlify/functions/:splat`, status: '200' }], + }) + .withFunction({ + path: 'hello.js', + handler: async () => ({ + statusCode: 200, + body: 'Hello World', + }), + }) + .buildAsync() + + await Promise.all([ + copyFile(`${__dirname}/assets/cert.pem`, `${builder.directory}/cert.pem`), + copyFile(`${__dirname}/assets/key.pem`, `${builder.directory}/key.pem`), + ]) + await withDevServer({ cwd: builder.directory, args }, async ({ port }) => { + const options = { https: { rejectUnauthorized: false } } + t.is(await got(`https://localhost:${port}`, options).text(), 'index') + t.is(await got(`https://localhost:${port}/api/hello`, options).text(), 'Hello World') + }) + }) + }) + + test(testName(`should use custom functions timeouts`, args), async (t) => { + await withSiteBuilder('site-with-custom-functions-timeout', async (builder) => { + await builder + .withNetlifyToml({ + config: { + build: { publish: 'public' }, + functions: { directory: 'functions' }, + }, + }) + .withFunction({ + path: 'hello.js', + handler: async () => { + await new Promise((resolve) => { + const SLEEP_TIME = 2000 + setTimeout(resolve, SLEEP_TIME) + }) + return { + statusCode: 200, + body: 'Hello World', + } + }, + }) + .buildAsync() + + const siteInfo = { + account_slug: 'test-account', + id: 'site_id', + name: 'site-name', + functions_config: { timeout: 1 }, + } + + const routes = [ + { path: 'sites/site_id', response: siteInfo }, + + { path: 'sites/site_id/service-instances', response: [] }, + { + path: 'accounts', + response: [{ slug: siteInfo.account_slug }], + }, + ] + + await withMockApi(routes, async ({ apiUrl }) => { + await withDevServer( + { + cwd: builder.directory, + offline: false, + env: { + NETLIFY_API_URL: apiUrl, + NETLIFY_SITE_ID: 'site_id', + NETLIFY_AUTH_TOKEN: 'fake-token', + }, + }, + async ({ url }) => { + const error = await t.throwsAsync(() => got(`${url}/.netlify/functions/hello`)) + t.true(error.response.body.includes('TimeoutError: Task timed out after 1.00 seconds')) + }, + ) + }) + }) + }) + + // we need curl to reproduce this issue + if (os.platform() !== 'win32') { + test(testName(`don't hang on 'Expect: 100-continue' header`, args), async () => { + await withSiteBuilder('site-with-expect-header', async (builder) => { + await builder + .withNetlifyToml({ + config: { + functions: { directory: 'functions' }, + }, + }) + .withFunction({ + path: 'hello.js', + handler: async () => ({ statusCode: 200, body: 'Hello' }), + }) + .buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + await curl(`${server.url}/.netlify/functions/hello`, [ + '-i', + '-v', + '-d', + '{"somefield":"somevalue"}', + '-H', + 'Content-Type: application/json', + '-H', + `Expect: 100-continue' header`, + ]) + }) + }) + }) + } + + test(testName(`serves non ascii static files correctly`, args), async (t) => { + await withSiteBuilder('site-with-non-ascii-files', async (builder) => { + await builder + .withContentFile({ + path: 'public/范.txt', + content: 'success', + }) + .withNetlifyToml({ + config: { + build: { publish: 'public' }, + redirects: [{ from: '/*', to: '/index.html', status: 200 }], + }, + }) + .buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got(`${server.url}/${encodeURIComponent('范.txt')}`) + t.is(response.body, 'success') + }) + }) + }) + + test(testName(`returns headers set by function`, args), async (t) => { + await withSiteBuilder('site-with-function-with-custom-headers', async (builder) => { + await builder + .withFunction({ + pathPrefix: 'netlify/functions', + path: 'custom-headers.js', + handler: async () => ({ + statusCode: 200, + body: '', + headers: { 'single-value-header': 'custom-value' }, + multiValueHeaders: { 'multi-value-header': ['custom-value1', 'custom-value2'] }, + metadata: { builder_function: true }, + }), + }) + .buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got(`${server.url}/.netlify/functions/custom-headers`) + t.is(response.headers['single-value-header'], 'custom-value') + t.is(response.headers['multi-value-header'], 'custom-value1, custom-value2') + const builderResponse = await got(`${server.url}/.netlify/builders/custom-headers`) + t.is(builderResponse.headers['single-value-header'], 'custom-value') + t.is(builderResponse.headers['multi-value-header'], 'custom-value1, custom-value2') + }) + }) + }) + + test(testName('should match redirect when path is URL encoded', args), async (t) => { + await withSiteBuilder('site-with-encoded-redirect', async (builder) => { + await builder + .withContentFile({ path: 'static/special[test].txt', content: `special` }) + .withRedirectsFile({ redirects: [{ from: '/_next/static/*', to: '/static/:splat', status: 200 }] }) + .buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const [response1, response2] = await Promise.all([ + got(`${server.url}/_next/static/special[test].txt`).text(), + got(`${server.url}/_next/static/special%5Btest%5D.txt`).text(), + ]) + t.is(response1, 'special') + t.is(response2, 'special') + }) + }) + }) + + test(testName(`should not redirect POST request to functions server when it doesn't exists`, args), async (t) => { + await withSiteBuilder('site-with-post-request', async (builder) => { + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + // an error is expected since we're sending a POST request to a static server + // the important thing is that it's not proxied to the functions server + const error = await t.throwsAsync(() => + got.post(`${server.url}/api/test`, { + headers: { + 'content-type': 'application/x-www-form-urlencoded', + }, + body: 'some=thing', + }), + ) + + t.is(error.message, 'Response code 405 (Method Not Allowed)') + }) + }) + }) +}) +/* eslint-enable require-await */ diff --git a/tests/command.deploy.test.js b/tests/integration/210.command.deploy.test.js similarity index 99% rename from tests/command.deploy.test.js rename to tests/integration/210.command.deploy.test.js index 99c5fb4eb58..2461835e123 100644 --- a/tests/command.deploy.test.js +++ b/tests/integration/210.command.deploy.test.js @@ -5,8 +5,8 @@ const process = require('process') const test = require('ava') const omit = require('omit.js').default -const { supportsEdgeHandlers } = require('../src/lib/account') -const { getToken } = require('../src/utils/command-helpers') +const { supportsEdgeHandlers } = require('../../src/lib/account') +const { getToken } = require('../../src/utils/command-helpers') const callCli = require('./utils/call-cli') const { createLiveTestSite, generateSiteName } = require('./utils/create-live-test-site') diff --git a/tests/command.graph.test.js b/tests/integration/220.command.graph.test.js similarity index 100% rename from tests/command.graph.test.js rename to tests/integration/220.command.graph.test.js diff --git a/tests/rules-proxy.test.js b/tests/integration/230.rules-proxy.test.js similarity index 84% rename from tests/rules-proxy.test.js rename to tests/integration/230.rules-proxy.test.js index aec23643fcc..1b66b9dbfe6 100644 --- a/tests/rules-proxy.test.js +++ b/tests/integration/230.rules-proxy.test.js @@ -4,7 +4,7 @@ const path = require('path') const test = require('ava') const getPort = require('get-port') -const { createRewriter } = require('../src/utils/rules-proxy') +const { createRewriter, getWatchers } = require('../../src/utils/rules-proxy') const got = require('./utils/got') const { createSiteBuilder } = require('./utils/site-builder') @@ -34,7 +34,9 @@ test.before(async (t) => { t.context.server = server t.context.builder = builder - return server.listen(port) + await new Promise((resolve) => { + server.listen(port, 'localhost', resolve) + }) }) const PORT = 8888 @@ -44,8 +46,8 @@ test.after(async (t) => { t.context.server.on('close', resolve) t.context.server.close() }) - // TODO: check why this line breaks the rewriter on windows - // await t.context.builder.cleanupAsync() + await Promise.all(getWatchers().map((watcher) => watcher.close())) + await t.context.builder.cleanupAsync() }) test('should apply re-write rule based on _redirects file', async (t) => { diff --git a/tests/telemetry.test.js b/tests/integration/240.telemetry.test.js similarity index 96% rename from tests/telemetry.test.js rename to tests/integration/240.telemetry.test.js index f3243066343..e75268bf10f 100644 --- a/tests/telemetry.test.js +++ b/tests/integration/240.telemetry.test.js @@ -3,7 +3,7 @@ const process = require('process') const test = require('ava') const { version: uuidVersion } = require('uuid') -const { name, version } = require('../package.json') +const { name, version } = require('../../package.json') const callCli = require('./utils/call-cli') const { withMockApi } = require('./utils/mock-api') diff --git a/tests/command.lm.test.js b/tests/integration/30.command.lm.test.js similarity index 98% rename from tests/command.lm.test.js rename to tests/integration/30.command.lm.test.js index 670b0398e21..67be02f8484 100644 --- a/tests/command.lm.test.js +++ b/tests/integration/30.command.lm.test.js @@ -6,7 +6,7 @@ const test = require('ava') const execa = require('execa') const ini = require('ini') -const { getPathInHome } = require('../src/lib/settings') +const { getPathInHome } = require('../../src/lib/settings') const callCli = require('./utils/call-cli') const { getCLIOptions, startMockApi } = require('./utils/mock-api') diff --git a/tests/integration/300.command.dev.test.js b/tests/integration/300.command.dev.test.js new file mode 100644 index 00000000000..7ed8ae86f7c --- /dev/null +++ b/tests/integration/300.command.dev.test.js @@ -0,0 +1,262 @@ +// Handlers are meant to be async outside tests +/* eslint-disable require-await */ +const path = require('path') +const process = require('process') + +// eslint-disable-next-line ava/use-test +const avaTest = require('ava') +const { isCI } = require('ci-info') + +const { withDevServer } = require('./utils/dev-server') +const got = require('./utils/got') +const { withSiteBuilder } = require('./utils/site-builder') + +const test = isCI ? avaTest.serial.bind(avaTest) : avaTest + +const testMatrix = [ + { args: [] }, + + // some tests are still failing with this enabled + // { args: ['--edgeHandlers'] } +] + +const testName = (title, args) => (args.length <= 0 ? title : `${title} - ${args.join(' ')}`) + +testMatrix.forEach(({ args }) => { + test(testName('should return index file when / is accessed', args), async (t) => { + await withSiteBuilder('site-with-index-file', async (builder) => { + builder.withContentFile({ + path: 'index.html', + content: '

⊂◉‿◉つ

', + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got(server.url).text() + t.is(response, '

⊂◉‿◉つ

') + }) + }) + }) + + test(testName('should return user defined headers when / is accessed', args), async (t) => { + await withSiteBuilder('site-with-headers-on-root', async (builder) => { + builder.withContentFile({ + path: 'index.html', + content: '

⊂◉‿◉つ

', + }) + + const headerName = 'X-Frame-Options' + const headerValue = 'SAMEORIGIN' + builder.withHeadersFile({ headers: [{ path: '/*', headers: [`${headerName}: ${headerValue}`] }] }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const { headers } = await got(server.url) + t.is(headers[headerName.toLowerCase()], headerValue) + }) + }) + }) + + test(testName('should return user defined headers when non-root path is accessed', args), async (t) => { + await withSiteBuilder('site-with-headers-on-non-root', async (builder) => { + builder.withContentFile({ + path: 'foo/index.html', + content: '

⊂◉‿◉つ

', + }) + + const headerName = 'X-Frame-Options' + const headerValue = 'SAMEORIGIN' + builder.withHeadersFile({ headers: [{ path: '/*', headers: [`${headerName}: ${headerValue}`] }] }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const { headers } = await got(`${server.url}/foo`) + t.is(headers[headerName.toLowerCase()], headerValue) + }) + }) + }) + + test(testName('should return response from a function with setTimeout', args), async (t) => { + await withSiteBuilder('site-with-set-timeout-function', async (builder) => { + builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({ + path: 'timeout.js', + handler: async () => { + console.log('ding') + // Wait for 4 seconds + const FUNCTION_TIMEOUT = 4e3 + await new Promise((resolve) => { + setTimeout(resolve, FUNCTION_TIMEOUT) + }) + return { + statusCode: 200, + body: 'ping', + metadata: { builder_function: true }, + } + }, + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got(`${server.url}/.netlify/functions/timeout`).text() + t.is(response, 'ping') + const builderResponse = await got(`${server.url}/.netlify/builders/timeout`).text() + t.is(builderResponse, 'ping') + }) + }) + }) + + test(testName('should fail when no metadata is set for builder function', args), async (t) => { + await withSiteBuilder('site-with-misconfigured-builder-function', async (builder) => { + builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({ + path: 'builder.js', + handler: async () => ({ + statusCode: 200, + body: 'ping', + }), + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got(`${server.url}/.netlify/functions/builder`) + t.is(response.body, 'ping') + t.is(response.statusCode, 200) + const builderResponse = await got(`${server.url}/.netlify/builders/builder`, { + throwHttpErrors: false, + }) + t.is( + builderResponse.body, + `{"message":"Function is not an on-demand builder. See https://ntl.fyi/create-builder for how to convert a function to a builder."}`, + ) + t.is(builderResponse.statusCode, 400) + }) + }) + }) + + test(testName('should serve function from a subdirectory', args), async (t) => { + await withSiteBuilder('site-with-from-subdirectory', async (builder) => { + builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({ + path: path.join('echo', 'echo.js'), + handler: async () => ({ + statusCode: 200, + body: 'ping', + metadata: { builder_function: true }, + }), + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got(`${server.url}/.netlify/functions/echo`).text() + t.is(response, 'ping') + const builderResponse = await got(`${server.url}/.netlify/builders/echo`).text() + t.is(builderResponse, 'ping') + }) + }) + }) + + test(testName('should pass .env.development vars to function', args), async (t) => { + await withSiteBuilder('site-with-env-development', async (builder) => { + builder + .withNetlifyToml({ config: { functions: { directory: 'functions' } } }) + .withEnvFile({ path: '.env.development', env: { TEST: 'FROM_DEV_FILE' } }) + .withFunction({ + path: 'env.js', + handler: async () => ({ + statusCode: 200, + body: `${process.env.TEST}`, + metadata: { builder_function: true }, + }), + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got(`${server.url}/.netlify/functions/env`).text() + t.is(response, 'FROM_DEV_FILE') + const builderResponse = await got(`${server.url}/.netlify/builders/env`).text() + t.is(builderResponse, 'FROM_DEV_FILE') + }) + }) + }) + + test(testName('should pass process env vars to function', args), async (t) => { + await withSiteBuilder('site-with-process-env', async (builder) => { + builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({ + path: 'env.js', + handler: async () => ({ + statusCode: 200, + body: `${process.env.TEST}`, + metadata: { builder_function: true }, + }), + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, env: { TEST: 'FROM_PROCESS_ENV' }, args }, async (server) => { + const response = await got(`${server.url}/.netlify/functions/env`).text() + t.is(response, 'FROM_PROCESS_ENV') + const builderResponse = await got(`${server.url}/.netlify/builders/env`).text() + t.is(builderResponse, 'FROM_PROCESS_ENV') + }) + }) + }) + + test(testName('should pass [build.environment] env vars to function', args), async (t) => { + await withSiteBuilder('site-with-build-environment', async (builder) => { + builder + .withNetlifyToml({ + config: { build: { environment: { TEST: 'FROM_CONFIG_FILE' } }, functions: { directory: 'functions' } }, + }) + .withFunction({ + path: 'env.js', + handler: async () => ({ + statusCode: 200, + body: `${process.env.TEST}`, + metadata: { builder_function: true }, + }), + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got(`${server.url}/.netlify/functions/env`).text() + t.is(response, 'FROM_CONFIG_FILE') + const builderResponse = await got(`${server.url}/.netlify/builders/env`).text() + t.is(builderResponse, 'FROM_CONFIG_FILE') + }) + }) + }) + + test(testName('[context.dev.environment] should override [build.environment]', args), async (t) => { + await withSiteBuilder('site-with-build-environment', async (builder) => { + builder + .withNetlifyToml({ + config: { + build: { environment: { TEST: 'DEFAULT_CONTEXT' } }, + context: { dev: { environment: { TEST: 'DEV_CONTEXT' } } }, + functions: { directory: 'functions' }, + }, + }) + .withFunction({ + path: 'env.js', + handler: async () => ({ + statusCode: 200, + body: `${process.env.TEST}`, + }), + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got(`${server.url}/.netlify/functions/env`).text() + t.is(response, 'DEV_CONTEXT') + }) + }) + }) +}) +/* eslint-enable require-await */ diff --git a/tests/command.dev.exec.test.js b/tests/integration/310.command.dev.exec.test.js similarity index 100% rename from tests/command.dev.exec.test.js rename to tests/integration/310.command.dev.exec.test.js diff --git a/tests/command.help.test.js b/tests/integration/320.command.help.test.js similarity index 100% rename from tests/command.help.test.js rename to tests/integration/320.command.help.test.js diff --git a/tests/serving-functions.test.js b/tests/integration/330.serving-functions.test.js similarity index 98% rename from tests/serving-functions.test.js rename to tests/integration/330.serving-functions.test.js index 5618aaf7abf..8431317fd0b 100644 --- a/tests/serving-functions.test.js +++ b/tests/integration/330.serving-functions.test.js @@ -1,7 +1,9 @@ /* eslint-disable require-await */ const { join } = require('path') -const test = require('ava') +// eslint-disable-next-line ava/use-test +const avaTest = require('ava') +const { isCI } = require('ci-info') const pWaitFor = require('p-wait-for') const { tryAndLogOutput, withDevServer } = require('./utils/dev-server') @@ -16,6 +18,8 @@ const WAIT_INTERVAL = 1800 const WAIT_TIMEOUT = 30_000 const WAIT_WRITE = 3000 +const test = isCI ? avaTest.serial.bind(avaTest) : avaTest + const gotCatch404 = async (url, options) => { try { return await got(url, options) @@ -83,7 +87,7 @@ testMatrix.forEach(({ args }) => { }) }) - test.skip(testName('Updates a TypeScript function when its main file is modified', args), async (t) => { + test(testName('Updates a TypeScript function when its main file is modified', args), async (t) => { await withSiteBuilder('ts-function-update-main-file', async (builder) => { const bundlerConfig = args.includes('esbuild') ? { node_bundler: 'esbuild' } : {} @@ -359,7 +363,7 @@ testMatrix.forEach(({ args }) => { }) }) - test.skip(testName('Adds a new TypeScript function when a function file is created', args), async (t) => { + test(testName('Adds a new TypeScript function when a function file is created', args), async (t) => { await withSiteBuilder('ts-function-create-function-file', async (builder) => { const bundlerConfig = args.includes('esbuild') ? { node_bundler: 'esbuild' } : {} diff --git a/tests/integration/400.command.dev.test.js b/tests/integration/400.command.dev.test.js new file mode 100644 index 00000000000..5063ecba97d --- /dev/null +++ b/tests/integration/400.command.dev.test.js @@ -0,0 +1,662 @@ +// Handlers are meant to be async outside tests +/* eslint-disable require-await */ +const path = require('path') +const process = require('process') + +// eslint-disable-next-line ava/use-test +const avaTest = require('ava') +const { isCI } = require('ci-info') +const FormData = require('form-data') + +const { withDevServer } = require('./utils/dev-server') +const got = require('./utils/got') +const { withSiteBuilder } = require('./utils/site-builder') + +const test = isCI ? avaTest.serial.bind(avaTest) : avaTest + +const testMatrix = [ + { args: [] }, + + // some tests are still failing with this enabled + // { args: ['--edgeHandlers'] } +] + +const testName = (title, args) => (args.length <= 0 ? title : `${title} - ${args.join(' ')}`) + +testMatrix.forEach(({ args }) => { + test(testName('should use [build.environment] and not [context.production.environment]', args), async (t) => { + await withSiteBuilder('site-with-build-environment', async (builder) => { + builder + .withNetlifyToml({ + config: { + build: { environment: { TEST: 'DEFAULT_CONTEXT' } }, + context: { production: { environment: { TEST: 'PRODUCTION_CONTEXT' } } }, + functions: { directory: 'functions' }, + }, + }) + .withFunction({ + path: 'env.js', + handler: async () => ({ + statusCode: 200, + body: `${process.env.TEST}`, + }), + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got(`${server.url}/.netlify/functions/env`).text() + t.is(response, 'DEFAULT_CONTEXT') + }) + }) + }) + + test(testName('should override .env.development with process env', args), async (t) => { + await withSiteBuilder('site-with-override', async (builder) => { + builder + .withNetlifyToml({ config: { functions: { directory: 'functions' } } }) + .withEnvFile({ path: '.env.development', env: { TEST: 'FROM_DEV_FILE' } }) + .withFunction({ + path: 'env.js', + handler: async () => ({ + statusCode: 200, + body: `${process.env.TEST}`, + }), + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, env: { TEST: 'FROM_PROCESS_ENV' }, args }, async (server) => { + const response = await got(`${server.url}/.netlify/functions/env`).text() + t.is(response, 'FROM_PROCESS_ENV') + }) + }) + }) + + test(testName('should override [build.environment] with process env', args), async (t) => { + await withSiteBuilder('site-with-build-environment-override', async (builder) => { + builder + .withNetlifyToml({ + config: { build: { environment: { TEST: 'FROM_CONFIG_FILE' } }, functions: { directory: 'functions' } }, + }) + .withFunction({ + path: 'env.js', + handler: async () => ({ + statusCode: 200, + body: `${process.env.TEST}`, + }), + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, env: { TEST: 'FROM_PROCESS_ENV' }, args }, async (server) => { + const response = await got(`${server.url}/.netlify/functions/env`).text() + t.is(response, 'FROM_PROCESS_ENV') + }) + }) + }) + + test(testName('should override value of the NETLIFY_DEV env variable', args), async (t) => { + await withSiteBuilder('site-with-netlify-dev-override', async (builder) => { + builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({ + path: 'env.js', + handler: async () => ({ + statusCode: 200, + body: `${process.env.NETLIFY_DEV}`, + }), + }) + + await builder.buildAsync() + + await withDevServer( + { cwd: builder.directory, env: { NETLIFY_DEV: 'FROM_PROCESS_ENV' }, args }, + async (server) => { + const response = await got(`${server.url}/.netlify/functions/env`).text() + t.is(response, 'true') + }, + ) + }) + }) + + test(testName('should set value of the CONTEXT env variable', args), async (t) => { + await withSiteBuilder('site-with-context-override', async (builder) => { + builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({ + path: 'env.js', + handler: async () => ({ + statusCode: 200, + body: `${process.env.CONTEXT}`, + }), + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got(`${server.url}/.netlify/functions/env`).text() + t.is(response, 'dev') + }) + }) + }) + + test(testName('should redirect using a wildcard when set in netlify.toml', args), async (t) => { + await withSiteBuilder('site-with-redirect-function', async (builder) => { + builder + .withNetlifyToml({ + config: { + functions: { directory: 'functions' }, + redirects: [{ from: '/api/*', to: '/.netlify/functions/:splat', status: 200 }], + }, + }) + .withFunction({ + path: 'ping.js', + handler: async () => ({ + statusCode: 200, + body: 'ping', + }), + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got(`${server.url}/api/ping`).text() + t.is(response, 'ping') + }) + }) + }) + + test(testName('should pass undefined body to functions event for GET requests when redirecting', args), async (t) => { + await withSiteBuilder('site-with-get-echo-function', async (builder) => { + builder + .withNetlifyToml({ + config: { + functions: { directory: 'functions' }, + redirects: [{ from: '/api/*', to: '/.netlify/functions/:splat', status: 200 }], + }, + }) + .withFunction({ + path: 'echo.js', + handler: async (event) => ({ + statusCode: 200, + body: JSON.stringify(event), + }), + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got(`${server.url}/api/echo?ding=dong`).json() + t.is(response.body, undefined) + t.is(response.headers.host, `${server.host}:${server.port}`) + t.is(response.httpMethod, 'GET') + t.is(response.isBase64Encoded, true) + t.is(response.path, '/api/echo') + t.deepEqual(response.queryStringParameters, { ding: 'dong' }) + }) + }) + }) + + test(testName('should pass body to functions event for POST requests when redirecting', args), async (t) => { + await withSiteBuilder('site-with-post-echo-function', async (builder) => { + builder + .withNetlifyToml({ + config: { + functions: { directory: 'functions' }, + redirects: [{ from: '/api/*', to: '/.netlify/functions/:splat', status: 200 }], + }, + }) + .withFunction({ + path: 'echo.js', + handler: async (event) => ({ + statusCode: 200, + body: JSON.stringify(event), + }), + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got + .post(`${server.url}/api/echo?ding=dong`, { + headers: { + 'content-type': 'application/x-www-form-urlencoded', + }, + body: 'some=thing', + }) + .json() + + t.is(response.body, 'some=thing') + t.is(response.headers.host, `${server.host}:${server.port}`) + t.is(response.headers['content-type'], 'application/x-www-form-urlencoded') + t.is(response.headers['content-length'], '10') + t.is(response.httpMethod, 'POST') + t.is(response.isBase64Encoded, false) + t.is(response.path, '/api/echo') + t.deepEqual(response.queryStringParameters, { ding: 'dong' }) + }) + }) + }) + + test(testName('should return an empty body for a function with no body when redirecting', args), async (t) => { + await withSiteBuilder('site-with-no-body-function', async (builder) => { + builder + .withNetlifyToml({ + config: { + functions: { directory: 'functions' }, + redirects: [{ from: '/api/*', to: '/.netlify/functions/:splat', status: 200 }], + }, + }) + .withFunction({ + path: 'echo.js', + handler: async () => ({ + statusCode: 200, + }), + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got.post(`${server.url}/api/echo?ding=dong`, { + headers: { + 'content-type': 'application/x-www-form-urlencoded', + }, + body: 'some=thing', + }) + + t.is(response.body, '') + t.is(response.statusCode, 200) + }) + }) + }) + + test(testName('should handle multipart form data when redirecting', args), async (t) => { + await withSiteBuilder('site-with-multi-part-function', async (builder) => { + builder + .withNetlifyToml({ + config: { + functions: { directory: 'functions' }, + redirects: [{ from: '/api/*', to: '/.netlify/functions/:splat', status: 200 }], + }, + }) + .withFunction({ + path: 'echo.js', + handler: async (event) => ({ + statusCode: 200, + body: JSON.stringify(event), + }), + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const form = new FormData() + form.append('some', 'thing') + + const expectedBoundary = form.getBoundary() + const expectedResponseBody = form.getBuffer().toString('base64') + + const response = await got + .post(`${server.url}/api/echo?ding=dong`, { + body: form, + }) + .json() + + t.is(response.headers.host, `${server.host}:${server.port}`) + t.is(response.headers['content-type'], `multipart/form-data; boundary=${expectedBoundary}`) + t.is(response.headers['content-length'], '164') + t.is(response.httpMethod, 'POST') + t.is(response.isBase64Encoded, true) + t.is(response.path, '/api/echo') + t.deepEqual(response.queryStringParameters, { ding: 'dong' }) + t.is(response.body, expectedResponseBody) + }) + }) + }) + + test(testName('should return 404 when redirecting to a non existing function', args), async (t) => { + await withSiteBuilder('site-with-missing-function', async (builder) => { + builder.withNetlifyToml({ + config: { + functions: { directory: 'functions' }, + redirects: [{ from: '/api/*', to: '/.netlify/functions/:splat', status: 200 }], + }, + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got + .post(`${server.url}/api/none`, { + body: 'nothing', + }) + .catch((error) => error.response) + + t.is(response.statusCode, 404) + }) + }) + }) + + test(testName('should parse function query parameters using simple parsing', args), async (t) => { + await withSiteBuilder('site-with-multi-part-function', async (builder) => { + builder + .withNetlifyToml({ + config: { + functions: { directory: 'functions' }, + }, + }) + .withFunction({ + path: 'echo.js', + handler: async (event) => ({ + statusCode: 200, + body: JSON.stringify(event), + }), + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response1 = await got(`${server.url}/.netlify/functions/echo?category[SOMETHING][]=something`).json() + const response2 = await got(`${server.url}/.netlify/functions/echo?category=one&category=two`).json() + + t.deepEqual(response1.queryStringParameters, { 'category[SOMETHING][]': 'something' }) + t.deepEqual(response2.queryStringParameters, { category: 'one, two' }) + }) + }) + }) + + test(testName('should handle form submission', args), async (t) => { + await withSiteBuilder('site-with-form', async (builder) => { + builder + .withContentFile({ + path: 'index.html', + content: '

⊂◉‿◉つ

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

⊂◉‿◉つ

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

⊂◉‿◉つ

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

foo') + t.is(response2, '

foo') + t.is(response3, '

not-foo') + }) + }) + }) +}) +/* eslint-enable require-await */ diff --git a/tests/command.dev.trace.test.js b/tests/integration/410.command.dev.trace.test.js similarity index 100% rename from tests/command.dev.trace.test.js rename to tests/integration/410.command.dev.trace.test.js diff --git a/tests/command.init.test.js b/tests/integration/420.command.init.test.js similarity index 100% rename from tests/command.init.test.js rename to tests/integration/420.command.init.test.js diff --git a/tests/integration/500.command.dev.test.js b/tests/integration/500.command.dev.test.js new file mode 100644 index 00000000000..c26810b24e8 --- /dev/null +++ b/tests/integration/500.command.dev.test.js @@ -0,0 +1,374 @@ +// Handlers are meant to be async outside tests +/* eslint-disable require-await */ +const path = require('path') + +// eslint-disable-next-line ava/use-test +const avaTest = require('ava') +const { isCI } = require('ci-info') +const FormData = require('form-data') + +const { withDevServer } = require('./utils/dev-server') +const got = require('./utils/got') +const { withSiteBuilder } = require('./utils/site-builder') + +const test = isCI ? avaTest.serial.bind(avaTest) : avaTest + +const testMatrix = [ + { args: [] }, + + // some tests are still failing with this enabled + // { args: ['--edgeHandlers'] } +] + +const testName = (title, args) => (args.length <= 0 ? title : `${title} - ${args.join(' ')}`) + +testMatrix.forEach(({ args }) => { + test(testName('should return 404 when redirecting to a non existing function', args), async (t) => { + await withSiteBuilder('site-with-missing-function', async (builder) => { + builder.withNetlifyToml({ + config: { + functions: { directory: 'functions' }, + redirects: [{ from: '/api/*', to: '/.netlify/functions/:splat', status: 200 }], + }, + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response = await got + .post(`${server.url}/api/none`, { + body: 'nothing', + }) + .catch((error) => error.response) + + t.is(response.statusCode, 404) + }) + }) + }) + + test(testName('should parse function query parameters using simple parsing', args), async (t) => { + await withSiteBuilder('site-with-multi-part-function', async (builder) => { + builder + .withNetlifyToml({ + config: { + functions: { directory: 'functions' }, + }, + }) + .withFunction({ + path: 'echo.js', + handler: async (event) => ({ + statusCode: 200, + body: JSON.stringify(event), + }), + }) + + await builder.buildAsync() + + await withDevServer({ cwd: builder.directory, args }, async (server) => { + const response1 = await got(`${server.url}/.netlify/functions/echo?category[SOMETHING][]=something`).json() + const response2 = await got(`${server.url}/.netlify/functions/echo?category=one&category=two`).json() + + t.deepEqual(response1.queryStringParameters, { 'category[SOMETHING][]': 'something' }) + t.deepEqual(response2.queryStringParameters, { category: 'one, two' }) + }) + }) + }) + + test(testName('should handle form submission', args), async (t) => { + await withSiteBuilder('site-with-form', async (builder) => { + builder + .withContentFile({ + path: 'index.html', + content: '

⊂◉‿◉つ

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

⊂◉‿◉つ

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

⊂◉‿◉つ

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

foo') + t.is(response2, '

foo') + t.is(response3, '

not-foo') + }) + }) + }) +}) +/* eslint-enable require-await */ diff --git a/tests/hugo.test.js b/tests/integration/510.hugo.test.js similarity index 100% rename from tests/hugo.test.js rename to tests/integration/510.hugo.test.js diff --git a/tests/command.link.test.js b/tests/integration/520.command.link.test.js similarity index 96% rename from tests/command.link.test.js rename to tests/integration/520.command.link.test.js index 3bb43d9d590..76a7ad47fe8 100644 --- a/tests/command.link.test.js +++ b/tests/integration/520.command.link.test.js @@ -2,7 +2,7 @@ const process = require('process') const test = require('ava') -const { isFileAsync } = require('../src/lib/fs') +const { isFileAsync } = require('../../src/lib/fs') const callCli = require('./utils/call-cli') const { getCLIOptions, withMockApi } = require('./utils/mock-api') diff --git a/tests/graph-codegen.test.js b/tests/integration/530.graph-codegen.test.js similarity index 98% rename from tests/graph-codegen.test.js rename to tests/integration/530.graph-codegen.test.js index 06d004645dd..e14e8e80806 100644 --- a/tests/graph-codegen.test.js +++ b/tests/integration/530.graph-codegen.test.js @@ -9,7 +9,7 @@ const { generateFunctionsSource, generateHandlerSource, parse, -} = require('../src/lib/one-graph/cli-netlify-graph') +} = require('../../src/lib/one-graph/cli-netlify-graph') const { normalize } = require('./utils/snapshots') diff --git a/tests/framework-detection.test.js b/tests/integration/600.framework-detection.test.js similarity index 98% rename from tests/framework-detection.test.js rename to tests/integration/600.framework-detection.test.js index a270009cded..0f433a2fd1d 100644 --- a/tests/framework-detection.test.js +++ b/tests/integration/600.framework-detection.test.js @@ -1,4 +1,6 @@ -const test = require('ava') +// eslint-disable-next-line ava/use-test +const avaTest = require('ava') +const { isCI } = require('ci-info') const execa = require('execa') const cliPath = require('./utils/cli-path') @@ -10,6 +12,8 @@ const { normalize } = require('./utils/snapshots') const content = 'Hello World!' +const test = isCI ? avaTest.serial.bind(avaTest) : avaTest + test('should default to process.cwd() and static server', async (t) => { await withSiteBuilder('site-with-index-file', async (builder) => { await builder diff --git a/tests/command.env.test.js b/tests/integration/610.command.env.test.js similarity index 100% rename from tests/command.env.test.js rename to tests/integration/610.command.env.test.js diff --git a/tests/serving-functions-rust.test.js b/tests/integration/620.serving-functions-rust.test.js similarity index 100% rename from tests/serving-functions-rust.test.js rename to tests/integration/620.serving-functions-rust.test.js diff --git a/tests/serving-functions-go.test.js b/tests/integration/630.serving-functions-go.test.js similarity index 100% rename from tests/serving-functions-go.test.js rename to tests/integration/630.serving-functions-go.test.js diff --git a/tests/assets/bundled-function-1.zip b/tests/integration/assets/bundled-function-1.zip similarity index 100% rename from tests/assets/bundled-function-1.zip rename to tests/integration/assets/bundled-function-1.zip diff --git a/tests/assets/cert.pem b/tests/integration/assets/cert.pem similarity index 100% rename from tests/assets/cert.pem rename to tests/integration/assets/cert.pem diff --git a/tests/assets/key.pem b/tests/integration/assets/key.pem similarity index 100% rename from tests/assets/key.pem rename to tests/integration/assets/key.pem diff --git a/tests/assets/netlifyGraphOperationsLibrary.graphql b/tests/integration/assets/netlifyGraphOperationsLibrary.graphql similarity index 100% rename from tests/assets/netlifyGraphOperationsLibrary.graphql rename to tests/integration/assets/netlifyGraphOperationsLibrary.graphql diff --git a/tests/assets/netlifyGraphSchema.graphql b/tests/integration/assets/netlifyGraphSchema.graphql similarity index 100% rename from tests/assets/netlifyGraphSchema.graphql rename to tests/integration/assets/netlifyGraphSchema.graphql diff --git a/tests/eleventy-site/.eleventy.js b/tests/integration/eleventy-site/.eleventy.js similarity index 100% rename from tests/eleventy-site/.eleventy.js rename to tests/integration/eleventy-site/.eleventy.js diff --git a/tests/eleventy-site/.gitignore b/tests/integration/eleventy-site/.gitignore similarity index 100% rename from tests/eleventy-site/.gitignore rename to tests/integration/eleventy-site/.gitignore diff --git a/tests/eleventy-site/_redirects b/tests/integration/eleventy-site/_redirects similarity index 100% rename from tests/eleventy-site/_redirects rename to tests/integration/eleventy-site/_redirects diff --git a/tests/eleventy-site/force.html b/tests/integration/eleventy-site/force.html similarity index 100% rename from tests/eleventy-site/force.html rename to tests/integration/eleventy-site/force.html diff --git a/tests/eleventy-site/functions/echo.js b/tests/integration/eleventy-site/functions/echo.js similarity index 100% rename from tests/eleventy-site/functions/echo.js rename to tests/integration/eleventy-site/functions/echo.js diff --git a/tests/eleventy-site/index.html b/tests/integration/eleventy-site/index.html similarity index 100% rename from tests/eleventy-site/index.html rename to tests/integration/eleventy-site/index.html diff --git a/tests/eleventy-site/netlify.toml b/tests/integration/eleventy-site/netlify.toml similarity index 100% rename from tests/eleventy-site/netlify.toml rename to tests/integration/eleventy-site/netlify.toml diff --git a/tests/eleventy-site/otherthing.html b/tests/integration/eleventy-site/otherthing.html similarity index 100% rename from tests/eleventy-site/otherthing.html rename to tests/integration/eleventy-site/otherthing.html diff --git a/tests/eleventy-site/package-lock.json b/tests/integration/eleventy-site/package-lock.json similarity index 100% rename from tests/eleventy-site/package-lock.json rename to tests/integration/eleventy-site/package-lock.json diff --git a/tests/eleventy-site/package.json b/tests/integration/eleventy-site/package.json similarity index 100% rename from tests/eleventy-site/package.json rename to tests/integration/eleventy-site/package.json diff --git a/tests/eleventy-site/test.html b/tests/integration/eleventy-site/test.html similarity index 100% rename from tests/eleventy-site/test.html rename to tests/integration/eleventy-site/test.html diff --git a/tests/hugo-site/config.toml b/tests/integration/hugo-site/config.toml similarity index 100% rename from tests/hugo-site/config.toml rename to tests/integration/hugo-site/config.toml diff --git a/tests/hugo-site/content/_index.html b/tests/integration/hugo-site/content/_index.html similarity index 100% rename from tests/hugo-site/content/_index.html rename to tests/integration/hugo-site/content/_index.html diff --git a/tests/hugo-site/layouts/_default/list.html b/tests/integration/hugo-site/layouts/_default/list.html similarity index 100% rename from tests/hugo-site/layouts/_default/list.html rename to tests/integration/hugo-site/layouts/_default/list.html diff --git a/tests/hugo-site/netlify.toml b/tests/integration/hugo-site/netlify.toml similarity index 100% rename from tests/hugo-site/netlify.toml rename to tests/integration/hugo-site/netlify.toml diff --git a/tests/hugo-site/package-lock.json b/tests/integration/hugo-site/package-lock.json similarity index 100% rename from tests/hugo-site/package-lock.json rename to tests/integration/hugo-site/package-lock.json diff --git a/tests/hugo-site/package.json b/tests/integration/hugo-site/package.json similarity index 100% rename from tests/hugo-site/package.json rename to tests/integration/hugo-site/package.json diff --git a/tests/hugo-site/static/_redirects b/tests/integration/hugo-site/static/_redirects similarity index 100% rename from tests/hugo-site/static/_redirects rename to tests/integration/hugo-site/static/_redirects diff --git a/tests/snapshots/command.graph.test.js.md b/tests/integration/snapshots/220.command.graph.test.js.md similarity index 93% rename from tests/snapshots/command.graph.test.js.md rename to tests/integration/snapshots/220.command.graph.test.js.md index 3d137590b47..717ba55c824 100644 --- a/tests/snapshots/command.graph.test.js.md +++ b/tests/integration/snapshots/220.command.graph.test.js.md @@ -1,6 +1,6 @@ -# Snapshot report for `tests/command.graph.test.js` +# Snapshot report for `tests/220.command.graph.test.js` -The actual snapshot is saved in `command.graph.test.js.snap`. +The actual snapshot is saved in `220.command.graph.test.js.snap`. Generated by [AVA](https://avajs.dev). diff --git a/tests/snapshots/command.graph.test.js.snap b/tests/integration/snapshots/220.command.graph.test.js.snap similarity index 100% rename from tests/snapshots/command.graph.test.js.snap rename to tests/integration/snapshots/220.command.graph.test.js.snap diff --git a/tests/snapshots/command.help.test.js.md b/tests/integration/snapshots/320.command.help.test.js.md similarity index 95% rename from tests/snapshots/command.help.test.js.md rename to tests/integration/snapshots/320.command.help.test.js.md index bfdc8989c5b..219dc1d6082 100644 --- a/tests/snapshots/command.help.test.js.md +++ b/tests/integration/snapshots/320.command.help.test.js.md @@ -1,6 +1,6 @@ -# Snapshot report for `tests/command.help.test.js` +# Snapshot report for `tests/320.command.help.test.js` -The actual snapshot is saved in `command.help.test.js.snap`. +The actual snapshot is saved in `320.command.help.test.js.snap`. Generated by [AVA](https://avajs.dev). diff --git a/tests/snapshots/command.help.test.js.snap b/tests/integration/snapshots/320.command.help.test.js.snap similarity index 100% rename from tests/snapshots/command.help.test.js.snap rename to tests/integration/snapshots/320.command.help.test.js.snap diff --git a/tests/snapshots/graph-codegen.test.js.md b/tests/integration/snapshots/530.graph-codegen.test.js.md similarity index 98% rename from tests/snapshots/graph-codegen.test.js.md rename to tests/integration/snapshots/530.graph-codegen.test.js.md index b5843f754aa..ae99965c6a2 100644 --- a/tests/snapshots/graph-codegen.test.js.md +++ b/tests/integration/snapshots/530.graph-codegen.test.js.md @@ -1,6 +1,6 @@ -# Snapshot report for `tests/graph-codegen.test.js` +# Snapshot report for `tests/integration/530.graph-codegen.test.js` -The actual snapshot is saved in `graph-codegen.test.js.snap`. +The actual snapshot is saved in `530.graph-codegen.test.js.snap`. Generated by [AVA](https://avajs.dev). diff --git a/tests/snapshots/graph-codegen.test.js.snap b/tests/integration/snapshots/530.graph-codegen.test.js.snap similarity index 100% rename from tests/snapshots/graph-codegen.test.js.snap rename to tests/integration/snapshots/530.graph-codegen.test.js.snap diff --git a/tests/snapshots/framework-detection.test.js.md b/tests/integration/snapshots/600.framework-detection.test.js.md similarity index 98% rename from tests/snapshots/framework-detection.test.js.md rename to tests/integration/snapshots/600.framework-detection.test.js.md index a4408a302f4..f2f0d7f3398 100644 --- a/tests/snapshots/framework-detection.test.js.md +++ b/tests/integration/snapshots/600.framework-detection.test.js.md @@ -1,6 +1,6 @@ -# Snapshot report for `tests/framework-detection.test.js` +# Snapshot report for `tests/600.framework-detection.test.js` -The actual snapshot is saved in `framework-detection.test.js.snap`. +The actual snapshot is saved in `600.framework-detection.test.js.snap`. Generated by [AVA](https://avajs.dev). diff --git a/tests/snapshots/framework-detection.test.js.snap b/tests/integration/snapshots/600.framework-detection.test.js.snap similarity index 100% rename from tests/snapshots/framework-detection.test.js.snap rename to tests/integration/snapshots/600.framework-detection.test.js.snap diff --git a/tests/snapshots/command.env.test.js.md b/tests/integration/snapshots/610.command.env.test.js.md similarity index 96% rename from tests/snapshots/command.env.test.js.md rename to tests/integration/snapshots/610.command.env.test.js.md index af74e0afbf7..6bc61aa1a53 100644 --- a/tests/snapshots/command.env.test.js.md +++ b/tests/integration/snapshots/610.command.env.test.js.md @@ -1,6 +1,6 @@ -# Snapshot report for `tests/command.env.test.js` +# Snapshot report for `tests/610.command.env.test.js` -The actual snapshot is saved in `command.env.test.js.snap`. +The actual snapshot is saved in `610.command.env.test.js.snap`. Generated by [AVA](https://avajs.dev). diff --git a/tests/snapshots/command.env.test.js.snap b/tests/integration/snapshots/610.command.env.test.js.snap similarity index 100% rename from tests/snapshots/command.env.test.js.snap rename to tests/integration/snapshots/610.command.env.test.js.snap diff --git a/tests/utils/call-cli.js b/tests/integration/utils/call-cli.js similarity index 100% rename from tests/utils/call-cli.js rename to tests/integration/utils/call-cli.js diff --git a/tests/integration/utils/cli-path.js b/tests/integration/utils/cli-path.js new file mode 100644 index 00000000000..33b066dec44 --- /dev/null +++ b/tests/integration/utils/cli-path.js @@ -0,0 +1,5 @@ +const path = require('path') + +const cliPath = path.resolve(__dirname, '../../../bin/run') + +module.exports = cliPath diff --git a/tests/utils/create-live-test-site.js b/tests/integration/utils/create-live-test-site.js similarity index 100% rename from tests/utils/create-live-test-site.js rename to tests/integration/utils/create-live-test-site.js diff --git a/tests/utils/curl.js b/tests/integration/utils/curl.js similarity index 100% rename from tests/utils/curl.js rename to tests/integration/utils/curl.js diff --git a/tests/utils/dev-server.js b/tests/integration/utils/dev-server.js similarity index 100% rename from tests/utils/dev-server.js rename to tests/integration/utils/dev-server.js diff --git a/tests/utils/external-server.js b/tests/integration/utils/external-server.js similarity index 100% rename from tests/utils/external-server.js rename to tests/integration/utils/external-server.js diff --git a/tests/utils/got.js b/tests/integration/utils/got.js similarity index 100% rename from tests/utils/got.js rename to tests/integration/utils/got.js diff --git a/tests/utils/handle-questions.js b/tests/integration/utils/handle-questions.js similarity index 100% rename from tests/utils/handle-questions.js rename to tests/integration/utils/handle-questions.js diff --git a/tests/utils/mock-api.js b/tests/integration/utils/mock-api.js similarity index 100% rename from tests/utils/mock-api.js rename to tests/integration/utils/mock-api.js diff --git a/tests/utils/mock-execa.js b/tests/integration/utils/mock-execa.js similarity index 91% rename from tests/utils/mock-execa.js rename to tests/integration/utils/mock-execa.js index 6c908b1115a..b02aee47252 100644 --- a/tests/utils/mock-execa.js +++ b/tests/integration/utils/mock-execa.js @@ -2,7 +2,7 @@ const { writeFile } = require('fs').promises const tempy = require('tempy') -const { rmdirRecursiveAsync } = require('../../src/lib/fs') +const { rmdirRecursiveAsync } = require('../../../src/lib/fs') // Saves to disk a JavaScript file with the contents provided and returns // an environment variable that replaces the `execa` module implementation. diff --git a/tests/utils/pause.js b/tests/integration/utils/pause.js similarity index 100% rename from tests/utils/pause.js rename to tests/integration/utils/pause.js diff --git a/tests/utils/process.js b/tests/integration/utils/process.js similarity index 100% rename from tests/utils/process.js rename to tests/integration/utils/process.js diff --git a/tests/utils/site-builder.js b/tests/integration/utils/site-builder.js similarity index 98% rename from tests/utils/site-builder.js rename to tests/integration/utils/site-builder.js index 3312a8c4174..88a63858b51 100644 --- a/tests/utils/site-builder.js +++ b/tests/integration/utils/site-builder.js @@ -10,7 +10,7 @@ const tempDirectory = require('temp-dir') const { toToml } = require('tomlify-j0.4') const { v4: uuidv4 } = require('uuid') -const { rmdirRecursiveAsync } = require('../../src/lib/fs') +const { rmdirRecursiveAsync } = require('../../../src/lib/fs') const ensureDir = (file) => mkdir(file, { recursive: true }) diff --git a/tests/utils/snapshots.js b/tests/integration/utils/snapshots.js similarity index 100% rename from tests/utils/snapshots.js rename to tests/integration/utils/snapshots.js diff --git a/src/lib/completion/tests/completion.test.js b/tests/unit/lib/completion/completion.test.js similarity index 94% rename from src/lib/completion/tests/completion.test.js rename to tests/unit/lib/completion/completion.test.js index 84d1744206e..851428e9d41 100644 --- a/src/lib/completion/tests/completion.test.js +++ b/tests/unit/lib/completion/completion.test.js @@ -5,8 +5,8 @@ const test = require('ava') const { Argument } = require('commander') const sinon = require('sinon') -const { BaseCommand } = require('../../../commands/base-command') -const { getAutocompletion } = require('../script') +const { BaseCommand } = require('../../../../src/commands/base-command') +const { getAutocompletion } = require('../../../../src/lib/completion/script') const createTestCommand = () => { const program = new BaseCommand('chef') @@ -37,7 +37,7 @@ test('should generate a completion file', (t) => { const stub = sinon.stub(fs, 'writeFileSync').callsFake(() => {}) const program = createTestCommand() // eslint-disable-next-line node/global-require - const { createAutocompletion } = require('../generate-autocompletion') + const { createAutocompletion } = require('../../../../src/lib/completion/generate-autocompletion') createAutocompletion(program) // @ts-ignore diff --git a/src/lib/completion/tests/snapshots/completion.test.js.md b/tests/unit/lib/completion/snapshots/completion.test.js.md similarity index 100% rename from src/lib/completion/tests/snapshots/completion.test.js.md rename to tests/unit/lib/completion/snapshots/completion.test.js.md diff --git a/src/lib/completion/tests/snapshots/completion.test.js.snap b/tests/unit/lib/completion/snapshots/completion.test.js.snap similarity index 100% rename from src/lib/completion/tests/snapshots/completion.test.js.snap rename to tests/unit/lib/completion/snapshots/completion.test.js.snap diff --git a/src/lib/exec-fetcher.test.js b/tests/unit/lib/exec-fetcher.test.js similarity index 98% rename from src/lib/exec-fetcher.test.js rename to tests/unit/lib/exec-fetcher.test.js index ad1d4c2e1ac..4ee8bef0fbe 100644 --- a/src/lib/exec-fetcher.test.js +++ b/tests/unit/lib/exec-fetcher.test.js @@ -10,7 +10,7 @@ const sinon = require('sinon') const processSpy = {} const fetchLatestSpy = sinon.stub() -const { fetchLatestVersion, getArch, getExecName } = proxyquire('./exec-fetcher', { +const { fetchLatestVersion, getArch, getExecName } = proxyquire('../../../src/lib/exec-fetcher', { 'gh-release-fetch': { fetchLatest: fetchLatestSpy, }, diff --git a/src/lib/functions/runtimes/js/builders/tests/netlify-lambda.test.js b/tests/unit/lib/functions/runtimes/js/builders/netlify-lambda.test.js similarity index 97% rename from src/lib/functions/runtimes/js/builders/tests/netlify-lambda.test.js rename to tests/unit/lib/functions/runtimes/js/builders/netlify-lambda.test.js index 45ac9634d58..6004704b19b 100644 --- a/src/lib/functions/runtimes/js/builders/tests/netlify-lambda.test.js +++ b/tests/unit/lib/functions/runtimes/js/builders/netlify-lambda.test.js @@ -1,7 +1,7 @@ const test = require('ava') const sinon = require('sinon') -const { detectNetlifyLambda } = require('../netlify-lambda') +const { detectNetlifyLambda } = require('../../../../../../../src/lib/functions/runtimes/js/builders/netlify-lambda') test(`should not find netlify-lambda from netlify-cli package.json`, async (t) => { t.is(await detectNetlifyLambda(), false) diff --git a/src/lib/functions/scheduled.test.js b/tests/unit/lib/functions/scheduled.test.js similarity index 94% rename from src/lib/functions/scheduled.test.js rename to tests/unit/lib/functions/scheduled.test.js index 24013abea86..2bb016e8fb8 100644 --- a/src/lib/functions/scheduled.test.js +++ b/tests/unit/lib/functions/scheduled.test.js @@ -1,6 +1,6 @@ const test = require('ava') -const { buildHelpResponse } = require('./scheduled') +const { buildHelpResponse } = require('../../../../src/lib/functions/scheduled') const withAccept = (accept) => buildHelpResponse({ diff --git a/src/lib/functions/server.test.js b/tests/unit/lib/functions/server.test.js similarity index 92% rename from src/lib/functions/server.test.js rename to tests/unit/lib/functions/server.test.js index 1e7906cc3e6..e4f7aa338e0 100644 --- a/src/lib/functions/server.test.js +++ b/tests/unit/lib/functions/server.test.js @@ -6,8 +6,8 @@ const test = require('ava') const express = require('express') const request = require('supertest') -const { FunctionsRegistry } = require('./registry') -const { createHandler } = require('./server') +const { FunctionsRegistry } = require('../../../../src/lib/functions/registry') +const { createHandler } = require('../../../../src/lib/functions/server') /** @type { express.Express} */ let app diff --git a/src/lib/http-agent.test.js b/tests/unit/lib/http-agent.test.js similarity index 95% rename from src/lib/http-agent.test.js rename to tests/unit/lib/http-agent.test.js index 3e76e8ecf1a..3f7626620d9 100644 --- a/src/lib/http-agent.test.js +++ b/tests/unit/lib/http-agent.test.js @@ -4,7 +4,7 @@ const test = require('ava') const { createProxyServer } = require('http-proxy') const { HttpsProxyAgent } = require('https-proxy-agent') -const { tryGetAgent } = require('./http-agent') +const { tryGetAgent } = require('../../../src/lib/http-agent') test(`should return an empty object when there is no httpProxy`, async (t) => { t.deepEqual(await tryGetAgent({}), {}) diff --git a/src/utils/deploy/hash-files.test.js b/tests/unit/utils/deploy/hash-files.test.js similarity index 82% rename from src/utils/deploy/hash-files.test.js rename to tests/unit/utils/deploy/hash-files.test.js index 9e231ef66ca..830101ad8a2 100644 --- a/src/utils/deploy/hash-files.test.js +++ b/tests/unit/utils/deploy/hash-files.test.js @@ -1,9 +1,8 @@ const test = require('ava') -const { withSiteBuilder } = require('../../../tests/utils/site-builder') - -const { DEFAULT_CONCURRENT_HASH } = require('./constants') -const { hashFiles } = require('./hash-files') +const { DEFAULT_CONCURRENT_HASH } = require('../../../../src/utils/deploy/constants') +const { hashFiles } = require('../../../../src/utils/deploy/hash-files') +const { withSiteBuilder } = require('../../../integration/utils/site-builder') test('Hashes files in a folder', async (t) => { await withSiteBuilder('site-with-content', async (builder) => { diff --git a/src/utils/deploy/hash-fns.test.js b/tests/unit/utils/deploy/hash-fns.test.js similarity index 86% rename from src/utils/deploy/hash-fns.test.js rename to tests/unit/utils/deploy/hash-fns.test.js index d8f790a4cdb..8b4d1d954f3 100644 --- a/src/utils/deploy/hash-fns.test.js +++ b/tests/unit/utils/deploy/hash-fns.test.js @@ -2,10 +2,9 @@ const test = require('ava') const tempy = require('tempy') -const { withSiteBuilder } = require('../../../tests/utils/site-builder') - -const { DEFAULT_CONCURRENT_HASH } = require('./constants') -const { hashFns } = require('./hash-fns') +const { DEFAULT_CONCURRENT_HASH } = require('../../../../src/utils/deploy/constants') +const { hashFns } = require('../../../../src/utils/deploy/hash-fns') +const { withSiteBuilder } = require('../../../integration/utils/site-builder') test('Hashes files in a folder', async (t) => { await withSiteBuilder('site-with-functions', async (builder) => { diff --git a/src/utils/deploy/util.test.js b/tests/unit/utils/deploy/util.test.js similarity index 86% rename from src/utils/deploy/util.test.js rename to tests/unit/utils/deploy/util.test.js index 04cd8252b3a..d4046f3f3af 100644 --- a/src/utils/deploy/util.test.js +++ b/tests/unit/utils/deploy/util.test.js @@ -2,7 +2,7 @@ const { join } = require('path') const test = require('ava') -const { normalizePath } = require('./util') +const { normalizePath } = require('../../../../src/utils/deploy/util') test('normalizes relative file paths', (t) => { const input = join('foo', 'bar', 'baz.js') diff --git a/src/utils/dot-env.test.js b/tests/unit/utils/dot-env.test.js similarity index 94% rename from src/utils/dot-env.test.js rename to tests/unit/utils/dot-env.test.js index 61b44340a67..b93bc8a6728 100644 --- a/src/utils/dot-env.test.js +++ b/tests/unit/utils/dot-env.test.js @@ -2,9 +2,8 @@ const process = require('process') const test = require('ava') -const { withSiteBuilder } = require('../../tests/utils/site-builder') - -const { tryLoadDotEnvFiles } = require('./dot-env') +const { tryLoadDotEnvFiles } = require('../../../src/utils/dot-env') +const { withSiteBuilder } = require('../../integration/utils/site-builder') test('should return an empty array for a site with no .env file', async (t) => { await withSiteBuilder('site-without-env-file', async (builder) => { diff --git a/src/utils/functions/get-functions.test.js b/tests/unit/utils/functions/get-functions.test.js similarity index 94% rename from src/utils/functions/get-functions.test.js rename to tests/unit/utils/functions/get-functions.test.js index ff79f34397f..a6ba31380da 100644 --- a/src/utils/functions/get-functions.test.js +++ b/tests/unit/utils/functions/get-functions.test.js @@ -3,9 +3,8 @@ const path = require('path') const test = require('ava') const sortOn = require('sort-on') -const { withSiteBuilder } = require('../../../tests/utils/site-builder') - -const { getFunctions, getFunctionsAndWatchDirs } = require('./get-functions') +const { getFunctions, getFunctionsAndWatchDirs } = require('../../../../src/utils/functions/get-functions') +const { withSiteBuilder } = require('../../../integration/utils/site-builder') test('should return empty object when an empty string is provided', async (t) => { const funcs = await getFunctions('') @@ -87,7 +86,7 @@ test.skip('should return additional watch dirs when functions requires a file ou path: 'index.js', // eslint-disable-next-line require-await handler: async () => { - // eslint-disable-next-line node/global-require, import/no-unresolved, node/no-missing-require + // eslint-disable-next-line node/global-require, import/no-unresolved const { logHello } = require('../utils') logHello() return { statusCode: 200, body: 'Logged Hello!' } diff --git a/src/utils/get-global-config.test.js b/tests/unit/utils/get-global-config.test.js similarity index 91% rename from src/utils/get-global-config.test.js rename to tests/unit/utils/get-global-config.test.js index b107c515e35..3e6c5a6114d 100644 --- a/src/utils/get-global-config.test.js +++ b/tests/unit/utils/get-global-config.test.js @@ -4,10 +4,9 @@ const path = require('path') const test = require('ava') -const { rmdirRecursiveAsync } = require('../lib/fs') -const { getLegacyPathInHome, getPathInHome } = require('../lib/settings') - -const getGlobalConfig = require('./get-global-config') +const { rmdirRecursiveAsync } = require('../../../src/lib/fs') +const { getLegacyPathInHome, getPathInHome } = require('../../../src/lib/settings') +const getGlobalConfig = require('../../../src/utils/get-global-config') const configPath = getPathInHome(['config.json']) const legacyConfigPath = getLegacyPathInHome(['config.json']) diff --git a/src/utils/gh-auth.test.js b/tests/unit/utils/gh-auth.test.js similarity index 91% rename from src/utils/gh-auth.test.js rename to tests/unit/utils/gh-auth.test.js index 580ea052b05..fceb451a805 100644 --- a/src/utils/gh-auth.test.js +++ b/tests/unit/utils/gh-auth.test.js @@ -4,7 +4,7 @@ const fetch = require('node-fetch') const sinon = require('sinon') // eslint-disable-next-line import/order -const openBrowser = require('./open-browser') +const openBrowser = require('../../../src/utils/open-browser') // Stub needs to be required before './gh-auth' as this uses the module /** @type {string} */ let host @@ -15,7 +15,7 @@ const stubbedModule = sinon.stub(openBrowser, 'openBrowser').callsFake(({ url }) }) // eslint-disable-next-line import/order -const { authWithNetlify } = require('./gh-auth') +const { authWithNetlify } = require('../../../src/utils/gh-auth') test.after(() => { stubbedModule.restore() diff --git a/src/utils/headers.test.js b/tests/unit/utils/headers.test.js similarity index 96% rename from src/utils/headers.test.js rename to tests/unit/utils/headers.test.js index 0b098f3a5fb..42a8eb37d0d 100644 --- a/src/utils/headers.test.js +++ b/tests/unit/utils/headers.test.js @@ -2,9 +2,8 @@ const path = require('path') const test = require('ava') -const { createSiteBuilder } = require('../../tests/utils/site-builder') - -const { headersForPath, parseHeaders } = require('./headers') +const { headersForPath, parseHeaders } = require('../../../src/utils/headers') +const { createSiteBuilder } = require('../../integration/utils/site-builder') const headers = [ { path: '/', headers: ['X-Frame-Options: SAMEORIGIN'] }, diff --git a/src/utils/init/config-github.test.js b/tests/unit/utils/init/config-github.test.js similarity index 92% rename from src/utils/init/config-github.test.js rename to tests/unit/utils/init/config-github.test.js index 270322b08ca..5e8001ef76c 100644 --- a/src/utils/init/config-github.test.js +++ b/tests/unit/utils/init/config-github.test.js @@ -3,7 +3,8 @@ const octokit = require('@octokit/rest') const test = require('ava') const sinon = require('sinon') -const githubAuth = require('../gh-auth') +// eslint-disable-next-line import/order +const githubAuth = require('../../../../src/utils/gh-auth') let getAuthenticatedResponse @@ -29,7 +30,7 @@ sinon.stub(githubAuth, 'getGitHubToken').callsFake(() => }), ) -const { getGitHubToken } = require('./config-github') +const { getGitHubToken } = require('../../../../src/utils/init/config-github') // mocked configstore let globalConfig diff --git a/src/utils/parse-raw-flags.test.js b/tests/unit/utils/parse-raw-flags.test.js similarity index 89% rename from src/utils/parse-raw-flags.test.js rename to tests/unit/utils/parse-raw-flags.test.js index 0cbc8715fe4..d1dd2c708f5 100644 --- a/src/utils/parse-raw-flags.test.js +++ b/tests/unit/utils/parse-raw-flags.test.js @@ -1,6 +1,6 @@ const test = require('ava') -const { aggressiveJSONParse, parseRawFlags } = require('./parse-raw-flags') +const { aggressiveJSONParse, parseRawFlags } = require('../../../src/utils/parse-raw-flags') test.serial('JSONTruthy works with various inputs', (t) => { const testPairs = [ diff --git a/src/utils/read-repo-url.test.js b/tests/unit/utils/read-repo-url.test.js similarity index 91% rename from src/utils/read-repo-url.test.js rename to tests/unit/utils/read-repo-url.test.js index da3a0d0f861..ebcf44c23ff 100644 --- a/src/utils/read-repo-url.test.js +++ b/tests/unit/utils/read-repo-url.test.js @@ -1,6 +1,6 @@ const test = require('ava') -const { parseRepoURL } = require('./read-repo-url') +const { parseRepoURL } = require('../../../src/utils/read-repo-url') test('parseRepoURL: should parse GitHub URL', (t) => { const url = new URL('https://github.com/netlify-labs/all-the-functions/tree/master/functions/9-using-middleware') diff --git a/src/utils/redirects.test.js b/tests/unit/utils/redirects.test.js similarity index 97% rename from src/utils/redirects.test.js rename to tests/unit/utils/redirects.test.js index 20771ea1d42..4b1266801c9 100644 --- a/src/utils/redirects.test.js +++ b/tests/unit/utils/redirects.test.js @@ -1,8 +1,7 @@ const test = require('ava') -const { withSiteBuilder } = require('../../tests/utils/site-builder') - -const { parseRedirects } = require('./redirects') +const { parseRedirects } = require('../../../src/utils/redirects') +const { withSiteBuilder } = require('../../integration/utils/site-builder') const defaultConfig = { redirects: [ diff --git a/src/utils/rules-proxy.test.js b/tests/unit/utils/rules-proxy.test.js similarity index 68% rename from src/utils/rules-proxy.test.js rename to tests/unit/utils/rules-proxy.test.js index 86df9c3534e..5b2b84aab0b 100644 --- a/src/utils/rules-proxy.test.js +++ b/tests/unit/utils/rules-proxy.test.js @@ -1,6 +1,6 @@ const test = require('ava') -const { getLanguage } = require('./rules-proxy') +const { getLanguage } = require('../../../src/utils/rules-proxy') test('getLanguage', (t) => { const language = getLanguage({ 'accept-language': 'ur' }) diff --git a/src/utils/telemetry/validation.test.js b/tests/unit/utils/telemetry/validation.test.js similarity index 96% rename from src/utils/telemetry/validation.test.js rename to tests/unit/utils/telemetry/validation.test.js index 5aca9d04aca..2d3c0d50c1d 100644 --- a/src/utils/telemetry/validation.test.js +++ b/tests/unit/utils/telemetry/validation.test.js @@ -1,6 +1,6 @@ const test = require('ava') -const isValidEventName = require('./validation') +const isValidEventName = require('../../../../src/utils/telemetry/validation') const getEventForProject = (projectName, eventName) => `${projectName}:${eventName}` diff --git a/tests/utils/cli-path.js b/tests/utils/cli-path.js deleted file mode 100644 index f045b62503a..00000000000 --- a/tests/utils/cli-path.js +++ /dev/null @@ -1,5 +0,0 @@ -const path = require('path') - -const cliPath = path.resolve(__dirname, '../../bin/run') - -module.exports = cliPath diff --git a/tools/affected-test.js b/tools/affected-test.js index 13638fb6cb0..40afa4d0920 100755 --- a/tools/affected-test.js +++ b/tools/affected-test.js @@ -8,8 +8,6 @@ const { grey } = require('chalk') const execa = require('execa') const { sync } = require('fast-glob') -const { ava } = require('../package.json') - const { DependencyGraph, fileVisitor, visitorPlugins } = require('./project-graph') const getChangedFiles = async (compareTarget = 'origin/main') => { @@ -28,10 +26,14 @@ const getChangedFiles = async (compareTarget = 'origin/main') => { const getAffectedFiles = (changedFiles) => { // glob is using only posix file paths on windows we need the `\` // by using join the paths are adjusted to the operating system - const testFiles = sync(ava.files).map((filePath) => join(filePath)) + const testFiles = sync(['tests/integration/**/*.test.js']).map((filePath) => join(filePath)) // in this case all files are affected - if (changedFiles.includes('npm-shrinkwrap.json') || changedFiles.includes('package.json')) { + if ( + changedFiles.includes('npm-shrinkwrap.json') || + changedFiles.includes('package.json') || + changedFiles.includes(join('.github', 'workflows', 'main.yml')) + ) { console.log('All files are affected based on the changeset') return testFiles } diff --git a/tools/tests/file-visitor-module.test.js b/tools/tests/file-visitor-module.test.js index df7f4b0e288..cad74a7c518 100644 --- a/tools/tests/file-visitor-module.test.js +++ b/tools/tests/file-visitor-module.test.js @@ -4,7 +4,7 @@ const { format } = require('util') const test = require('ava') const mock = require('mock-fs') -const { normalize } = require('../../tests/utils/snapshots') +const { normalize } = require('../../tests/integration/utils/snapshots') const { DependencyGraph, fileVisitor } = require('../project-graph') const { esModuleMockedFileSystem } = require('./utils/file-systems') diff --git a/tools/tests/file-visitor.test.js b/tools/tests/file-visitor.test.js index 31e5347767f..431d338ab96 100644 --- a/tools/tests/file-visitor.test.js +++ b/tools/tests/file-visitor.test.js @@ -4,7 +4,7 @@ const { format } = require('util') const test = require('ava') const mock = require('mock-fs') -const { normalize } = require('../../tests/utils/snapshots') +const { normalize } = require('../../tests/integration/utils/snapshots') const { DependencyGraph, fileVisitor } = require('../project-graph') const { simpleMockedFileSystem } = require('./utils/file-systems') From cd7af03c024a412e12fb0bd5295f4fe0977c94e0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 3 Feb 2022 15:46:31 +0000 Subject: [PATCH 15/18] fix(deps): update dependency graphql to v16.3.0 (#4148) Co-authored-by: Renovate Bot Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: ehmicky Co-authored-by: Erez Rokah --- npm-shrinkwrap.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index eeb47f8437c..0b573707b0c 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -57,7 +57,7 @@ "gh-release-fetch": "^3.0.0", "git-repo-info": "^2.1.0", "gitconfiglocal": "^2.1.0", - "graphql": "^16.1.0", + "graphql": "^16.3.0", "hasbin": "^1.2.3", "hasha": "^5.2.2", "http-proxy": "^1.18.0", @@ -11144,9 +11144,9 @@ "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" }, "node_modules/graphql": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.2.0.tgz", - "integrity": "sha512-MuQd7XXrdOcmfwuLwC2jNvx0n3rxIuNYOxUtiee5XOmfrWo613ar2U8pE7aHAKh8VwfpifubpD9IP+EdEAEOsA==", + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.3.0.tgz", + "integrity": "sha512-xm+ANmA16BzCT5pLjuXySbQVFwH3oJctUVdy81w1sV0vBU0KgDdBGtxQOUd5zqOBk/JayAFeG8Dlmeq74rjm/A==", "engines": { "node": "^12.22.0 || ^14.16.0 || >=16.0.0" } @@ -29899,9 +29899,9 @@ "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" }, "graphql": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.2.0.tgz", - "integrity": "sha512-MuQd7XXrdOcmfwuLwC2jNvx0n3rxIuNYOxUtiee5XOmfrWo613ar2U8pE7aHAKh8VwfpifubpD9IP+EdEAEOsA==" + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.3.0.tgz", + "integrity": "sha512-xm+ANmA16BzCT5pLjuXySbQVFwH3oJctUVdy81w1sV0vBU0KgDdBGtxQOUd5zqOBk/JayAFeG8Dlmeq74rjm/A==" }, "graphviz": { "version": "0.0.9", From 93b64cf7c2d8beb82d893b114037c57a80892f94 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 3 Feb 2022 15:57:34 +0000 Subject: [PATCH 16/18] fix(deps): update dependency netlify-onegraph-internal to v0.0.18 (#4170) Co-authored-by: Renovate Bot Co-authored-by: Erez Rokah Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- npm-shrinkwrap.json | 16 ++++++++-------- package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 0b573707b0c..10d6f791e34 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -81,7 +81,7 @@ "multiparty": "^4.2.1", "netlify": "^10.1.2", "netlify-headers-parser": "^6.0.1", - "netlify-onegraph-internal": "0.0.16", + "netlify-onegraph-internal": "0.0.18", "netlify-redirect-parser": "^13.0.1", "netlify-redirector": "^0.2.1", "node-fetch": "^2.6.0", @@ -148,7 +148,7 @@ "tomlify-j0.4": "^3.0.0", "tree-kill": "^1.2.2", "typescript": "^4.4.4", - "verdaccio": "^5.5.2" + "verdaccio": "^5.4.0" }, "engines": { "node": "^12.20.0 || ^14.14.0 || >=16.0.0" @@ -15017,9 +15017,9 @@ } }, "node_modules/netlify-onegraph-internal": { - "version": "0.0.16", - "resolved": "https://registry.npmjs.org/netlify-onegraph-internal/-/netlify-onegraph-internal-0.0.16.tgz", - "integrity": "sha512-reQ/C7ztbYCDMXqFLSw0rBwOi866sqdBjagUs6Ug6LXDkCIGHBTjMX0iwNhEn7+/WzV4f1lJj66NhdjF5Q4/aQ==", + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/netlify-onegraph-internal/-/netlify-onegraph-internal-0.0.18.tgz", + "integrity": "sha512-dUODB7zQDj03gwXQZQPlxFAfB1NBAtAt0A/CvWkfieG0g3MuRFAT/TOQGWtFesN1HkRZbW7dnC3JR9S9jB66WQ==", "dependencies": { "graphql": "16.0.0", "node-fetch": "^2.6.0", @@ -32784,9 +32784,9 @@ } }, "netlify-onegraph-internal": { - "version": "0.0.16", - "resolved": "https://registry.npmjs.org/netlify-onegraph-internal/-/netlify-onegraph-internal-0.0.16.tgz", - "integrity": "sha512-reQ/C7ztbYCDMXqFLSw0rBwOi866sqdBjagUs6Ug6LXDkCIGHBTjMX0iwNhEn7+/WzV4f1lJj66NhdjF5Q4/aQ==", + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/netlify-onegraph-internal/-/netlify-onegraph-internal-0.0.18.tgz", + "integrity": "sha512-dUODB7zQDj03gwXQZQPlxFAfB1NBAtAt0A/CvWkfieG0g3MuRFAT/TOQGWtFesN1HkRZbW7dnC3JR9S9jB66WQ==", "requires": { "graphql": "16.0.0", "node-fetch": "^2.6.0", diff --git a/package.json b/package.json index 49d4e73219e..f2e0544483c 100644 --- a/package.json +++ b/package.json @@ -148,7 +148,7 @@ "multiparty": "^4.2.1", "netlify": "^10.1.2", "netlify-headers-parser": "^6.0.1", - "netlify-onegraph-internal": "0.0.16", + "netlify-onegraph-internal": "0.0.18", "netlify-redirect-parser": "^13.0.1", "netlify-redirector": "^0.2.1", "node-fetch": "^2.6.0", From 432774f3ede4b97e0b990b81e60d595dde520147 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 3 Feb 2022 16:10:39 +0000 Subject: [PATCH 17/18] fix(deps): update dependency lambda-local to v2.0.1 (#4051) Co-authored-by: Renovate Bot Co-authored-by: ehmicky Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- npm-shrinkwrap.json | 33 +++++++++------------------------ package.json | 2 +- 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 10d6f791e34..cff459751be 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -70,7 +70,7 @@ "is-wsl": "^2.2.0", "isexe": "^2.0.0", "jwt-decode": "^3.0.0", - "lambda-local": "2.0.0", + "lambda-local": "2.0.1", "listr": "^0.14.3", "locate-path": "^6.0.0", "lodash": "^4.17.20", @@ -13230,11 +13230,11 @@ "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" }, "node_modules/lambda-local": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lambda-local/-/lambda-local-2.0.0.tgz", - "integrity": "sha512-5Z7ZEhqVYJSm3djoq7QLDkEk7Ao+jNYbARo3nk3wtjKpgCnEbzOuraxDPDWg7OlZ4JKcsRDP+wNLeORMdbF2ow==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/lambda-local/-/lambda-local-2.0.1.tgz", + "integrity": "sha512-21AoIJYuGRPNMCEtxAOa/BP2j0fNY10IVYMQ1pRqDyhSJi5xt4r4IZUqWF40+aYU6TJ1SdB7t5s1BmSq391ILQ==", "dependencies": { - "commander": "^7.2.0", + "commander": "^8.3.0", "dotenv": "^10.0.0", "winston": "^3.3.3" }, @@ -13245,14 +13245,6 @@ "node": ">=6" } }, - "node_modules/lambda-local/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "engines": { - "node": ">= 10" - } - }, "node_modules/latest-version": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", @@ -31417,20 +31409,13 @@ "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" }, "lambda-local": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lambda-local/-/lambda-local-2.0.0.tgz", - "integrity": "sha512-5Z7ZEhqVYJSm3djoq7QLDkEk7Ao+jNYbARo3nk3wtjKpgCnEbzOuraxDPDWg7OlZ4JKcsRDP+wNLeORMdbF2ow==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/lambda-local/-/lambda-local-2.0.1.tgz", + "integrity": "sha512-21AoIJYuGRPNMCEtxAOa/BP2j0fNY10IVYMQ1pRqDyhSJi5xt4r4IZUqWF40+aYU6TJ1SdB7t5s1BmSq391ILQ==", "requires": { - "commander": "^7.2.0", + "commander": "^8.3.0", "dotenv": "^10.0.0", "winston": "^3.3.3" - }, - "dependencies": { - "commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" - } } }, "latest-version": { diff --git a/package.json b/package.json index f2e0544483c..d017fae84fd 100644 --- a/package.json +++ b/package.json @@ -137,7 +137,7 @@ "is-wsl": "^2.2.0", "isexe": "^2.0.0", "jwt-decode": "^3.0.0", - "lambda-local": "2.0.0", + "lambda-local": "2.0.1", "listr": "^0.14.3", "locate-path": "^6.0.0", "lodash": "^4.17.20", From d5d421d0cc10b449d5c5d8470bac7e9fec6cdfc7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 3 Feb 2022 16:20:55 +0000 Subject: [PATCH 18/18] fix(deps): update dependency @netlify/build to ^26.2.2 (#4138) Co-authored-by: Renovate Bot Co-authored-by: Erez Rokah Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- npm-shrinkwrap.json | 66 ++++++++++++++++++++++++++++----------------- package.json | 2 +- 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index cff459751be..735ccb00f08 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "@netlify/build": "^26.2.0", + "@netlify/build": "^26.2.2", "@netlify/config": "^17.0.6", "@netlify/framework-info": "^9.0.0", "@netlify/local-functions-proxy": "^1.1.1", @@ -2313,9 +2313,9 @@ } }, "node_modules/@netlify/build": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@netlify/build/-/build-26.2.0.tgz", - "integrity": "sha512-2yNzdO0nCRIZ5TLB7WUlis2NTSonVjairWnd1s/Tt9SBF3L3Z+IU/xSiXgAwYZ/xe+tkziUltwrx9l8rxm2VGw==", + "version": "26.2.2", + "resolved": "https://registry.npmjs.org/@netlify/build/-/build-26.2.2.tgz", + "integrity": "sha512-yHgxYTDmIJIBjWem+KmIDf9Y6Y88tRq0/alAcDQcDjYsMmWwJoKpvmMKe6Jn0x/UW413N88WjgbVi8Oxzxzc0Q==", "dependencies": { "@bugsnag/js": "^7.0.0", "@netlify/cache-utils": "^4.0.0", @@ -2323,9 +2323,9 @@ "@netlify/functions-utils": "^4.0.0", "@netlify/git-utils": "^4.0.0", "@netlify/plugin-edge-handlers": "^3.0.4", - "@netlify/plugins-list": "^6.3.0", + "@netlify/plugins-list": "^6.3.2", "@netlify/run-utils": "^4.0.0", - "@netlify/zip-it-and-ship-it": "^5.5.0", + "@netlify/zip-it-and-ship-it": "5.5.2", "@sindresorhus/slugify": "^1.1.0", "@types/node": "^16.0.0", "ansi-escapes": "^4.3.2", @@ -2911,9 +2911,9 @@ } }, "node_modules/@netlify/plugins-list": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@netlify/plugins-list/-/plugins-list-6.3.1.tgz", - "integrity": "sha512-6yAYBSELlg3oROgrFt1OCa5QwhINfkoBM2rv2Bpo7T5guwsCsEw7vI/O6nMLNDs6wGUyN43cjv2HTxaJBM/i4A==", + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/@netlify/plugins-list/-/plugins-list-6.3.2.tgz", + "integrity": "sha512-rQ6sZirWDGEJ52xGm1fMolZ9GoxTpAvql7bfzBp8qsBNj7CHbDa0iCJKvSUpNSXx82wLFhFrUHWT5MntLj/D/w==", "engines": { "node": "^12.20.0 || ^14.14.0 || >=16.0.0" } @@ -2989,11 +2989,11 @@ } }, "node_modules/@netlify/zip-it-and-ship-it": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@netlify/zip-it-and-ship-it/-/zip-it-and-ship-it-5.5.0.tgz", - "integrity": "sha512-V7hVGr8xwdiPF7kmH+TSvekorjFI/OTYe34dlEvEmPhEuDG3LejRjy2XzQ0hW5TpFXvd4V+JRPH4jMh85Xt6qg==", + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/@netlify/zip-it-and-ship-it/-/zip-it-and-ship-it-5.5.2.tgz", + "integrity": "sha512-5uBfXHYarfNeOxrW7JK4rJaq1Xk2be0Ogt/MvLDr0kuKPtPmfHyNrWmr9JlZ1qtR/SuV1t2x22Tj/mxecL8eIQ==", "dependencies": { - "@babel/parser": "^7.15.7", + "@babel/parser": "7.16.8", "@netlify/esbuild": "^0.13.6", "@vercel/nft": "^0.17.0", "archiver": "^5.3.0", @@ -3033,6 +3033,17 @@ "node": "^12.20.0 || ^14.14.0 || >=16.0.0" } }, + "node_modules/@netlify/zip-it-and-ship-it/node_modules/@babel/parser": { + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.8.tgz", + "integrity": "sha512-i7jDUfrVBWc+7OKcBzEe5n7fbv3i2fWtxKzzCvOjnzSxMfWMigAhtfJ7qzZNGFNMsCCd67+uz553dYKWXPvCKw==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@netlify/zip-it-and-ship-it/node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -23220,9 +23231,9 @@ } }, "@netlify/build": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@netlify/build/-/build-26.2.0.tgz", - "integrity": "sha512-2yNzdO0nCRIZ5TLB7WUlis2NTSonVjairWnd1s/Tt9SBF3L3Z+IU/xSiXgAwYZ/xe+tkziUltwrx9l8rxm2VGw==", + "version": "26.2.2", + "resolved": "https://registry.npmjs.org/@netlify/build/-/build-26.2.2.tgz", + "integrity": "sha512-yHgxYTDmIJIBjWem+KmIDf9Y6Y88tRq0/alAcDQcDjYsMmWwJoKpvmMKe6Jn0x/UW413N88WjgbVi8Oxzxzc0Q==", "requires": { "@bugsnag/js": "^7.0.0", "@netlify/cache-utils": "^4.0.0", @@ -23230,9 +23241,9 @@ "@netlify/functions-utils": "^4.0.0", "@netlify/git-utils": "^4.0.0", "@netlify/plugin-edge-handlers": "^3.0.4", - "@netlify/plugins-list": "^6.3.0", + "@netlify/plugins-list": "^6.3.2", "@netlify/run-utils": "^4.0.0", - "@netlify/zip-it-and-ship-it": "^5.5.0", + "@netlify/zip-it-and-ship-it": "5.5.2", "@sindresorhus/slugify": "^1.1.0", "@types/node": "^16.0.0", "ansi-escapes": "^4.3.2", @@ -23622,9 +23633,9 @@ } }, "@netlify/plugins-list": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@netlify/plugins-list/-/plugins-list-6.3.1.tgz", - "integrity": "sha512-6yAYBSELlg3oROgrFt1OCa5QwhINfkoBM2rv2Bpo7T5guwsCsEw7vI/O6nMLNDs6wGUyN43cjv2HTxaJBM/i4A==" + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/@netlify/plugins-list/-/plugins-list-6.3.2.tgz", + "integrity": "sha512-rQ6sZirWDGEJ52xGm1fMolZ9GoxTpAvql7bfzBp8qsBNj7CHbDa0iCJKvSUpNSXx82wLFhFrUHWT5MntLj/D/w==" }, "@netlify/routing-local-proxy": { "version": "0.34.1", @@ -23670,11 +23681,11 @@ } }, "@netlify/zip-it-and-ship-it": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@netlify/zip-it-and-ship-it/-/zip-it-and-ship-it-5.5.0.tgz", - "integrity": "sha512-V7hVGr8xwdiPF7kmH+TSvekorjFI/OTYe34dlEvEmPhEuDG3LejRjy2XzQ0hW5TpFXvd4V+JRPH4jMh85Xt6qg==", + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/@netlify/zip-it-and-ship-it/-/zip-it-and-ship-it-5.5.2.tgz", + "integrity": "sha512-5uBfXHYarfNeOxrW7JK4rJaq1Xk2be0Ogt/MvLDr0kuKPtPmfHyNrWmr9JlZ1qtR/SuV1t2x22Tj/mxecL8eIQ==", "requires": { - "@babel/parser": "^7.15.7", + "@babel/parser": "7.16.8", "@netlify/esbuild": "^0.13.6", "@vercel/nft": "^0.17.0", "archiver": "^5.3.0", @@ -23708,6 +23719,11 @@ "yargs": "^16.0.0" }, "dependencies": { + "@babel/parser": { + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.8.tgz", + "integrity": "sha512-i7jDUfrVBWc+7OKcBzEe5n7fbv3i2fWtxKzzCvOjnzSxMfWMigAhtfJ7qzZNGFNMsCCd67+uz553dYKWXPvCKw==" + }, "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", diff --git a/package.json b/package.json index d017fae84fd..3500b7966d6 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "prettier": "--ignore-path .gitignore --loglevel=warn \"{src,tools,scripts,site,tests,.github}/**/*.{mjs,cjs,js,md,yml,json,html}\" \"*.{mjs,cjs,js,yml,json,html}\" \".*.{mjs,cjs,js,yml,json,html}\" \"!CHANGELOG.md\" \"!npm-shrinkwrap.json\" \"!.github/**/*.md\"" }, "dependencies": { - "@netlify/build": "^26.2.0", + "@netlify/build": "^26.2.2", "@netlify/config": "^17.0.6", "@netlify/framework-info": "^9.0.0", "@netlify/local-functions-proxy": "^1.1.1",