From 7240f0236c002a69e84136a066a77e322fdd0be4 Mon Sep 17 00:00:00 2001 From: Will Conrad Date: Wed, 9 Oct 2024 11:32:27 -0500 Subject: [PATCH 01/39] feat: added logic handeling for the `env:set` command Will prompt the user if scope and/or context is not provided Co-authored-by: Thomas Lane --- src/commands/env/env-set.ts | 70 +++++++++++++++++++++++++++++++++++-- src/commands/env/env.ts | 38 ++++++++++++++++++++ 2 files changed, 105 insertions(+), 3 deletions(-) diff --git a/src/commands/env/env-set.ts b/src/commands/env/env-set.ts index daa45a2ca84..69f989af0ed 100644 --- a/src/commands/env/env-set.ts +++ b/src/commands/env/env-set.ts @@ -1,6 +1,7 @@ import { OptionValues } from 'commander' +import inquirer from 'inquirer' -import { chalk, error, log, logJson } from '../../utils/command-helpers.js' +import { chalk, error, log, exit, logJson } from '../../utils/command-helpers.js' import { AVAILABLE_CONTEXTS, AVAILABLE_SCOPES, translateFromEnvelopeToMongo } from '../../utils/env/index.js' import BaseCommand from '../base-command.js' @@ -29,6 +30,8 @@ const setInEnvelope = async ({ api, context, key, scope, secret, siteInfo, value } // fetch envelope env vars + const userData = await api.getAccount({accountId}) + log(userData) const envelopeVariables = await api.getEnvVars({ accountId, siteId }) const contexts = context || ['all'] let scopes = scope || AVAILABLE_SCOPES @@ -109,15 +112,76 @@ const setInEnvelope = async ({ api, context, key, scope, secret, siteInfo, value export const envSet = async (key: string, value: string, options: OptionValues, command: BaseCommand) => { const { context, scope, secret } = options - const { api, cachedConfig, site } = command.netlify const siteId = site.id - if (!siteId) { log('No site id found, please run inside a site folder or `netlify link`') return false } + const noForce = options.force !== true + + if (noForce) { + if (context === undefined && scope === undefined) { + log(`${chalk.redBright('Warning')}: No context defined, environent variable will be set for all contexts`) + log(`${chalk.redBright('Warning')}: No scope defined, environent variable will be set for all scopes`) + log(`${chalk.yellowBright('Notice')}: Scope setting is only available to paid Netlify accounts`) + log() + log(`${key}=${value}`) + log() + log('To skip this prompt, pass a --force flag to the delete command') + const { wantsToSet } = await inquirer.prompt({ + type: 'confirm', + name: 'wantsToSet', + message: `WARNING: Are you sure you want to set ${key}=${value} in all contexts and scopes?`, + default: false, + }) + log() + if (!wantsToSet) { + exit() + } + } else if (context === undefined) { + log(`${chalk.redBright('Warning')}: No context defined, environent variable will be set for all contexts`) + log(`${chalk.yellowBright('Notice')}: Scope setting is only available to paid Netlify accounts`) + log() + log(`${key}=${value}`) + log() + log('To skip this prompt, pass a --force flag to the delete command') + const { wantsToSet } = await inquirer.prompt({ + type: 'confirm', + name: 'wantsToSet', + message: `WARNING: Are you sure you want to set ${key}=${value} in all contexts?`, + default: false, + }) + log() + if (!wantsToSet) { + exit() + } + } else if (scope === undefined) { + log(`${chalk.redBright('Warning')}: No scopes defined, environent variable will be set for all scopes`) + log() + log(`${key}=${value}`) + log() + log('To skip this prompt, pass a --force flag to the delete command') + + const { wantsToSet } = await inquirer.prompt({ + type: 'confirm', + name: 'wantsToSet', + message: `WARNING: Are you sure you want to set ${key}=${value} in all scopes?`, + default: false, + }) + log() + if (!wantsToSet) { + exit() + } + } + } + + // Account type verification + // if (scope && freeAccount) { + // log(`${chalk.yellowBright('Notice')}: Scope setting is only available to paid Netlify accounts`) + // } + const { siteInfo } = cachedConfig // Get current environment variables set in the UI diff --git a/src/commands/env/env.ts b/src/commands/env/env.ts index 9016b4b3df2..386dbaca46e 100644 --- a/src/commands/env/env.ts +++ b/src/commands/env/env.ts @@ -103,6 +103,7 @@ export const createEnvCommand = (program: BaseCommand) => { 'runtime', ]), ) + .addOption(new Option('-f, --force', 'force the operation without warnings')) .option('--secret', 'Indicate whether the environment variable value can be read again.') .description('Set value of environment variable') .addExamples([ @@ -119,6 +120,43 @@ export const createEnvCommand = (program: BaseCommand) => { await envSet(key, value, options, command) }) + // + // - To prompt before we add an enviroment varaible + // - if context is not given, then the context will be all + // - If env:set command is run without any options, then we need to prompt the user to let them know that env will be set for all contexts and scope + // - If the user passes the -f, then we need to disable the promp + // - this will be our force flag, to bypass prompting + // + // - context and scope are undefined + // - we need to prompt the user to let them know that env will be set for all contexts and scope + // - context is defined, but scope is not + // - conext is not defined, but scope is defined + // + // + // Milestones: + // 1. If no flags are given, then we need to promp the user Y/N, + // - we need to let the user know that they are going to set the enviroment variable for all scopes and contexts + // + // 2. If the context flag is given, and the scope is not given + // - + // + // 2. The `-f` should skip the promp + // - if the user gives us a -f, then we need to skip the prmopt + // + // 3. --scope without premium + // - check the status code and message returend by the api + // - If the error message is generic + // - Check with Daniel, and see what would be the best option + // - validate the user has a premium subscription + // - if the user does not have a premium subscription, then we need to throw an error + // - we can check the api status code and message to see if the user has a premium subscription + // - infer if the env variable is not created, and the `--scope` flag is passed + // - we can let the user konw that the variable was not created, and they `--scope` is a premium feature + // - we can infer this to them + // - Netlify makes their api better with better error messages + // - add this to the documentation, + // + // program .command('env:unset') .aliases(['env:delete', 'env:remove']) From 8bc880adaab41f8dff02ef96dc45dd530badfc46 Mon Sep 17 00:00:00 2001 From: t Date: Wed, 9 Oct 2024 13:41:08 -0400 Subject: [PATCH 02/39] feat: prompt before setting env variable across context and scope Co-authored-by: Will --- src/commands/env/env-set.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/commands/env/env-set.ts b/src/commands/env/env-set.ts index 69f989af0ed..195187dfce7 100644 --- a/src/commands/env/env-set.ts +++ b/src/commands/env/env-set.ts @@ -30,8 +30,8 @@ const setInEnvelope = async ({ api, context, key, scope, secret, siteInfo, value } // fetch envelope env vars - const userData = await api.getAccount({accountId}) - log(userData) + // const userData = await api.getAccount({accountId}) + // log(userData) const envelopeVariables = await api.getEnvVars({ accountId, siteId }) const contexts = context || ['all'] let scopes = scope || AVAILABLE_SCOPES @@ -93,11 +93,19 @@ const setInEnvelope = async ({ api, context, key, scope, secret, siteInfo, value await api.updateEnvVar({ ...params, body }) } } else { + // create whole env var const body = [{ key, is_secret: secret, scopes, values }] await api.createEnvVars({ ...params, body }) } } catch (error_) { + // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'. + if (error_.json.status === 500) { + log(`${chalk.redBright('ERROR')}: Environment variable ${key} not created`) + if (scope) { + log(`${chalk.yellowBright('Notice')}: Scope setting is only available to paid Netlify accounts`) + } + } // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'. throw error_.json ? error_.json.msg : error_ } From e068e562dae5771596d8df9a81c305319aabb20d Mon Sep 17 00:00:00 2001 From: t Date: Wed, 9 Oct 2024 13:49:30 -0400 Subject: [PATCH 03/39] fix: prettier Co-authored-by: Will --- src/commands/env/env-set.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/commands/env/env-set.ts b/src/commands/env/env-set.ts index 195187dfce7..dfddb2c2bd8 100644 --- a/src/commands/env/env-set.ts +++ b/src/commands/env/env-set.ts @@ -93,7 +93,6 @@ const setInEnvelope = async ({ api, context, key, scope, secret, siteInfo, value await api.updateEnvVar({ ...params, body }) } } else { - // create whole env var const body = [{ key, is_secret: secret, scopes, values }] await api.createEnvVars({ ...params, body }) From 5b5c050eeb8a4eeb372a0ba09ac876363a9276da Mon Sep 17 00:00:00 2001 From: t Date: Wed, 9 Oct 2024 15:01:13 -0400 Subject: [PATCH 04/39] fix: refactored prompts Co-authored-by: Will --- src/commands/env/env-set.ts | 128 ++++++++++-------- src/commands/env/env.ts | 8 +- .../integration/commands/env/env-set.test.ts | 20 ++- 3 files changed, 92 insertions(+), 64 deletions(-) diff --git a/src/commands/env/env-set.ts b/src/commands/env/env-set.ts index dfddb2c2bd8..c95de00cb1f 100644 --- a/src/commands/env/env-set.ts +++ b/src/commands/env/env-set.ts @@ -5,6 +5,11 @@ import { chalk, error, log, exit, logJson } from '../../utils/command-helpers.js import { AVAILABLE_CONTEXTS, AVAILABLE_SCOPES, translateFromEnvelopeToMongo } from '../../utils/env/index.js' import BaseCommand from '../base-command.js' +type ContextScope = { + context?: string + scope?: string +} + /** * Updates the env for a site configured with Envelope with a new key/value pair * @returns {Promise} @@ -99,12 +104,13 @@ const setInEnvelope = async ({ api, context, key, scope, secret, siteInfo, value } } catch (error_) { // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'. - if (error_.json.status === 500) { + if (error_.json && error_.json.status === 500) { log(`${chalk.redBright('ERROR')}: Environment variable ${key} not created`) if (scope) { log(`${chalk.yellowBright('Notice')}: Scope setting is only available to paid Netlify accounts`) } } + // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'. throw error_.json ? error_.json.msg : error_ } @@ -117,6 +123,63 @@ const setInEnvelope = async ({ api, context, key, scope, secret, siteInfo, value } } +const generateWarningMessage = ({ context, scope }: ContextScope): string[] => { + const warningMessages: string[] = [] + + if (context === undefined) { + warningMessages.push( + `${chalk.redBright('Warning')}: No context defined, environment variable will be set for all contexts`, + ) + } + if (scope === undefined) { + warningMessages.push( + `${chalk.redBright('Warning')}: No scope defined, environment variable will be set for all scopes`, + ) + } + if (scope) { + warningMessages.push(`${chalk.yellowBright('Notice')}: Scope setting is only available to paid Netlify accounts`) + } + return warningMessages +} + +const logWarningsAndNotices = (key: string, value: string, contextScope: ContextScope): void => { + const warnings = generateWarningMessage(contextScope) + warnings.forEach((message) => log(message)) + log() + log(`${key}=${value}`) + log() + log('To skip this prompt, pass a --force flag to the delete command') +} + +const getConfirmationMessage = (key: string, value: string, { context, scope }: ContextScope) => { + let message = `WARNING: Are you sure you want to set ${key}=${value}` + + if (context == undefined && scope === undefined) { + message += ' in all contexts and scopes?' + } else if (context === undefined) { + message += ` in all contexts?` + } else if (scope === undefined) { + message += ` in all scopes?` + } + return message +} + +const confirmSetEnviroment = async (key: string, value: string, contextScope: ContextScope): Promise => { + const message = getConfirmationMessage(key, value, contextScope) + + const { wantsToSet } = await inquirer.prompt({ + type: 'confirm', + name: 'wantsToSet', + message, + default: false, + }) + + log() + if (!wantsToSet) { + exit() + } +} + export const envSet = async (key: string, value: string, options: OptionValues, command: BaseCommand) => { const { context, scope, secret } = options const { api, cachedConfig, site } = command.netlify @@ -128,67 +191,12 @@ export const envSet = async (key: string, value: string, options: OptionValues, const noForce = options.force !== true - if (noForce) { - if (context === undefined && scope === undefined) { - log(`${chalk.redBright('Warning')}: No context defined, environent variable will be set for all contexts`) - log(`${chalk.redBright('Warning')}: No scope defined, environent variable will be set for all scopes`) - log(`${chalk.yellowBright('Notice')}: Scope setting is only available to paid Netlify accounts`) - log() - log(`${key}=${value}`) - log() - log('To skip this prompt, pass a --force flag to the delete command') - const { wantsToSet } = await inquirer.prompt({ - type: 'confirm', - name: 'wantsToSet', - message: `WARNING: Are you sure you want to set ${key}=${value} in all contexts and scopes?`, - default: false, - }) - log() - if (!wantsToSet) { - exit() - } - } else if (context === undefined) { - log(`${chalk.redBright('Warning')}: No context defined, environent variable will be set for all contexts`) - log(`${chalk.yellowBright('Notice')}: Scope setting is only available to paid Netlify accounts`) - log() - log(`${key}=${value}`) - log() - log('To skip this prompt, pass a --force flag to the delete command') - const { wantsToSet } = await inquirer.prompt({ - type: 'confirm', - name: 'wantsToSet', - message: `WARNING: Are you sure you want to set ${key}=${value} in all contexts?`, - default: false, - }) - log() - if (!wantsToSet) { - exit() - } - } else if (scope === undefined) { - log(`${chalk.redBright('Warning')}: No scopes defined, environent variable will be set for all scopes`) - log() - log(`${key}=${value}`) - log() - log('To skip this prompt, pass a --force flag to the delete command') - - const { wantsToSet } = await inquirer.prompt({ - type: 'confirm', - name: 'wantsToSet', - message: `WARNING: Are you sure you want to set ${key}=${value} in all scopes?`, - default: false, - }) - log() - if (!wantsToSet) { - exit() - } - } + // Checks if -f is passed, if not, then we need to prompt the user if scope or context is not provided + if (noForce && (!context || !scope)) { + logWarningsAndNotices(key, value, { context, scope }) + await confirmSetEnviroment(key, value, { context, scope }) } - // Account type verification - // if (scope && freeAccount) { - // log(`${chalk.yellowBright('Notice')}: Scope setting is only available to paid Netlify accounts`) - // } - const { siteInfo } = cachedConfig // Get current environment variables set in the UI diff --git a/src/commands/env/env.ts b/src/commands/env/env.ts index 386dbaca46e..1216d152064 100644 --- a/src/commands/env/env.ts +++ b/src/commands/env/env.ts @@ -134,15 +134,16 @@ export const createEnvCommand = (program: BaseCommand) => { // // // Milestones: + // (Done) // 1. If no flags are given, then we need to promp the user Y/N, // - we need to let the user know that they are going to set the enviroment variable for all scopes and contexts - // + // (Done) // 2. If the context flag is given, and the scope is not given // - // // 2. The `-f` should skip the promp // - if the user gives us a -f, then we need to skip the prmopt - // + // (Done) // 3. --scope without premium // - check the status code and message returend by the api // - If the error message is generic @@ -155,7 +156,8 @@ export const createEnvCommand = (program: BaseCommand) => { // - we can infer this to them // - Netlify makes their api better with better error messages // - add this to the documentation, - // + // 4. Test + // 5. Refactor // program .command('env:unset') diff --git a/tests/integration/commands/env/env-set.test.ts b/tests/integration/commands/env/env-set.test.ts index 2eb4b9c2a68..a603b9b96d5 100644 --- a/tests/integration/commands/env/env-set.test.ts +++ b/tests/integration/commands/env/env-set.test.ts @@ -1,17 +1,35 @@ -import { describe, expect, test } from 'vitest' +import { describe, expect, test, vi } from 'vitest' +import inquirer from 'inquirer' import { FixtureTestContext, setupFixtureTests } from '../../utils/fixture.js' import routes from './api-routes.js' +vi.mock('inquirer', () => ({ + default: { + prompt: vi.fn(), + }, +})) + describe('env:list command', () => { setupFixtureTests('empty-project', { mockApi: { routes } }, () => { + // vi.mock('inquirer', () => ({ + // prompt: vi.fn().mockResolvedValue({ wantsToSet: true }), + // })) + test('should create and return new var in the dev context', async ({ fixture, mockApi }) => { + // vi.spyOn(inquirer, 'prompt').mockImplementation(() => Promise.resolve({ confirm: true })) + // const mockedInquirer = inquirer as jest.Mocked + // mockedInquirer.prompt.mockImplementation(async () => ({ wantsToSet: true })) + ;(inquirer.prompt as any).mockResolvedValue({ wantsToSet: true }) + const cliResponse = await fixture.callCli(['env:set', 'NEW_VAR', 'new-value', '--context', 'dev', '--json'], { offline: false, parseJson: true, }) + console.log(cliResponse) + expect(cliResponse).toEqual({ EXISTING_VAR: 'envelope-dev-value', OTHER_VAR: 'envelope-all-value', From f77233da1ce4dbf682422378df886e7342c4f409 Mon Sep 17 00:00:00 2001 From: t Date: Wed, 9 Oct 2024 16:08:46 -0400 Subject: [PATCH 05/39] fix: refactor prompts Co-authored-by: Will --- src/commands/env/env-set.ts | 1 + tests/integration/commands/env/env-set.test.ts | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/commands/env/env-set.ts b/src/commands/env/env-set.ts index c95de00cb1f..da1f20b9c3a 100644 --- a/src/commands/env/env-set.ts +++ b/src/commands/env/env-set.ts @@ -161,6 +161,7 @@ const getConfirmationMessage = (key: string, value: string, { context, scope }: } else if (scope === undefined) { message += ` in all scopes?` } + console.log(message) return message } diff --git a/tests/integration/commands/env/env-set.test.ts b/tests/integration/commands/env/env-set.test.ts index a603b9b96d5..cfa984da2eb 100644 --- a/tests/integration/commands/env/env-set.test.ts +++ b/tests/integration/commands/env/env-set.test.ts @@ -11,6 +11,10 @@ vi.mock('inquirer', () => ({ }, })) +vi.mock('/src/commands/env/env-set.ts', () => ({ + confirmSetEnviroment: vi.fn(), +})) + describe('env:list command', () => { setupFixtureTests('empty-project', { mockApi: { routes } }, () => { // vi.mock('inquirer', () => ({ @@ -21,7 +25,7 @@ describe('env:list command', () => { // vi.spyOn(inquirer, 'prompt').mockImplementation(() => Promise.resolve({ confirm: true })) // const mockedInquirer = inquirer as jest.Mocked // mockedInquirer.prompt.mockImplementation(async () => ({ wantsToSet: true })) - ;(inquirer.prompt as any).mockResolvedValue({ wantsToSet: true }) + vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: true }) // Simulates user answering 'Yes' const cliResponse = await fixture.callCli(['env:set', 'NEW_VAR', 'new-value', '--context', 'dev', '--json'], { offline: false, From ba4197831b18d1cd02cdee7ef45c992a7bec14f9 Mon Sep 17 00:00:00 2001 From: t Date: Wed, 9 Oct 2024 17:04:44 -0400 Subject: [PATCH 06/39] feat: env:unset prompts user before unsetting env variable indiscriminantly across contexts Co-authored-by: Will --- src/commands/env/env-unset.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/commands/env/env-unset.ts b/src/commands/env/env-unset.ts index 044a32f4942..cde2228b366 100644 --- a/src/commands/env/env-unset.ts +++ b/src/commands/env/env-unset.ts @@ -1,6 +1,6 @@ import { OptionValues } from 'commander' - -import { chalk, log, logJson } from '../../utils/command-helpers.js' +import inquirer from 'inquirer' +import { chalk, log, logJson, exit } from '../../utils/command-helpers.js' import { AVAILABLE_CONTEXTS, translateFromEnvelopeToMongo } from '../../utils/env/index.js' import BaseCommand from '../base-command.js' @@ -49,8 +49,21 @@ const unsetInEnvelope = async ({ api, context, key, siteInfo }) => { } } } else { - // otherwise, if no context passed, delete the whole key - await api.deleteEnvVar({ accountId, siteId, key }) + // otherwise, if no context passed, prompt for approval, + // then delete the whole key + log(`${chalk.redBright('Warning')}: No context defined, environment variable will be unset for all contexts`) + const message = `Are you sure you want to unset ${key}?` + const { wantsToSet } = await inquirer.prompt({ + type: 'confirm', + name: 'wantsToSet', + message, + default: false, + }) + if (wantsToSet) { + await api.deleteEnvVar({ accountId, siteId, key }) + } else { + exit() + } } } catch (error_) { // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'. From 679dcc4ca71a28d43a4208b406d7cdcca3247e92 Mon Sep 17 00:00:00 2001 From: Will Conrad Date: Wed, 9 Oct 2024 19:19:28 -0500 Subject: [PATCH 07/39] feat: created tests for env:set prompts Created several tests to check env:test prompts --- package-lock.json | 3 +- package.json | 2 +- src/commands/env/env-set.ts | 38 +-- .../integration/commands/env/env-set.test.ts | 243 ++++++++++++------ 4 files changed, 189 insertions(+), 97 deletions(-) diff --git a/package-lock.json b/package-lock.json index 075ec6791d1..85f3efe69a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -134,7 +134,7 @@ "@netlify/functions": "2.8.2", "@sindresorhus/slugify": "2.2.1", "@types/fs-extra": "11.0.4", - "@types/inquirer": "9.0.7", + "@types/inquirer": "^9.0.7", "@types/jsonwebtoken": "9.0.7", "@types/node": "20.14.8", "@types/node-fetch": "2.6.11", @@ -5745,6 +5745,7 @@ "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-9.0.7.tgz", "integrity": "sha512-Q0zyBupO6NxGRZut/JdmqYKOnN95Eg5V8Csg3PGKkP+FnvsUZx1jAyK7fztIszxxMuoBA6E3KXWvdZVXIpx60g==", "dev": true, + "license": "MIT", "dependencies": { "@types/through": "*", "rxjs": "^7.2.0" diff --git a/package.json b/package.json index 6b370c01ca3..c6f1551fa08 100644 --- a/package.json +++ b/package.json @@ -191,7 +191,7 @@ "@netlify/functions": "2.8.2", "@sindresorhus/slugify": "2.2.1", "@types/fs-extra": "11.0.4", - "@types/inquirer": "9.0.7", + "@types/inquirer": "^9.0.7", "@types/jsonwebtoken": "9.0.7", "@types/node": "20.14.8", "@types/node-fetch": "2.6.11", diff --git a/src/commands/env/env-set.ts b/src/commands/env/env-set.ts index da1f20b9c3a..121c557b10c 100644 --- a/src/commands/env/env-set.ts +++ b/src/commands/env/env-set.ts @@ -1,9 +1,11 @@ +/* eslint-disable array-callback-return */ import { OptionValues } from 'commander' import inquirer from 'inquirer' import { chalk, error, log, exit, logJson } from '../../utils/command-helpers.js' import { AVAILABLE_CONTEXTS, AVAILABLE_SCOPES, translateFromEnvelopeToMongo } from '../../utils/env/index.js' import BaseCommand from '../base-command.js' +import { printBanner } from '../../utils/banner.js' type ContextScope = { context?: string @@ -152,31 +154,33 @@ const logWarningsAndNotices = (key: string, value: string, contextScope: Context } const getConfirmationMessage = (key: string, value: string, { context, scope }: ContextScope) => { - let message = `WARNING: Are you sure you want to set ${key}=${value}` - - if (context == undefined && scope === undefined) { + let message = `${chalk.redBright('Warning')}: Are you sure you want to set ${key}=${value}` + if (context === undefined && scope === undefined) { message += ' in all contexts and scopes?' } else if (context === undefined) { message += ` in all contexts?` } else if (scope === undefined) { message += ` in all scopes?` } - console.log(message) return message } const confirmSetEnviroment = async (key: string, value: string, contextScope: ContextScope): Promise => { - const message = getConfirmationMessage(key, value, contextScope) - - const { wantsToSet } = await inquirer.prompt({ - type: 'confirm', - name: 'wantsToSet', - message, - default: false, - }) - - log() - if (!wantsToSet) { + try { + const message = getConfirmationMessage(key, value, contextScope) + const { wantsToSet } = await inquirer.prompt({ + type: 'confirm', + name: 'wantsToSet', + message, + default: false, + }) + log() + if (!wantsToSet) { + exit() + } + // eslint-disable-next-line @typescript-eslint/no-shadow + } catch (error) { + console.error(error) exit() } } @@ -189,9 +193,9 @@ export const envSet = async (key: string, value: string, options: OptionValues, log('No site id found, please run inside a site folder or `netlify link`') return false } - const noForce = options.force !== true - + log('context', context) + log('scope', scope) // Checks if -f is passed, if not, then we need to prompt the user if scope or context is not provided if (noForce && (!context || !scope)) { logWarningsAndNotices(key, value, { context, scope }) diff --git a/tests/integration/commands/env/env-set.test.ts b/tests/integration/commands/env/env-set.test.ts index cfa984da2eb..5efbdceaab8 100644 --- a/tests/integration/commands/env/env-set.test.ts +++ b/tests/integration/commands/env/env-set.test.ts @@ -1,153 +1,128 @@ -import { describe, expect, test, vi } from 'vitest' +import process from 'process' + +import chalk from 'chalk' import inquirer from 'inquirer' +import { describe, expect, test, vi, beforeEach } from 'vitest' +import BaseCommand from '../../../../src/commands/base-command.js' +import { createEnvCommand } from '../../../../src/commands/env/env.js' +import { log } from '../../../../src/utils/command-helpers.js' import { FixtureTestContext, setupFixtureTests } from '../../utils/fixture.js' +import { getEnvironmentVariables, withMockApi } from '../../utils/mock-api.js' import routes from './api-routes.js' -vi.mock('inquirer', () => ({ - default: { - prompt: vi.fn(), - }, -})) - -vi.mock('/src/commands/env/env-set.ts', () => ({ - confirmSetEnviroment: vi.fn(), +vi.mock('../../../../src/utils/command-helpers.js', async () => ({ + ...(await vi.importActual('../../../../src/utils/command-helpers.js')), + log: vi.fn(), })) -describe('env:list command', () => { +describe('env:set command', () => { setupFixtureTests('empty-project', { mockApi: { routes } }, () => { - // vi.mock('inquirer', () => ({ - // prompt: vi.fn().mockResolvedValue({ wantsToSet: true }), - // })) - test('should create and return new var in the dev context', async ({ fixture, mockApi }) => { - // vi.spyOn(inquirer, 'prompt').mockImplementation(() => Promise.resolve({ confirm: true })) - // const mockedInquirer = inquirer as jest.Mocked - // mockedInquirer.prompt.mockImplementation(async () => ({ wantsToSet: true })) - vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: true }) // Simulates user answering 'Yes' - - const cliResponse = await fixture.callCli(['env:set', 'NEW_VAR', 'new-value', '--context', 'dev', '--json'], { - offline: false, - parseJson: true, - }) - - console.log(cliResponse) - + const cliResponse = await fixture.callCli( + ['env:set', 'NEW_VAR', 'new-value', '--context', 'dev', '--json', '--force'], + { + offline: false, + parseJson: true, + }, + ) expect(cliResponse).toEqual({ EXISTING_VAR: 'envelope-dev-value', OTHER_VAR: 'envelope-all-value', NEW_VAR: 'new-value', }) - const postRequest = mockApi?.requests.find( (request) => request.method === 'POST' && request.path === '/api/v1/accounts/test-account/env', ) - expect(postRequest.body[0].key).toBe('NEW_VAR') expect(postRequest.body[0].values[0].context).toBe('dev') expect(postRequest.body[0].values[0].value).toBe('new-value') }) - test('should update an existing var in the dev context', async ({ fixture, mockApi }) => { const cliResponse = await fixture.callCli( - ['env:set', 'EXISTING_VAR', 'envelope-new-value', '--context', 'dev', '--json'], + ['env:set', 'EXISTING_VAR', 'envelope-new-value', '--context', 'dev', '--json', '--force'], { offline: false, parseJson: true, }, ) - expect(cliResponse).toEqual({ EXISTING_VAR: 'envelope-new-value', OTHER_VAR: 'envelope-all-value', }) - const patchRequest = mockApi?.requests.find( (request) => request.method === 'PATCH' && request.path === '/api/v1/accounts/test-account/env/EXISTING_VAR', ) - expect(patchRequest.body.value).toBe('envelope-new-value') expect(patchRequest.body.context).toBe('dev') }) - test('should support variadic options', async ({ fixture, mockApi }) => { const cliResponse = await fixture.callCli( - ['env:set', 'EXISTING_VAR', 'multiple', '--context', 'deploy-preview', 'production', '--json'], + ['env:set', 'EXISTING_VAR', 'multiple', '--context', 'deploy-preview', 'production', '--json', '--force'], { offline: false, parseJson: true, }, ) - expect(cliResponse).toEqual({ EXISTING_VAR: 'multiple', OTHER_VAR: 'envelope-all-value', }) - const patchRequests = mockApi?.requests.filter( (request) => request.method === 'PATCH' && request.path === '/api/v1/accounts/test-account/env/EXISTING_VAR', ) - expect(patchRequests).toHaveLength(2) - // The order of the request might not be always the same, so we need to find the request const dpRequest = patchRequests?.find((request) => request.body.context === 'deploy-preview') expect(dpRequest).not.toBeUndefined() expect(dpRequest.body.value).toBe('multiple') - const prodRequest = patchRequests?.find((request) => request.body.context === 'production') expect(prodRequest).not.toBeUndefined() expect(prodRequest.body.value).toBe('multiple') }) - test('should update existing var without flags', async ({ fixture, mockApi }) => { - const cliResponse = await fixture.callCli(['env:set', 'EXISTING_VAR', 'new-envelope-value', '--json'], { - offline: false, - parseJson: true, - }) - + const cliResponse = await fixture.callCli( + ['env:set', 'EXISTING_VAR', 'new-envelope-value', '--json', '--force'], + { + offline: false, + parseJson: true, + }, + ) expect(cliResponse).toEqual({ EXISTING_VAR: 'new-envelope-value', OTHER_VAR: 'envelope-all-value', }) - const putRequest = mockApi?.requests.find( (request) => request.method === 'PUT' && request.path === '/api/v1/accounts/test-account/env/EXISTING_VAR', ) - expect(putRequest.body.key).toBe('EXISTING_VAR') expect(putRequest.body.values[0].context).toBe('all') expect(putRequest.body.values[0].value).toBe('new-envelope-value') }) - test('should set the scope of an existing env var without needing a value', async ({ fixture, mockApi, }) => { const cliResponse = await fixture.callCli( - ['env:set', 'EXISTING_VAR', '--scope', 'runtime', 'post-processing', '--json'], + ['env:set', 'EXISTING_VAR', '--scope', 'runtime', 'post-processing', '--json', '--force'], { offline: false, parseJson: true, }, ) - expect(cliResponse).toEqual({ EXISTING_VAR: 'envelope-dev-value', OTHER_VAR: 'envelope-all-value', }) - const putRequest = mockApi?.requests.find( (request) => request.method === 'PUT' && request.path === '/api/v1/accounts/test-account/env/EXISTING_VAR', ) - expect(putRequest.body.values[0].context).toBe('production') expect(putRequest.body.values[1].context).toBe('dev') expect(putRequest.body.scopes[0]).toBe('runtime') expect(putRequest.body.scopes[1]).toBe('post-processing') }) - test('should create new secret values for multiple contexts', async ({ fixture, mockApi }) => { const cliResponse = await fixture.callCli( [ @@ -160,23 +135,21 @@ describe('env:list command', () => { 'deploy-preview', 'branch-deploy', '--json', + '--force', ], { offline: false, parseJson: true, }, ) - expect(cliResponse).toEqual({ TOTALLY_NEW_SECRET: 'shhhhhhecret', EXISTING_VAR: 'envelope-prod-value', OTHER_VAR: 'envelope-all-value', }) - const postRequest = mockApi?.requests.find( (request) => request.method === 'POST' && request.path === '/api/v1/accounts/test-account/env', ) - expect(postRequest.body).toHaveLength(1) expect(postRequest.body[0].key).toBe('TOTALLY_NEW_SECRET') expect(postRequest.body[0].is_secret).toBe(true) @@ -184,47 +157,39 @@ describe('env:list command', () => { expect(postRequest.body[0].values[0].value).toBe('shhhhhhecret') expect(postRequest.body[0].values).toHaveLength(3) }) - test('should update a single value for production context', async ({ fixture, mockApi }) => { const cliResponse = await fixture.callCli( - ['env:set', 'EXISTING_VAR', 'envelope-new-value', '--secret', '--context', 'production', '--json'], + ['env:set', 'EXISTING_VAR', 'envelope-new-value', '--secret', '--context', 'production', '--json', '--force'], { offline: false, parseJson: true, }, ) - expect(cliResponse).toEqual({ EXISTING_VAR: 'envelope-new-value', OTHER_VAR: 'envelope-all-value', }) - const patchRequest = mockApi?.requests.find( (request) => request.method === 'PATCH' && request.path === '/api/v1/accounts/test-account/env/EXISTING_VAR', ) - expect(patchRequest.body.context).toBe('production') expect(patchRequest.body.value).toBe('envelope-new-value') }) - test('should convert an `all` env var to a secret when no value is passed', async ({ fixture, mockApi, }) => { - const cliResponse = await fixture.callCli(['env:set', 'OTHER_VAR', '--secret', '--json'], { + const cliResponse = await fixture.callCli(['env:set', 'OTHER_VAR', '--secret', '--json', '--force'], { offline: false, parseJson: true, }) - expect(cliResponse).toEqual({ EXISTING_VAR: 'envelope-dev-value', OTHER_VAR: 'envelope-all-value', }) - const putRequest = mockApi?.requests.find( (request) => request.method === 'PUT' && request.path === '/api/v1/accounts/test-account/env/OTHER_VAR', ) - expect(putRequest.body.is_secret).toBe(true) expect(putRequest.body.values.length).toBe(4) expect(putRequest.body.values[0].context).toBe('production') @@ -238,25 +203,21 @@ describe('env:list command', () => { expect(putRequest.body.scopes[1]).toBe('functions') expect(putRequest.body.scopes[2]).toBe('runtime') }) - test('should convert an env var with many values to a secret when no value is passed', async ({ fixture, mockApi, }) => { - const cliResponse = await fixture.callCli(['env:set', 'EXISTING_VAR', '--secret', '--json'], { + const cliResponse = await fixture.callCli(['env:set', 'EXISTING_VAR', '--secret', '--json', '--force'], { offline: false, parseJson: true, }) - expect(cliResponse).toEqual({ EXISTING_VAR: 'envelope-dev-value', OTHER_VAR: 'envelope-all-value', }) - const putRequest = mockApi?.requests.find( (request) => request.method === 'PUT' && request.path === '/api/v1/accounts/test-account/env/EXISTING_VAR', ) - expect(putRequest.body.is_secret).toBe(true) expect(putRequest.body.values.length).toBe(2) expect(putRequest.body.values[0].context).toBe('production') @@ -267,26 +228,24 @@ describe('env:list command', () => { expect(putRequest.body.scopes[0]).toBe('builds') expect(putRequest.body.scopes[1]).toBe('functions') }) - describe('errors', () => { test.concurrent( 'should error when a value is passed without --context', async ({ fixture }) => { await expect( - fixture.callCli(['env:set', 'TOTALLY_NEW', 'cool-value', '--secret'], { + fixture.callCli(['env:set', 'TOTALLY_NEW', 'cool-value', '--secret', '--force'], { offline: false, parseJson: false, }), ).rejects.toThrowError(`please specify a non-development context`) }, ) - test.concurrent( 'should error when set with a post-processing --scope', async ({ fixture }) => { await expect( fixture.callCli( - ['env:set', 'TOTALLY_NEW', 'cool-value', '--secret', '--scope', 'builds', 'post-processing'], + ['env:set', 'TOTALLY_NEW', 'cool-value', '--secret', '--scope', 'builds', 'post-processing', '--force'], { offline: false, parseJson: false, @@ -295,12 +254,11 @@ describe('env:list command', () => { ).rejects.toThrowError(`Secret values cannot be used within the post-processing scope.`) }, ) - test.concurrent( 'should error when --scope and --context are passed on an existing env var', async ({ fixture }) => { await expect( - fixture.callCli(['env:set', 'EXISTING_VAR', '--scope', 'functions', '--context', 'production'], { + fixture.callCli(['env:set', 'EXISTING_VAR', '--scope', 'functions', '--context', 'production', '--force'], { offline: false, parseJson: false, }), @@ -310,3 +268,132 @@ describe('env:list command', () => { }) }) }) + +describe('envSet Prompts if --force flag is not passed', () => { + const envVarName = 'VAR_NAME' + const envVarValue = 'value' + + const expectedMessageNoContext = `${chalk.redBright( + 'Warning', + )}: No context defined, environment variable will be set for all contexts` + const expectedMessageNoScope = `${chalk.redBright( + 'Warning', + )}: No scope defined, environment variable will be set for all scopes` + const scopeOnlyAvailable = `${chalk.yellowBright('Notice')}: Scope setting is only available to paid Netlify accounts` + const skipConfirmationMessage = 'To skip this prompt, pass a --force flag to the delete command' + const envKeyValuePairMessage = `${envVarName}=${envVarValue}` + + beforeEach(() => { + vi.resetAllMocks() + }) + + test('should log warnings and prompts for confirmation if scope and context flag is not passed ', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const program = new BaseCommand('netlify') + + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: true }) + createEnvCommand(program) + + await program.parseAsync(['', '', 'env:set', envVarName, envVarValue]) + + expect(promptSpy).toHaveBeenCalledWith({ + type: 'confirm', + name: 'wantsToSet', + message: expect.stringContaining( + `${chalk.redBright( + 'Warning', + )}: Are you sure you want to set ${envVarName}=${envVarValue} in all contexts and scopes?`, + ), + default: false, + }) + + expect(log).toHaveBeenCalledWith(expectedMessageNoContext) + expect(log).toHaveBeenCalledWith(expectedMessageNoScope) + expect(log).toHaveBeenCalledWith(skipConfirmationMessage) + expect(log).toHaveBeenCalledWith(envKeyValuePairMessage) + }) + }) + test('should log warnings and prompts if context flag is passed but scope flag is not', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const program = new BaseCommand('netlify') + + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: true }) + createEnvCommand(program) + + await program.parseAsync(['', '', 'env:set', envVarName, envVarValue, '--context', 'production']) + + expect(promptSpy).toHaveBeenCalledWith({ + type: 'confirm', + name: 'wantsToSet', + message: expect.stringContaining( + `${chalk.redBright('Warning')}: Are you sure you want to set ${envVarName}=${envVarValue} in all scopes?`, + ), + default: false, + }) + + expect(log).toHaveBeenCalledWith(expectedMessageNoScope) + expect(log).toHaveBeenCalledWith(skipConfirmationMessage) + expect(log).toHaveBeenCalledWith(envKeyValuePairMessage) + }) + }) + + test('should log warnings and prompts if scope flag is passed but context flag is not', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const program = new BaseCommand('netlify') + + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: true }) + createEnvCommand(program) + + await program.parseAsync(['', '', 'env:set', envVarName, envVarValue, '--scope', 'runtime']) + + expect(promptSpy).toHaveBeenCalledWith({ + type: 'confirm', + name: 'wantsToSet', + message: expect.stringContaining( + `${chalk.redBright('Warning')}: Are you sure you want to set ${envVarName}=${envVarValue} in all contexts?`, + ), + default: false, + }) + + expect(log).toHaveBeenCalledWith(expectedMessageNoContext) + expect(log).toHaveBeenCalledWith(scopeOnlyAvailable) + expect(log).toHaveBeenCalledWith(skipConfirmationMessage) + expect(log).toHaveBeenCalledWith(envKeyValuePairMessage) + }) + }) + + test('should skip warnings and prompts if scope and context flag is passed', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const program = new BaseCommand('netlify') + + const promptSpy = vi.spyOn(inquirer, 'prompt') + createEnvCommand(program) + + await program.parseAsync([ + '', + '', + 'env:set', + envVarName, + envVarValue, + '--scope', + 'runtime', + '--context', + 'production', + ]) + + expect(promptSpy).not.toHaveBeenCalled() + console.log(log.mock.calls) + expect(log).toHaveBeenCalledWith(expectedMessageNoContext) + expect(log).toHaveBeenCalledWith(expectedMessageNoScope) + expect(log).toHaveBeenCalledWith(envKeyValuePairMessage) + }) + }) +}) From f19207cb718feec6b6676547622d87ef352ef6d9 Mon Sep 17 00:00:00 2001 From: Will Conrad Date: Fri, 11 Oct 2024 12:39:05 -0500 Subject: [PATCH 08/39] build: refactored env:set promts and rewrote tests created a new directory in utils called prompts, to store all future prompts. rewrote the prompts to only check for destructive actions. added tests for each of the destructive prompts Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> --- src/commands/env/env-set.ts | 88 ++-------- src/commands/env/env-unset.ts | 23 +-- src/utils/prompts/confirm-prompt.ts | 21 +++ src/utils/prompts/env-set-prompts.ts | 39 +++++ src/utils/prompts/unset-set-prompts.ts | 19 ++ .../integration/commands/env/env-set.test.ts | 165 +++++++----------- 6 files changed, 159 insertions(+), 196 deletions(-) create mode 100644 src/utils/prompts/confirm-prompt.ts create mode 100644 src/utils/prompts/env-set-prompts.ts create mode 100644 src/utils/prompts/unset-set-prompts.ts diff --git a/src/commands/env/env-set.ts b/src/commands/env/env-set.ts index 121c557b10c..98a6bd84135 100644 --- a/src/commands/env/env-set.ts +++ b/src/commands/env/env-set.ts @@ -1,23 +1,16 @@ -/* eslint-disable array-callback-return */ import { OptionValues } from 'commander' -import inquirer from 'inquirer' import { chalk, error, log, exit, logJson } from '../../utils/command-helpers.js' import { AVAILABLE_CONTEXTS, AVAILABLE_SCOPES, translateFromEnvelopeToMongo } from '../../utils/env/index.js' +import { envSetPrompts } from '../../utils/prompts/env-set-prompts.js' import BaseCommand from '../base-command.js' -import { printBanner } from '../../utils/banner.js' - -type ContextScope = { - context?: string - scope?: string -} /** * Updates the env for a site configured with Envelope with a new key/value pair * @returns {Promise} */ // @ts-expect-error TS(7031) FIXME: Binding element 'api' implicitly has an 'any' type... Remove this comment to see the full error message -const setInEnvelope = async ({ api, context, key, scope, secret, siteInfo, value }) => { +const setInEnvelope = async ({ api, context, key, noForce, scope, secret, siteInfo, value }) => { const accountId = siteInfo.account_slug const siteId = siteInfo.id @@ -58,6 +51,11 @@ const setInEnvelope = async ({ api, context, key, scope, secret, siteInfo, value // @ts-expect-error TS(7006) FIXME: Parameter 'envVar' implicitly has an 'any' type. const existing = envelopeVariables.find((envVar) => envVar.key === key) + // Checks if -f is passed, if not, then we need to prompt the user if scope or context is not provided + if (noForce && existing && (!context || !scope)) { + await envSetPrompts(key) + } + const params = { accountId, siteId, key } try { if (existing) { @@ -125,87 +123,21 @@ const setInEnvelope = async ({ api, context, key, scope, secret, siteInfo, value } } -const generateWarningMessage = ({ context, scope }: ContextScope): string[] => { - const warningMessages: string[] = [] - - if (context === undefined) { - warningMessages.push( - `${chalk.redBright('Warning')}: No context defined, environment variable will be set for all contexts`, - ) - } - if (scope === undefined) { - warningMessages.push( - `${chalk.redBright('Warning')}: No scope defined, environment variable will be set for all scopes`, - ) - } - if (scope) { - warningMessages.push(`${chalk.yellowBright('Notice')}: Scope setting is only available to paid Netlify accounts`) - } - return warningMessages -} - -const logWarningsAndNotices = (key: string, value: string, contextScope: ContextScope): void => { - const warnings = generateWarningMessage(contextScope) - warnings.forEach((message) => log(message)) - log() - log(`${key}=${value}`) - log() - log('To skip this prompt, pass a --force flag to the delete command') -} - -const getConfirmationMessage = (key: string, value: string, { context, scope }: ContextScope) => { - let message = `${chalk.redBright('Warning')}: Are you sure you want to set ${key}=${value}` - if (context === undefined && scope === undefined) { - message += ' in all contexts and scopes?' - } else if (context === undefined) { - message += ` in all contexts?` - } else if (scope === undefined) { - message += ` in all scopes?` - } - return message -} - -const confirmSetEnviroment = async (key: string, value: string, contextScope: ContextScope): Promise => { - try { - const message = getConfirmationMessage(key, value, contextScope) - const { wantsToSet } = await inquirer.prompt({ - type: 'confirm', - name: 'wantsToSet', - message, - default: false, - }) - log() - if (!wantsToSet) { - exit() - } - // eslint-disable-next-line @typescript-eslint/no-shadow - } catch (error) { - console.error(error) - exit() - } -} - export const envSet = async (key: string, value: string, options: OptionValues, command: BaseCommand) => { const { context, scope, secret } = options const { api, cachedConfig, site } = command.netlify const siteId = site.id + if (!siteId) { log('No site id found, please run inside a site folder or `netlify link`') return false } - const noForce = options.force !== true - log('context', context) - log('scope', scope) - // Checks if -f is passed, if not, then we need to prompt the user if scope or context is not provided - if (noForce && (!context || !scope)) { - logWarningsAndNotices(key, value, { context, scope }) - await confirmSetEnviroment(key, value, { context, scope }) - } + const noForce = options.force !== true const { siteInfo } = cachedConfig // Get current environment variables set in the UI - const finalEnv = await setInEnvelope({ api, siteInfo, key, value, context, scope, secret }) + const finalEnv = await setInEnvelope({ api, siteInfo, key, noForce, value, context, scope, secret }) if (!finalEnv) { return false diff --git a/src/commands/env/env-unset.ts b/src/commands/env/env-unset.ts index cde2228b366..5d7da2210a7 100644 --- a/src/commands/env/env-unset.ts +++ b/src/commands/env/env-unset.ts @@ -1,9 +1,9 @@ import { OptionValues } from 'commander' -import inquirer from 'inquirer' + import { chalk, log, logJson, exit } from '../../utils/command-helpers.js' import { AVAILABLE_CONTEXTS, translateFromEnvelopeToMongo } from '../../utils/env/index.js' +import { envUnsetPrompts } from '../../utils/prompts/unset-set-prompts.js' import BaseCommand from '../base-command.js' - /** * Deletes a given key from the env of a site configured with Envelope * @returns {Promise} @@ -26,6 +26,8 @@ const unsetInEnvelope = async ({ api, context, key, siteInfo }) => { return env } + await envUnsetPrompts(key) + const params = { accountId, siteId, key } try { if (context) { @@ -49,21 +51,8 @@ const unsetInEnvelope = async ({ api, context, key, siteInfo }) => { } } } else { - // otherwise, if no context passed, prompt for approval, - // then delete the whole key - log(`${chalk.redBright('Warning')}: No context defined, environment variable will be unset for all contexts`) - const message = `Are you sure you want to unset ${key}?` - const { wantsToSet } = await inquirer.prompt({ - type: 'confirm', - name: 'wantsToSet', - message, - default: false, - }) - if (wantsToSet) { - await api.deleteEnvVar({ accountId, siteId, key }) - } else { - exit() - } + // otherwise, if no context passed, delete the whole key + await api.deleteEnvVar({ accountId, siteId, key }) } } catch (error_) { // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'. diff --git a/src/utils/prompts/confirm-prompt.ts b/src/utils/prompts/confirm-prompt.ts new file mode 100644 index 00000000000..dc3c36426c6 --- /dev/null +++ b/src/utils/prompts/confirm-prompt.ts @@ -0,0 +1,21 @@ +import inquirer from 'inquirer' + +import { log, exit } from '../command-helpers.js' + +export const confirmPrompt = async (message: string): Promise => { + try { + const { wantsToSet } = await inquirer.prompt({ + type: 'confirm', + name: 'wantsToSet', + message, + default: false, + }) + log() + if (!wantsToSet) { + exit() + } + } catch (error) { + console.error(error) + exit() + } +} diff --git a/src/utils/prompts/env-set-prompts.ts b/src/utils/prompts/env-set-prompts.ts new file mode 100644 index 00000000000..3dbb5b248b8 --- /dev/null +++ b/src/utils/prompts/env-set-prompts.ts @@ -0,0 +1,39 @@ +import { chalk, log } from '../command-helpers.js' + +import { confirmPrompt } from './confirm-prompt.js' + +// const generateMessage = ({ context, scope }: ContextScope, variableName: string): void => { +// log() +// log(`${chalk.redBright('Warning')}: The environment variable ${variableName} already exists!`) + +// if (!context && !scope) { +// log(`${chalk.redBright('Warning')}: No context or scope defined - this will apply to ALL contexts and ALL scopes`) +// } else if (!context) { +// log(`${chalk.redBright('Warning')}: No context defined - this will apply to ALL contexts`) +// } else if (!scope) { +// log(`${chalk.redBright('Warning')}: No scope defined - this will apply to ALL scopes`) +// } + +// log() +// log(`• New Context: ${context || 'ALL'}`) +// log(`• New Scope: ${scope || 'ALL'}`) +// log() +// log(`${chalk.yellowBright('Notice')}: To skip this prompt, pass a -f or --force flag`) +// log() +// } + +const generateSetMessage = (variableName: string): void => { + log() + log(`${chalk.redBright('Warning')}: The environment variable ${chalk.bgBlueBright(variableName)} already exists!`) + log() + log( + `${chalk.yellowBright( + 'Notice', + )}: To overwrite the existing variable without confirmation, pass the -f or --force flag.`, + ) +} + +export const envSetPrompts = async (key: string): Promise => { + generateSetMessage(key) + await confirmPrompt('The environment variable already exists. Do you want to overwrite it?') +} diff --git a/src/utils/prompts/unset-set-prompts.ts b/src/utils/prompts/unset-set-prompts.ts new file mode 100644 index 00000000000..bf99557c316 --- /dev/null +++ b/src/utils/prompts/unset-set-prompts.ts @@ -0,0 +1,19 @@ +import { chalk, log } from '../command-helpers.js' + +import { confirmPrompt } from './confirm-prompt.js' + +const generateUnsetMessage = (variableName: string): void => { + log() + log( + `${chalk.redBright('Warning')}: The environment variable ${chalk.bgBlueBright( + variableName, + )} will be unset (deleted)!`, + ) + log() + log(`${chalk.yellowBright('Notice')}: To unset the variable without confirmation, pass the -f or --force flag.`) +} + +export const envUnsetPrompts = async (key: string): Promise => { + generateUnsetMessage(key) + await confirmPrompt('Are you sure you want to unset (delete) the environment variable?') +} diff --git a/tests/integration/commands/env/env-set.test.ts b/tests/integration/commands/env/env-set.test.ts index 5efbdceaab8..352d78c0fcf 100644 --- a/tests/integration/commands/env/env-set.test.ts +++ b/tests/integration/commands/env/env-set.test.ts @@ -267,133 +267,96 @@ describe('env:set command', () => { ) }) }) -}) - -describe('envSet Prompts if --force flag is not passed', () => { - const envVarName = 'VAR_NAME' - const envVarValue = 'value' - const expectedMessageNoContext = `${chalk.redBright( - 'Warning', - )}: No context defined, environment variable will be set for all contexts` - const expectedMessageNoScope = `${chalk.redBright( - 'Warning', - )}: No scope defined, environment variable will be set for all scopes` - const scopeOnlyAvailable = `${chalk.yellowBright('Notice')}: Scope setting is only available to paid Netlify accounts` - const skipConfirmationMessage = 'To skip this prompt, pass a --force flag to the delete command' - const envKeyValuePairMessage = `${envVarName}=${envVarValue}` + describe.only('user is prompted to confirm when setting an env var that already exists', () => { + // already exists as value in withMockApi + const existingVar = 'EXISTING_VAR' + const newEnvValue = 'value' - beforeEach(() => { - vi.resetAllMocks() - }) + const expectedMessageAlreadyExists = `${chalk.redBright('Warning')}: The environment variable ${chalk.bgBlueBright( + existingVar, + )} already exists!` - test('should log warnings and prompts for confirmation if scope and context flag is not passed ', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + const expectedNoticeMessage = `${chalk.yellowBright( + 'Notice', + )}: To overwrite the existing variable without confirmation, pass the -f or --force flag.` - const program = new BaseCommand('netlify') + const expectedSuccessMessage = `Set environment variable ${chalk.yellow( + `${existingVar}=${newEnvValue}`, + )} in the ${chalk.magenta('all')} context` - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: true }) - createEnvCommand(program) + beforeEach(() => { + vi.resetAllMocks() + }) - await program.parseAsync(['', '', 'env:set', envVarName, envVarValue]) + test('should log warnings and prompts if enviroment variable already exists', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - expect(promptSpy).toHaveBeenCalledWith({ - type: 'confirm', - name: 'wantsToSet', - message: expect.stringContaining( - `${chalk.redBright( - 'Warning', - )}: Are you sure you want to set ${envVarName}=${envVarValue} in all contexts and scopes?`, - ), - default: false, - }) + const program = new BaseCommand('netlify') - expect(log).toHaveBeenCalledWith(expectedMessageNoContext) - expect(log).toHaveBeenCalledWith(expectedMessageNoScope) - expect(log).toHaveBeenCalledWith(skipConfirmationMessage) - expect(log).toHaveBeenCalledWith(envKeyValuePairMessage) - }) - }) - test('should log warnings and prompts if context flag is passed but scope flag is not', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: true }) - const program = new BaseCommand('netlify') + createEnvCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: true }) - createEnvCommand(program) + await program.parseAsync(['', '', 'env:set', existingVar, newEnvValue]) - await program.parseAsync(['', '', 'env:set', envVarName, envVarValue, '--context', 'production']) + expect(promptSpy).toHaveBeenCalledWith({ + type: 'confirm', + name: 'wantsToSet', + message: expect.stringContaining('The environment variable already exists. Do you want to overwrite it?'), + default: false, + }) - expect(promptSpy).toHaveBeenCalledWith({ - type: 'confirm', - name: 'wantsToSet', - message: expect.stringContaining( - `${chalk.redBright('Warning')}: Are you sure you want to set ${envVarName}=${envVarValue} in all scopes?`, - ), - default: false, + expect(log).toHaveBeenCalledWith(expectedMessageAlreadyExists) + expect(log).toHaveBeenCalledWith(expectedNoticeMessage) + expect(log).toHaveBeenCalledWith(expectedSuccessMessage) }) - - expect(log).toHaveBeenCalledWith(expectedMessageNoScope) - expect(log).toHaveBeenCalledWith(skipConfirmationMessage) - expect(log).toHaveBeenCalledWith(envKeyValuePairMessage) }) - }) - test('should log warnings and prompts if scope flag is passed but context flag is not', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + test('should skip warnings and prompts if enviroment variable does not exist', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const program = new BaseCommand('netlify') + const program = new BaseCommand('netlify') - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: true }) - createEnvCommand(program) + const promptSpy = vi.spyOn(inquirer, 'prompt') - await program.parseAsync(['', '', 'env:set', envVarName, envVarValue, '--scope', 'runtime']) + createEnvCommand(program) - expect(promptSpy).toHaveBeenCalledWith({ - type: 'confirm', - name: 'wantsToSet', - message: expect.stringContaining( - `${chalk.redBright('Warning')}: Are you sure you want to set ${envVarName}=${envVarValue} in all contexts?`, - ), - default: false, - }) + await program.parseAsync(['', '', 'env:set', 'NEW_ENV_VAR', 'NEW_VALUE']) - expect(log).toHaveBeenCalledWith(expectedMessageNoContext) - expect(log).toHaveBeenCalledWith(scopeOnlyAvailable) - expect(log).toHaveBeenCalledWith(skipConfirmationMessage) - expect(log).toHaveBeenCalledWith(envKeyValuePairMessage) + expect(promptSpy).not.toHaveBeenCalled() + + expect(log).not.toHaveBeenCalledWith(expectedMessageAlreadyExists) + expect(log).not.toHaveBeenCalledWith(expectedNoticeMessage) + expect(log).toHaveBeenCalledWith( + `Set environment variable ${chalk.yellow(`NEW_ENV_VAR=NEW_VALUE`)} in the ${chalk.magenta('all')} context`, + ) + }) }) - }) - test('should skip warnings and prompts if scope and context flag is passed', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + test('should skip warnings and prompts if -f flag is passed', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const program = new BaseCommand('netlify') - const program = new BaseCommand('netlify') + const promptSpy = vi.spyOn(inquirer, 'prompt') - const promptSpy = vi.spyOn(inquirer, 'prompt') - createEnvCommand(program) + try { + await program.parseAsync(['', '', 'env:set', existingVar, newEnvValue, '-f']) + } catch (error) { + // We expect the process to exit, so this is fine + expect(error.message).toContain('process.exit unexpectedly called') + } - await program.parseAsync([ - '', - '', - 'env:set', - envVarName, - envVarValue, - '--scope', - 'runtime', - '--context', - 'production', - ]) + expect(promptSpy).not.toHaveBeenCalled() - expect(promptSpy).not.toHaveBeenCalled() - console.log(log.mock.calls) - expect(log).toHaveBeenCalledWith(expectedMessageNoContext) - expect(log).toHaveBeenCalledWith(expectedMessageNoScope) - expect(log).toHaveBeenCalledWith(envKeyValuePairMessage) + expect(log).not.toHaveBeenCalledWith(expectedMessageAlreadyExists) + expect(log).not.toHaveBeenCalledWith(expectedNoticeMessage) + expect(log).not.toHaveBeenCalledWith(expectedSuccessMessage) + }) }) }) }) From a40438207b13d3048134e3d4f461d3cf50b0768d Mon Sep 17 00:00:00 2001 From: Will Conrad Date: Fri, 11 Oct 2024 18:14:50 -0500 Subject: [PATCH 09/39] feat: added prompt for env:clone and tests Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> --- src/commands/env/env-clone.ts | 11 +- src/commands/env/env-set.ts | 25 ++- src/commands/env/env-unset.ts | 10 +- src/commands/env/env.ts | 43 +---- src/utils/prompts/env-clone-prompt.ts | 49 +++++ src/utils/prompts/env-set-prompts.ts | 20 --- tests/integration/commands/env/api-routes.ts | 57 ++++++ .../commands/env/env-clone.test.ts | 169 ++++++++++++++++++ .../integration/commands/env/env-set.test.ts | 33 +++- .../commands/env/env-unset.test.ts | 146 +++++++++++++-- 10 files changed, 465 insertions(+), 98 deletions(-) create mode 100644 src/utils/prompts/env-clone-prompt.ts create mode 100644 tests/integration/commands/env/env-clone.test.ts diff --git a/src/commands/env/env-clone.ts b/src/commands/env/env-clone.ts index 1d06a515eed..1d904eef836 100644 --- a/src/commands/env/env-clone.ts +++ b/src/commands/env/env-clone.ts @@ -1,6 +1,7 @@ import { OptionValues } from 'commander' import { chalk, log, error as logError } from '../../utils/command-helpers.js' +import { envClonePrompts } from '../../utils/prompts/env-clone-prompt.js' import BaseCommand from '../base-command.js' // @ts-expect-error TS(7006) FIXME: Parameter 'api' implicitly has an 'any' type. @@ -18,7 +19,7 @@ const safeGetSite = async (api, siteId) => { * @returns {Promise} */ // @ts-expect-error TS(7031) FIXME: Binding element 'api' implicitly has an 'any' type... Remove this comment to see the full error message -const cloneEnvVars = async ({ api, siteFrom, siteTo }): Promise => { +const cloneEnvVars = async ({ api, siteFrom, siteTo, skip }): Promise => { const [envelopeFrom, envelopeTo] = await Promise.all([ api.getEnvVars({ accountId: siteFrom.account_slug, siteId: siteFrom.id }), api.getEnvVars({ accountId: siteTo.account_slug, siteId: siteTo.id }), @@ -36,6 +37,10 @@ const cloneEnvVars = async ({ api, siteFrom, siteTo }): Promise => { const siteId = siteTo.id // @ts-expect-error TS(7031) FIXME: Binding element 'key' implicitly has an 'any' type... Remove this comment to see the full error message const envVarsToDelete = envelopeTo.filter(({ key }) => keysFrom.includes(key)) + + if (envVarsToDelete.length !== 0 && Boolean(skip) === false) { + await envClonePrompts(siteTo.id, envVarsToDelete) + } // delete marked env vars in parallel // @ts-expect-error TS(7031) FIXME: Binding element 'key' implicitly has an 'any' type... Remove this comment to see the full error message await Promise.all(envVarsToDelete.map(({ key }) => api.deleteEnvVar({ accountId, siteId, key }))) @@ -47,12 +52,12 @@ const cloneEnvVars = async ({ api, siteFrom, siteTo }): Promise => { // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'. throw error.json ? error.json.msg : error } - return true } export const envClone = async (options: OptionValues, command: BaseCommand) => { const { api, site } = command.netlify + const { skip } = options if (!site.id && !options.from) { log( @@ -81,7 +86,7 @@ export const envClone = async (options: OptionValues, command: BaseCommand) => { return false } - const success = await cloneEnvVars({ api, siteFrom, siteTo }) + const success = await cloneEnvVars({ api, siteFrom, siteTo, skip }) if (!success) { return false diff --git a/src/commands/env/env-set.ts b/src/commands/env/env-set.ts index 98a6bd84135..e10f495152f 100644 --- a/src/commands/env/env-set.ts +++ b/src/commands/env/env-set.ts @@ -10,7 +10,7 @@ import BaseCommand from '../base-command.js' * @returns {Promise} */ // @ts-expect-error TS(7031) FIXME: Binding element 'api' implicitly has an 'any' type... Remove this comment to see the full error message -const setInEnvelope = async ({ api, context, key, noForce, scope, secret, siteInfo, value }) => { +const setInEnvelope = async ({ api, context, force, key, scope, secret, siteInfo, value }) => { const accountId = siteInfo.account_slug const siteId = siteInfo.id @@ -51,8 +51,8 @@ const setInEnvelope = async ({ api, context, key, noForce, scope, secret, siteIn // @ts-expect-error TS(7006) FIXME: Parameter 'envVar' implicitly has an 'any' type. const existing = envelopeVariables.find((envVar) => envVar.key === key) - // Checks if -f is passed, if not, then we need to prompt the user if scope or context is not provided - if (noForce && existing && (!context || !scope)) { + // Checks if -f is passed and if it is an existing variaible, then we need to prompt the user + if (Boolean(force) === false && existing) { await envSetPrompts(key) } @@ -103,13 +103,12 @@ const setInEnvelope = async ({ api, context, key, noForce, scope, secret, siteIn await api.createEnvVars({ ...params, body }) } } catch (error_) { - // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'. - if (error_.json && error_.json.status === 500) { - log(`${chalk.redBright('ERROR')}: Environment variable ${key} not created`) - if (scope) { - log(`${chalk.yellowBright('Notice')}: Scope setting is only available to paid Netlify accounts`) - } - } + // if (error_.json && error_.json.status === 500) { + // log(`${chalk.redBright('ERROR')}: Environment variable ${key} not created`) + // if (scope) { + // log(`${chalk.yellowBright('Notice')}: Scope setting is only available to paid Netlify accounts`) + // } + // } // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'. throw error_.json ? error_.json.msg : error_ @@ -124,20 +123,18 @@ const setInEnvelope = async ({ api, context, key, noForce, scope, secret, siteIn } export const envSet = async (key: string, value: string, options: OptionValues, command: BaseCommand) => { - const { context, scope, secret } = options + const { context, force, scope, secret } = options const { api, cachedConfig, site } = command.netlify const siteId = site.id - if (!siteId) { log('No site id found, please run inside a site folder or `netlify link`') return false } - const noForce = options.force !== true const { siteInfo } = cachedConfig // Get current environment variables set in the UI - const finalEnv = await setInEnvelope({ api, siteInfo, key, noForce, value, context, scope, secret }) + const finalEnv = await setInEnvelope({ api, siteInfo, force, key, value, context, scope, secret }) if (!finalEnv) { return false diff --git a/src/commands/env/env-unset.ts b/src/commands/env/env-unset.ts index 5d7da2210a7..958df29e310 100644 --- a/src/commands/env/env-unset.ts +++ b/src/commands/env/env-unset.ts @@ -9,7 +9,7 @@ import BaseCommand from '../base-command.js' * @returns {Promise} */ // @ts-expect-error TS(7031) FIXME: Binding element 'api' implicitly has an 'any' type... Remove this comment to see the full error message -const unsetInEnvelope = async ({ api, context, key, siteInfo }) => { +const unsetInEnvelope = async ({ api, context, force, key, siteInfo }) => { const accountId = siteInfo.account_slug const siteId = siteInfo.id // fetch envelope env vars @@ -26,7 +26,9 @@ const unsetInEnvelope = async ({ api, context, key, siteInfo }) => { return env } - await envUnsetPrompts(key) + if (Boolean(force) === false) { + await envUnsetPrompts(key) + } const params = { accountId, siteId, key } try { @@ -66,7 +68,7 @@ const unsetInEnvelope = async ({ api, context, key, siteInfo }) => { } export const envUnset = async (key: string, options: OptionValues, command: BaseCommand) => { - const { context } = options + const { context, force } = options const { api, cachedConfig, site } = command.netlify const siteId = site.id @@ -77,7 +79,7 @@ export const envUnset = async (key: string, options: OptionValues, command: Base const { siteInfo } = cachedConfig - const finalEnv = await unsetInEnvelope({ api, context, siteInfo, key }) + const finalEnv = await unsetInEnvelope({ api, context, force, siteInfo, key }) // Return new environment variables of site if using json flag if (options.json) { diff --git a/src/commands/env/env.ts b/src/commands/env/env.ts index 1216d152064..98e34476d7b 100644 --- a/src/commands/env/env.ts +++ b/src/commands/env/env.ts @@ -103,7 +103,7 @@ export const createEnvCommand = (program: BaseCommand) => { 'runtime', ]), ) - .addOption(new Option('-f, --force', 'force the operation without warnings')) + .option('-f, --force', 'force the operation without warnings') .option('--secret', 'Indicate whether the environment variable value can be read again.') .description('Set value of environment variable') .addExamples([ @@ -120,45 +120,6 @@ export const createEnvCommand = (program: BaseCommand) => { await envSet(key, value, options, command) }) - // - // - To prompt before we add an enviroment varaible - // - if context is not given, then the context will be all - // - If env:set command is run without any options, then we need to prompt the user to let them know that env will be set for all contexts and scope - // - If the user passes the -f, then we need to disable the promp - // - this will be our force flag, to bypass prompting - // - // - context and scope are undefined - // - we need to prompt the user to let them know that env will be set for all contexts and scope - // - context is defined, but scope is not - // - conext is not defined, but scope is defined - // - // - // Milestones: - // (Done) - // 1. If no flags are given, then we need to promp the user Y/N, - // - we need to let the user know that they are going to set the enviroment variable for all scopes and contexts - // (Done) - // 2. If the context flag is given, and the scope is not given - // - - // - // 2. The `-f` should skip the promp - // - if the user gives us a -f, then we need to skip the prmopt - // (Done) - // 3. --scope without premium - // - check the status code and message returend by the api - // - If the error message is generic - // - Check with Daniel, and see what would be the best option - // - validate the user has a premium subscription - // - if the user does not have a premium subscription, then we need to throw an error - // - we can check the api status code and message to see if the user has a premium subscription - // - infer if the env variable is not created, and the `--scope` flag is passed - // - we can let the user konw that the variable was not created, and they `--scope` is a premium feature - // - we can infer this to them - // - Netlify makes their api better with better error messages - // - add this to the documentation, - // 4. Test - // 5. Refactor - // program .command('env:unset') .aliases(['env:delete', 'env:remove']) @@ -170,6 +131,7 @@ export const createEnvCommand = (program: BaseCommand) => { // @ts-expect-error TS(7006) FIXME: Parameter 'context' implicitly has an 'any' type. (context, previous = []) => [...previous, normalizeContext(context)], ) + .option('-f, --force', 'force the operation without warnings') .addExamples([ 'netlify env:unset VAR_NAME # unset in all contexts', 'netlify env:unset VAR_NAME --context production', @@ -185,6 +147,7 @@ export const createEnvCommand = (program: BaseCommand) => { .command('env:clone') .alias('env:migrate') .option('-f, --from ', 'Site ID (From)') + .option('-s, --skip', 'Skip prompts') .requiredOption('-t, --to ', 'Site ID (To)') .description(`Clone environment variables from one site to another`) .addExamples(['netlify env:clone --to ', 'netlify env:clone --to --from ']) diff --git a/src/utils/prompts/env-clone-prompt.ts b/src/utils/prompts/env-clone-prompt.ts new file mode 100644 index 00000000000..7534aef923c --- /dev/null +++ b/src/utils/prompts/env-clone-prompt.ts @@ -0,0 +1,49 @@ +import { chalk, log } from '../command-helpers.js' + +import { confirmPrompt } from './confirm-prompt.js' + +type User = { + id: string + email: string + avatar_url: string + full_name: string +} + +type EnvVar = { + key: string + scopes: string[] + values: Record[] + updated_at: string + updated_by: User + is_secret: boolean +} + +const generateSetMessage = (envVarsToDelete: EnvVar[], siteId: string): void => { + log() + log( + `${chalk.redBright( + 'Warning', + )}: The following environment variables are already set on the site with ID ${chalk.bgBlueBright( + siteId, + )}. They will be overwritten!`, + ) + log() + + log(`${chalk.yellowBright('Notice')}: The following variables will be overwritten:`) + log() + envVarsToDelete.forEach((envVar) => { + log(envVar.key) + }) + + log() + log( + `${chalk.yellowBright( + 'Notice', + )}: To overwrite the existing variables without confirmation prompts, pass the -s or --skip flag.`, + ) +} + +export const envClonePrompts = async (siteId: string, envVarsToDelete: EnvVar[]): Promise => { + generateSetMessage(envVarsToDelete, siteId) + await confirmPrompt('Do you want to proceed with overwriting these variables?') +} diff --git a/src/utils/prompts/env-set-prompts.ts b/src/utils/prompts/env-set-prompts.ts index 3dbb5b248b8..306761f1d19 100644 --- a/src/utils/prompts/env-set-prompts.ts +++ b/src/utils/prompts/env-set-prompts.ts @@ -2,26 +2,6 @@ import { chalk, log } from '../command-helpers.js' import { confirmPrompt } from './confirm-prompt.js' -// const generateMessage = ({ context, scope }: ContextScope, variableName: string): void => { -// log() -// log(`${chalk.redBright('Warning')}: The environment variable ${variableName} already exists!`) - -// if (!context && !scope) { -// log(`${chalk.redBright('Warning')}: No context or scope defined - this will apply to ALL contexts and ALL scopes`) -// } else if (!context) { -// log(`${chalk.redBright('Warning')}: No context defined - this will apply to ALL contexts`) -// } else if (!scope) { -// log(`${chalk.redBright('Warning')}: No scope defined - this will apply to ALL scopes`) -// } - -// log() -// log(`• New Context: ${context || 'ALL'}`) -// log(`• New Scope: ${scope || 'ALL'}`) -// log() -// log(`${chalk.yellowBright('Notice')}: To skip this prompt, pass a -f or --force flag`) -// log() -// } - const generateSetMessage = (variableName: string): void => { log() log(`${chalk.redBright('Warning')}: The environment variable ${chalk.bgBlueBright(variableName)} already exists!`) diff --git a/tests/integration/commands/env/api-routes.ts b/tests/integration/commands/env/api-routes.ts index 83bb95d00d8..b0394b5d7b8 100644 --- a/tests/integration/commands/env/api-routes.ts +++ b/tests/integration/commands/env/api-routes.ts @@ -8,6 +8,25 @@ const siteInfo = { id: 'site_id', name: 'site-name', } + +const secondSiteInfo = { + account_slug: 'test-account-2', + build_settings: { + env: {}, + }, + id: 'site_id_2', + name: 'site-name-2', +} + +const thirdSiteInfo = { + account_slug: 'test-account-3', + build_settings: { + env: {}, + }, + id: 'site_id_3', + name: 'site-name-3', +} + const existingVar = { key: 'EXISTING_VAR', scopes: ['builds', 'functions'], @@ -24,6 +43,7 @@ const existingVar = { }, ], } + const otherVar = { key: 'OTHER_VAR', scopes: ['builds', 'functions', 'runtime', 'post_processing'], @@ -35,9 +55,14 @@ const otherVar = { }, ], } + const response = [existingVar, otherVar] +const secondSiteResponse = [existingVar] + const routes = [ { path: 'sites/site_id', response: siteInfo }, + { path: 'sites/site_id_2', response: secondSiteInfo }, + { path: 'sites/site_id_3', response: thirdSiteInfo }, { path: 'sites/site_id/service-instances', response: [] }, { path: 'accounts', @@ -59,11 +84,33 @@ const routes = [ path: 'accounts/test-account/env', response, }, + { + path: 'accounts/test-account-2/env/EXISTING_VAR', + response: existingVar, + }, + { + path: 'accounts/test-account-2/env', + response: secondSiteResponse, + }, + { + path: 'accounts/test-account-3/env', + response: [{}], + }, { path: 'accounts/test-account/env', method: HTTPMethod.POST, response: {}, }, + { + path: 'accounts/test-account-2/env', + method: HTTPMethod.POST, + response: {}, + }, + { + path: 'accounts/test-account-3/env', + method: HTTPMethod.POST, + response: {}, + }, { path: 'accounts/test-account/env/EXISTING_VAR', method: HTTPMethod.PUT, @@ -79,6 +126,16 @@ const routes = [ method: HTTPMethod.DELETE, response: {}, }, + { + path: 'accounts/test-account-2/env/EXISTING_VAR', + method: HTTPMethod.DELETE, + response: {}, + }, + { + path: 'accounts/test-account-3/env/EXISTING_VAR', + method: HTTPMethod.DELETE, + response: {}, + }, { path: 'accounts/test-account/env/EXISTING_VAR/value/1234', method: HTTPMethod.DELETE, diff --git a/tests/integration/commands/env/env-clone.test.ts b/tests/integration/commands/env/env-clone.test.ts new file mode 100644 index 00000000000..62321b747e5 --- /dev/null +++ b/tests/integration/commands/env/env-clone.test.ts @@ -0,0 +1,169 @@ +import process from 'process' + +import chalk from 'chalk' +import inquirer from 'inquirer' +import { describe, expect, test, vi, beforeEach } from 'vitest' + +import BaseCommand from '../../../../src/commands/base-command.js' +import { createEnvCommand } from '../../../../src/commands/env/env.js' +import { log } from '../../../../src/utils/command-helpers.js' +import { FixtureTestContext, setupFixtureTests } from '../../utils/fixture.js' +import { getEnvironmentVariables, withMockApi } from '../../utils/mock-api.js' + +import routes from './api-routes.js' + +vi.mock('../../../../src/utils/command-helpers.js', async () => ({ + ...(await vi.importActual('../../../../src/utils/command-helpers.js')), + log: vi.fn(), +})) + +describe('env:clone command', () => { + setupFixtureTests('empty-project', { mockApi: { routes } }, () => { + test.only('should create and return new var in the dev context', async ({ + fixture, + mockApi, + }) => { + const cliResponse = await fixture.callCli(['env:clone', '-t', 'site_id_3', '--skip'], { + offline: false, + parseJson: false, + }) + + console.log(cliResponse) + const postRequest = mockApi?.requests.find( + (request) => request.method === 'POST' && request.path === '/api/v1/accounts/test-account/env', + ) + + console.log(mockApi?.requests.map((request) => request.method)) + // expect(postRequest.body[0].key).toBe('NEW_VAR') + // expect(postRequest.body[0].values[0].context).toBe('dev') + // expect(postRequest.body[0].values[0].value).toBe('new-value') + }) + }) +}) + +describe('user is prompted to confirm when setting an env var that already exists', () => { + const sharedEnvVars = 'EXISTING_VAR' + const siteIdTwo = 'site_id_2' + const warningMessage = `${chalk.redBright( + 'Warning', + )}: The following environment variables are already set on the site with ID ${chalk.bgBlueBright( + siteIdTwo, + )}. They will be overwritten!` + const expectedNoticeMessage = `${chalk.yellowBright('Notice')}: The following variables will be overwritten:` + + const expectedSkipMessage = `${chalk.yellowBright( + 'Notice', + )}: To overwrite the existing variables without confirmation prompts, pass the -s or --skip flag.` + const expectedSuccessMessage = `Successfully cloned environment variables from ${chalk.green( + 'site-name', + )} to ${chalk.green('site-name-2')}` + + beforeEach(() => { + vi.resetAllMocks() + }) + + test('should log warnings and prompts if enviroment variable already exists', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const program = new BaseCommand('netlify') + createEnvCommand(program) + + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: true }) + + await program.parseAsync(['', '', 'env:clone', '-t', 'site_id_2']) + + expect(promptSpy).toHaveBeenCalledWith({ + type: 'confirm', + name: 'wantsToSet', + message: expect.stringContaining('Do you want to proceed with overwriting these variables?'), + default: false, + }) + + expect(log).toHaveBeenCalledWith( + `${chalk.redBright( + 'Warning', + )}: The following environment variables are already set on the site with ID ${chalk.bgBlueBright( + 'site_id_2', + )}. They will be overwritten!`, + ) + + expect(log).toHaveBeenCalledWith(warningMessage) + expect(log).toHaveBeenCalledWith(expectedNoticeMessage) + expect(log).toHaveBeenCalledWith(sharedEnvVars) + expect(log).toHaveBeenCalledWith(expectedSkipMessage) + expect(log).toHaveBeenCalledWith(expectedSuccessMessage) + }) + }) + + test('should skip warnings and prompts if -s flag is passed', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const program = new BaseCommand('netlify') + createEnvCommand(program) + + const promptSpy = vi.spyOn(inquirer, 'prompt') + + await program.parseAsync(['', '', 'env:clone', '-s', '-t', 'site_id_2']) + + expect(promptSpy).not.toHaveBeenCalled() + + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(expectedNoticeMessage) + expect(log).not.toHaveBeenCalledWith(sharedEnvVars) + expect(log).not.toHaveBeenCalledWith(expectedSkipMessage) + expect(log).toHaveBeenCalledWith(expectedSuccessMessage) + }) + }) + + test('should exit user reponds is no to confirmatnion prompt', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const program = new BaseCommand('netlify') + createEnvCommand(program) + + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: false }) + + try { + await program.parseAsync(['', '', 'env:clone', '-t', 'site_id_2']) + } catch (error) { + // We expect the process to exit, so this is fine + expect(error.message).toContain('process.exit unexpectedly called') + } + + expect(promptSpy).toHaveBeenCalled() + + expect(log).toHaveBeenCalledWith(warningMessage) + expect(log).toHaveBeenCalledWith(expectedNoticeMessage) + expect(log).toHaveBeenCalledWith(sharedEnvVars) + expect(log).toHaveBeenCalledWith(expectedSkipMessage) + expect(log).not.toHaveBeenCalledWith(expectedSuccessMessage) + }) + }) + + test('should not run prompts if sites have no enviroment variables in common', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + const expectedSuccessMessageSite3 = `Successfully cloned environment variables from ${chalk.green( + 'site-name', + )} to ${chalk.green('site-name-3')}` + + const program = new BaseCommand('netlify') + createEnvCommand(program) + + const promptSpy = vi.spyOn(inquirer, 'prompt') + + await program.parseAsync(['', '', 'env:clone', '-t', 'site_id_3']) + + expect(promptSpy).not.toHaveBeenCalled() + + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(expectedNoticeMessage) + expect(log).not.toHaveBeenCalledWith(sharedEnvVars) + expect(log).not.toHaveBeenCalledWith(expectedSkipMessage) + expect(log).toHaveBeenCalledWith(expectedSuccessMessageSite3) + }) + }) +}) diff --git a/tests/integration/commands/env/env-set.test.ts b/tests/integration/commands/env/env-set.test.ts index 352d78c0fcf..7c711292157 100644 --- a/tests/integration/commands/env/env-set.test.ts +++ b/tests/integration/commands/env/env-set.test.ts @@ -268,7 +268,7 @@ describe('env:set command', () => { }) }) - describe.only('user is prompted to confirm when setting an env var that already exists', () => { + describe('user is prompted to confirm when setting an env var that already exists', () => { // already exists as value in withMockApi const existingVar = 'EXISTING_VAR' const newEnvValue = 'value' @@ -319,11 +319,10 @@ describe('env:set command', () => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) const program = new BaseCommand('netlify') + createEnvCommand(program) const promptSpy = vi.spyOn(inquirer, 'prompt') - createEnvCommand(program) - await program.parseAsync(['', '', 'env:set', 'NEW_ENV_VAR', 'NEW_VALUE']) expect(promptSpy).not.toHaveBeenCalled() @@ -341,20 +340,40 @@ describe('env:set command', () => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) const program = new BaseCommand('netlify') + createEnvCommand(program) const promptSpy = vi.spyOn(inquirer, 'prompt') + await program.parseAsync(['', '', 'env:set', existingVar, newEnvValue, '-f']) + + expect(promptSpy).not.toHaveBeenCalled() + + expect(log).not.toHaveBeenCalledWith(expectedMessageAlreadyExists) + expect(log).not.toHaveBeenCalledWith(expectedNoticeMessage) + expect(log).toHaveBeenCalledWith(expectedSuccessMessage) + }) + }) + + test('should exit user reponds is no to confirmatnion prompt', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const program = new BaseCommand('netlify') + createEnvCommand(program) + + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: false }) + try { - await program.parseAsync(['', '', 'env:set', existingVar, newEnvValue, '-f']) + await program.parseAsync(['', '', 'env:set', existingVar, newEnvValue]) } catch (error) { // We expect the process to exit, so this is fine expect(error.message).toContain('process.exit unexpectedly called') } - expect(promptSpy).not.toHaveBeenCalled() + expect(promptSpy).toHaveBeenCalled() - expect(log).not.toHaveBeenCalledWith(expectedMessageAlreadyExists) - expect(log).not.toHaveBeenCalledWith(expectedNoticeMessage) + expect(log).toHaveBeenCalledWith(expectedMessageAlreadyExists) + expect(log).toHaveBeenCalledWith(expectedNoticeMessage) expect(log).not.toHaveBeenCalledWith(expectedSuccessMessage) }) }) diff --git a/tests/integration/commands/env/env-unset.test.ts b/tests/integration/commands/env/env-unset.test.ts index 7a5d5aae73d..ecb0e1fab53 100644 --- a/tests/integration/commands/env/env-unset.test.ts +++ b/tests/integration/commands/env/env-unset.test.ts @@ -1,13 +1,26 @@ -import { describe, expect, test } from 'vitest' +import process from 'process' +import chalk from 'chalk' +import inquirer from 'inquirer' +import { describe, expect, test, vi, beforeEach } from 'vitest' + +import BaseCommand from '../../../../src/commands/base-command.js' +import { createEnvCommand } from '../../../../src/commands/env/env.js' +import { log } from '../../../../src/utils/command-helpers.js' import { FixtureTestContext, setupFixtureTests } from '../../utils/fixture.js' +import { getEnvironmentVariables, withMockApi } from '../../utils/mock-api.js' import routes from './api-routes.js' +vi.mock('../../../../src/utils/command-helpers.js', async () => ({ + ...(await vi.importActual('../../../../src/utils/command-helpers.js')), + log: vi.fn(), +})) + describe('env:unset command', () => { setupFixtureTests('empty-project', { mockApi: { routes } }, () => { test('should remove existing variable', async ({ fixture, mockApi }) => { - const cliResponse = await fixture.callCli(['env:unset', '--json', 'EXISTING_VAR'], { + const cliResponse = await fixture.callCli(['env:unset', '--json', 'EXISTING_VAR', '--force'], { offline: false, parseJson: true, }) @@ -22,10 +35,13 @@ describe('env:unset command', () => { }) test('should remove existing variable value', async ({ fixture, mockApi }) => { - const cliResponse = await fixture.callCli(['env:unset', 'EXISTING_VAR', '--context', 'production', '--json'], { - offline: false, - parseJson: true, - }) + const cliResponse = await fixture.callCli( + ['env:unset', 'EXISTING_VAR', '--context', 'production', '--json', '--force'], + { + offline: false, + parseJson: true, + }, + ) expect(cliResponse).toEqual({ OTHER_VAR: 'envelope-all-value', @@ -37,10 +53,13 @@ describe('env:unset command', () => { }) test('should split up an `all` value', async ({ fixture, mockApi }) => { - const cliResponse = await fixture.callCli(['env:unset', 'OTHER_VAR', '--context', 'branch-deploy', '--json'], { - offline: false, - parseJson: true, - }) + const cliResponse = await fixture.callCli( + ['env:unset', 'OTHER_VAR', '--context', 'branch-deploy', '--json', '--force'], + { + offline: false, + parseJson: true, + }, + ) expect(cliResponse).toEqual({}) @@ -55,4 +74,111 @@ describe('env:unset command', () => { expect(patchRequests).toHaveLength(3) }) }) + + describe('user is prompted to confirm when unsetting an env var that already exists', () => { + // already exists as value in withMockApi + const existingVar = 'EXISTING_VAR' + + const expectedWarningMessage = `${chalk.redBright('Warning')}: The environment variable ${chalk.bgBlueBright( + existingVar, + )} will be unset (deleted)!` + + const expectedNoticeMessage = `${chalk.yellowBright( + 'Notice', + )}: To unset the variable without confirmation, pass the -f or --force flag.` + + const expectedSuccessMessage = `Unset environment variable ${chalk.yellow(`${existingVar}`)} in the ${chalk.magenta( + 'all', + )} context` + + beforeEach(() => { + vi.resetAllMocks() + }) + + test('should log warnings and prompts if enviroment variable already exists', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const program = new BaseCommand('netlify') + createEnvCommand(program) + + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: true }) + + await program.parseAsync(['', '', 'env:unset', existingVar]) + + expect(promptSpy).toHaveBeenCalledWith({ + type: 'confirm', + name: 'wantsToSet', + message: expect.stringContaining('Are you sure you want to unset (delete) the environment variable?'), + default: false, + }) + + expect(log).toHaveBeenCalledWith(expectedWarningMessage) + expect(log).toHaveBeenCalledWith(expectedNoticeMessage) + expect(log).toHaveBeenCalledWith(expectedSuccessMessage) + }) + }) + + test('should skip warnings and prompts if -f flag is passed', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const program = new BaseCommand('netlify') + createEnvCommand(program) + + const promptSpy = vi.spyOn(inquirer, 'prompt') + + await program.parseAsync(['', '', 'env:unset', existingVar, '-f']) + + expect(promptSpy).not.toHaveBeenCalled() + + expect(log).not.toHaveBeenCalledWith(expectedWarningMessage) + expect(log).not.toHaveBeenCalledWith(expectedNoticeMessage) + expect(log).toHaveBeenCalledWith(expectedSuccessMessage) + }) + }) + + test('should exit user reponds is no to confirmatnion prompt', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const program = new BaseCommand('netlify') + createEnvCommand(program) + + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: false }) + + try { + await program.parseAsync(['', '', 'env:unset', existingVar]) + } catch (error) { + // We expect the process to exit, so this is fine + expect(error.message).toContain('process.exit unexpectedly called') + } + + expect(promptSpy).toHaveBeenCalled() + + expect(log).toHaveBeenCalledWith(expectedWarningMessage) + expect(log).toHaveBeenCalledWith(expectedNoticeMessage) + expect(log).not.toHaveBeenCalledWith(expectedSuccessMessage) + }) + }) + + test('should not run prompts if enviroment variable does not exist', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const program = new BaseCommand('netlify') + createEnvCommand(program) + + const promptSpy = vi.spyOn(inquirer, 'prompt') + + await program.parseAsync(['', '', 'env:unset', 'NEW_ENV_VAR']) + + expect(promptSpy).not.toHaveBeenCalled() + + expect(log).not.toHaveBeenCalledWith(expectedWarningMessage) + expect(log).not.toHaveBeenCalledWith(expectedNoticeMessage) + expect(log).not.toHaveBeenCalledWith(expectedSuccessMessage) + }) + }) + }) }) From 1d6c8f6f7251b2182f4122280c963a198cda2fc4 Mon Sep 17 00:00:00 2001 From: Will Conrad Date: Fri, 11 Oct 2024 18:19:49 -0500 Subject: [PATCH 10/39] fix: prettier fix Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> --- .../integration/commands/env/env-clone.test.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/integration/commands/env/env-clone.test.ts b/tests/integration/commands/env/env-clone.test.ts index 62321b747e5..6d7f7e4ab4a 100644 --- a/tests/integration/commands/env/env-clone.test.ts +++ b/tests/integration/commands/env/env-clone.test.ts @@ -28,15 +28,15 @@ describe('env:clone command', () => { parseJson: false, }) - console.log(cliResponse) - const postRequest = mockApi?.requests.find( - (request) => request.method === 'POST' && request.path === '/api/v1/accounts/test-account/env', - ) - - console.log(mockApi?.requests.map((request) => request.method)) - // expect(postRequest.body[0].key).toBe('NEW_VAR') - // expect(postRequest.body[0].values[0].context).toBe('dev') - // expect(postRequest.body[0].values[0].value).toBe('new-value') + // console.log(cliResponse) + // const postRequest = mockApi?.requests.find( + // (request) => request.method === 'POST' && request.path === '/api/v1/accounts/test-account/env', + // ) + + // console.log(mockApi?.requests.map((request) => request.method)) + // // expect(postRequest.body[0].key).toBe('NEW_VAR') + // // expect(postRequest.body[0].values[0].context).toBe('dev') + // // expect(postRequest.body[0].values[0].value).toBe('new-value') }) }) }) From 4ebe87f25a0dbc12bd6e9eaae58b2e4dc6c95e24 Mon Sep 17 00:00:00 2001 From: Will Conrad Date: Mon, 14 Oct 2024 15:28:19 -0500 Subject: [PATCH 11/39] build: added prompts and tests for blob command for blobl:set and blob:delete Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> --- src/commands/blobs/blobs-delete.ts | 11 +- src/commands/blobs/blobs-set.ts | 17 +- src/commands/blobs/blobs.ts | 2 + src/commands/env/env-clone.ts | 10 +- src/commands/env/env.ts | 2 +- src/utils/prompts/blob-delete-prompts.ts | 20 ++ src/utils/prompts/blob-set-prompt.ts | 19 ++ src/utils/prompts/env-clone-prompt.ts | 2 +- .../commands/blobs/blobs-delete.test.ts | 183 ++++++++++++++ .../commands/blobs/blobs-set.test.ts | 228 ++++++++++++++++++ .../integration/commands/blobs/blobs.test.ts | 6 +- .../commands/env/env-clone.test.ts | 219 ++++++++--------- 12 files changed, 584 insertions(+), 135 deletions(-) create mode 100644 src/utils/prompts/blob-delete-prompts.ts create mode 100644 src/utils/prompts/blob-set-prompt.ts create mode 100644 tests/integration/commands/blobs/blobs-delete.test.ts create mode 100644 tests/integration/commands/blobs/blobs-set.test.ts diff --git a/src/commands/blobs/blobs-delete.ts b/src/commands/blobs/blobs-delete.ts index e26ffa813c3..c430a947a26 100644 --- a/src/commands/blobs/blobs-delete.ts +++ b/src/commands/blobs/blobs-delete.ts @@ -1,12 +1,15 @@ import { getStore } from '@netlify/blobs' -import { chalk, error as printError } from '../../utils/command-helpers.js' +import { chalk, error as printError, log } from '../../utils/command-helpers.js' +import { blobDeletePrompts } from '../../utils/prompts/blob-delete-prompts.js' /** * The blobs:delete command */ export const blobsDelete = async (storeName: string, key: string, _options: Record, command: any) => { const { api, siteInfo } = command.netlify + const { force } = _options + const store = getStore({ apiURL: `${api.scheme}://${api.host}`, name: storeName, @@ -14,8 +17,14 @@ export const blobsDelete = async (storeName: string, key: string, _options: Reco token: api.accessToken ?? '', }) + if (force === undefined) { + await blobDeletePrompts(key, storeName) + } + try { await store.delete(key) + + log(`${chalk.greenBright('Success')}: Blob ${chalk.yellow(key)} deleted from store ${chalk.yellow(storeName)}`) } catch { return printError(`Could not delete blob ${chalk.yellow(key)} from store ${chalk.yellow(storeName)}`) } diff --git a/src/commands/blobs/blobs-set.ts b/src/commands/blobs/blobs-set.ts index e4e76bdf53b..da81e5a14f3 100644 --- a/src/commands/blobs/blobs-set.ts +++ b/src/commands/blobs/blobs-set.ts @@ -4,11 +4,13 @@ import { resolve } from 'path' import { getStore } from '@netlify/blobs' import { OptionValues } from 'commander' -import { chalk, error as printError, isNodeError } from '../../utils/command-helpers.js' +import { chalk, error as printError, isNodeError, log } from '../../utils/command-helpers.js' +import { blobSetPrompts } from '../../utils/prompts/blob-set-prompt.js' import BaseCommand from '../base-command.js' interface Options extends OptionValues { input?: string + force?: string } export const blobsSet = async ( @@ -19,19 +21,17 @@ export const blobsSet = async ( command: BaseCommand, ) => { const { api, siteInfo } = command.netlify - const { input } = options + const { force, input } = options const store = getStore({ apiURL: `${api.scheme}://${api.host}`, name: storeName, siteID: siteInfo.id ?? '', token: api.accessToken ?? '', }) - let value = valueParts.join(' ') if (input) { const inputPath = resolve(input) - try { value = await fs.readFile(inputPath, 'utf8') } catch (error) { @@ -57,8 +57,17 @@ export const blobsSet = async ( ) } + if (force === undefined) { + const existingValue = await store.get(key) + + if (existingValue) { + await blobSetPrompts(key, storeName) + } + } + try { await store.set(key, value) + log(`${chalk.greenBright('Success')}: Blob ${chalk.yellow(key)} set in store ${chalk.yellow(storeName)}`) } catch { return printError(`Could not set blob ${chalk.yellow(key)} in store ${chalk.yellow(storeName)}`) } diff --git a/src/commands/blobs/blobs.ts b/src/commands/blobs/blobs.ts index f3a3a1ddeff..e77e19cae2e 100644 --- a/src/commands/blobs/blobs.ts +++ b/src/commands/blobs/blobs.ts @@ -19,6 +19,7 @@ export const createBlobsCommand = (program: BaseCommand) => { .description(`Deletes an object with a given key, if it exists, from a Netlify Blobs store`) .argument('', 'Name of the store') .argument('', 'Object key') + .option('-f, --force', 'Force the operation without warnings') .alias('blob:delete') .hook('preAction', requiresSiteInfo) .action(async (storeName: string, key: string, _options: OptionValues, command: BaseCommand) => { @@ -70,6 +71,7 @@ export const createBlobsCommand = (program: BaseCommand) => { .argument('', 'Object key') .argument('[value...]', 'Object value') .option('-i, --input ', 'Defines the filesystem path where the blob data should be read from') + .option('-f, --force', 'Force the operation without warnings') .alias('blob:set') .hook('preAction', requiresSiteInfo) diff --git a/src/commands/env/env-clone.ts b/src/commands/env/env-clone.ts index 1d904eef836..78d14a8d05a 100644 --- a/src/commands/env/env-clone.ts +++ b/src/commands/env/env-clone.ts @@ -19,7 +19,7 @@ const safeGetSite = async (api, siteId) => { * @returns {Promise} */ // @ts-expect-error TS(7031) FIXME: Binding element 'api' implicitly has an 'any' type... Remove this comment to see the full error message -const cloneEnvVars = async ({ api, siteFrom, siteTo, skip }): Promise => { +const cloneEnvVars = async ({ api, force, siteFrom, siteTo }): Promise => { const [envelopeFrom, envelopeTo] = await Promise.all([ api.getEnvVars({ accountId: siteFrom.account_slug, siteId: siteFrom.id }), api.getEnvVars({ accountId: siteTo.account_slug, siteId: siteTo.id }), @@ -37,8 +37,8 @@ const cloneEnvVars = async ({ api, siteFrom, siteTo, skip }): Promise = const siteId = siteTo.id // @ts-expect-error TS(7031) FIXME: Binding element 'key' implicitly has an 'any' type... Remove this comment to see the full error message const envVarsToDelete = envelopeTo.filter(({ key }) => keysFrom.includes(key)) - - if (envVarsToDelete.length !== 0 && Boolean(skip) === false) { + + if (envVarsToDelete.length !== 0 && Boolean(force) === false) { await envClonePrompts(siteTo.id, envVarsToDelete) } // delete marked env vars in parallel @@ -57,7 +57,7 @@ const cloneEnvVars = async ({ api, siteFrom, siteTo, skip }): Promise = export const envClone = async (options: OptionValues, command: BaseCommand) => { const { api, site } = command.netlify - const { skip } = options + const { force } = options if (!site.id && !options.from) { log( @@ -86,7 +86,7 @@ export const envClone = async (options: OptionValues, command: BaseCommand) => { return false } - const success = await cloneEnvVars({ api, siteFrom, siteTo, skip }) + const success = await cloneEnvVars({ api, siteFrom, siteTo, force }) if (!success) { return false diff --git a/src/commands/env/env.ts b/src/commands/env/env.ts index 98e34476d7b..7babb1d4eb7 100644 --- a/src/commands/env/env.ts +++ b/src/commands/env/env.ts @@ -147,7 +147,7 @@ export const createEnvCommand = (program: BaseCommand) => { .command('env:clone') .alias('env:migrate') .option('-f, --from ', 'Site ID (From)') - .option('-s, --skip', 'Skip prompts') + .option('--force', 'force the operation without warnings') .requiredOption('-t, --to ', 'Site ID (To)') .description(`Clone environment variables from one site to another`) .addExamples(['netlify env:clone --to ', 'netlify env:clone --to --from ']) diff --git a/src/utils/prompts/blob-delete-prompts.ts b/src/utils/prompts/blob-delete-prompts.ts new file mode 100644 index 00000000000..eccc4c14f37 --- /dev/null +++ b/src/utils/prompts/blob-delete-prompts.ts @@ -0,0 +1,20 @@ +import { chalk, log } from '../command-helpers.js' + +import { confirmPrompt } from './confirm-prompt.js' + +const generateBlobWarningMessage = (key: string, storeName: string): void => { + log() + log( + `${chalk.redBright('Warning')}: The following blob key ${chalk.cyan(key)} will be deleted from store ${chalk.cyan( + storeName, + )}:`, + ) + log() + log(`${chalk.yellowBright('Notice')}: To overwrite without this warning, you can use the --force flag.`) + log() +} + +export const blobDeletePrompts = async (key: string, storeName: string): Promise => { + generateBlobWarningMessage(key, storeName) + await confirmPrompt('Do you want to proceed with deleting the value at this key?') +} diff --git a/src/utils/prompts/blob-set-prompt.ts b/src/utils/prompts/blob-set-prompt.ts new file mode 100644 index 00000000000..5f5a1d1a46c --- /dev/null +++ b/src/utils/prompts/blob-set-prompt.ts @@ -0,0 +1,19 @@ +import { chalk, log } from '../command-helpers.js' + +import { confirmPrompt } from './confirm-prompt.js' + +const generateBlobWarningMessage = (key: string, storeName: string): void => { + log() + log(`${chalk.redBright('Warning')}: The following blob key already exists in store ${chalk.cyan(storeName)}:`) + log() + log(`${chalk.bold(key)}`) + log() + log(`This operation will ${chalk.redBright('overwrite')} the existing value.`) + log(`${chalk.yellowBright('Notice')}: To overwrite without this warning, you can use the --force flag.`) + log() +} + +export const blobSetPrompts = async (key: string, storeName: string): Promise => { + generateBlobWarningMessage(key, storeName) + await confirmPrompt('Do you want to proceed with overwriting this blob key existing value?') +} diff --git a/src/utils/prompts/env-clone-prompt.ts b/src/utils/prompts/env-clone-prompt.ts index 7534aef923c..fbcf67000e6 100644 --- a/src/utils/prompts/env-clone-prompt.ts +++ b/src/utils/prompts/env-clone-prompt.ts @@ -39,7 +39,7 @@ const generateSetMessage = (envVarsToDelete: EnvVar[], siteId: string): void => log( `${chalk.yellowBright( 'Notice', - )}: To overwrite the existing variables without confirmation prompts, pass the -s or --skip flag.`, + )}: To overwrite the existing variables without confirmation prompts, pass the --force flag.`, ) } diff --git a/tests/integration/commands/blobs/blobs-delete.test.ts b/tests/integration/commands/blobs/blobs-delete.test.ts new file mode 100644 index 00000000000..dc387c0bd7f --- /dev/null +++ b/tests/integration/commands/blobs/blobs-delete.test.ts @@ -0,0 +1,183 @@ +import process from 'process' + +import { getStore } from '@netlify/blobs' +import chalk from 'chalk' +import inquirer from 'inquirer' +import { describe, expect, test, vi, beforeEach } from 'vitest' + +import BaseCommand from '../../../../src/commands/base-command.js' +import { createBlobsCommand } from '../../../../src/commands/blobs/blobs.js' +import { log } from '../../../../src/utils/command-helpers.js' +import { Route } from '../../utils/mock-api-vitest.js' +import { getEnvironmentVariables, withMockApi } from '../../utils/mock-api.js' + +const siteInfo = { + account_slug: 'test-account', + id: 'site_id', + name: 'site-name', + feature_flags: { + edge_functions_npm_support: true, + }, + functions_config: { timeout: 1 }, +} + +vi.mock('../../../../src/utils/command-helpers.js', async () => ({ + ...(await vi.importActual('../../../../src/utils/command-helpers.js')), + log: vi.fn(), +})) + +vi.mock('@netlify/blobs', () => ({ + getStore: vi.fn(), +})) + +const routes: Route[] = [ + { path: 'sites/site_id', response: siteInfo }, + + { path: 'sites/site_id/service-instances', response: [] }, + { + path: 'accounts', + response: [{ slug: siteInfo.account_slug }], + }, +] + +describe('blob:delete command', () => { + const storeName = 'my-store' + const key = 'my-key' + + const warningMessage = `${chalk.redBright('Warning')}: The following blob key ${chalk.cyan( + key, + )} will be deleted from store ${chalk.cyan(storeName)}:` + + const noticeMessage = `${chalk.yellowBright( + 'Notice', + )}: To overwrite without this warning, you can use the --force flag.` + + const successMessage = `${chalk.greenBright('Success')}: Blob ${chalk.yellow(key)} deleted from store ${chalk.yellow( + storeName, + )}` + + beforeEach(() => { + vi.resetAllMocks() + }) + + test('should log warning message and prompt for confirmation', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const mockDelete = vi.fn().mockResolvedValue('true') + + ;(getStore as any).mockReturnValue({ + delete: mockDelete, + }) + + const program = new BaseCommand('netlify') + createBlobsCommand(program) + + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: true }) + + await program.parseAsync(['', '', 'blob:delete', storeName, key]) + + expect(promptSpy).toHaveBeenCalledWith({ + type: 'confirm', + name: 'wantsToSet', + message: expect.stringContaining('Do you want to proceed with deleting the value at this key?'), + default: false, + }) + + expect(log).toHaveBeenCalledWith(warningMessage) + expect(log).toHaveBeenCalledWith(noticeMessage) + expect(log).toHaveBeenCalledWith(successMessage) + }) + }) + + test('should exit if user responds with no to confirmation prompt', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const mockDelete = vi.fn().mockResolvedValue('true') + + ;(getStore as any).mockReturnValue({ + delete: mockDelete, + }) + + const program = new BaseCommand('netlify') + createBlobsCommand(program) + + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: false }) + + try { + await program.parseAsync(['', '', 'blob:delete', storeName, key]) + } catch (error) { + // We expect the process to exit, so this is fine + expect(error.message).toContain('process.exit unexpectedly called') + } + + expect(promptSpy).toHaveBeenCalledWith({ + type: 'confirm', + name: 'wantsToSet', + message: expect.stringContaining('Do you want to proceed with deleting the value at this key?'), + default: false, + }) + + expect(log).toHaveBeenCalledWith(warningMessage) + expect(log).toHaveBeenCalledWith(noticeMessage) + expect(log).not.toHaveBeenCalledWith(successMessage) + }) + }) + + test('should not log warning message and prompt for confirmation if --force flag is passed', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const mockDelete = vi.fn().mockResolvedValue('true') + + ;(getStore as any).mockReturnValue({ + delete: mockDelete, + }) + + const program = new BaseCommand('netlify') + createBlobsCommand(program) + + const promptSpy = vi.spyOn(inquirer, 'prompt') + + await program.parseAsync(['', '', 'blob:delete', storeName, key, '--force']) + + expect(promptSpy).not.toHaveBeenCalled() + + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(noticeMessage) + expect(log).toHaveBeenCalledWith(successMessage) + }) + }) + + test('should log error message if delete fails', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const mockDelete = vi.fn().mockRejectedValue('') + + ;(getStore as any).mockReturnValue({ + delete: mockDelete, + }) + + const program = new BaseCommand('netlify') + createBlobsCommand(program) + + const promptSpy = vi.spyOn(inquirer, 'prompt') + + try { + await program.parseAsync(['', '', 'blob:delete', storeName, key, '--force']) + } catch (error) { + expect(error.message).toContain( + `Could not delete blob ${chalk.yellow(key)} from store ${chalk.yellow(storeName)}`, + ) + } + + expect(promptSpy).not.toHaveBeenCalled() + + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(noticeMessage) + expect(log).not.toHaveBeenCalledWith(successMessage) + }) + }) +}) diff --git a/tests/integration/commands/blobs/blobs-set.test.ts b/tests/integration/commands/blobs/blobs-set.test.ts new file mode 100644 index 00000000000..0024bc1876b --- /dev/null +++ b/tests/integration/commands/blobs/blobs-set.test.ts @@ -0,0 +1,228 @@ +import process from 'process' + +import { getStore } from '@netlify/blobs' +import chalk from 'chalk' +import inquirer from 'inquirer' +import { describe, expect, test, vi, beforeEach } from 'vitest' + +import BaseCommand from '../../../../src/commands/base-command.js' +import { createBlobsCommand } from '../../../../src/commands/blobs/blobs.js' +import { log } from '../../../../src/utils/command-helpers.js' +import { Route } from '../../utils/mock-api-vitest.js' +import { getEnvironmentVariables, withMockApi } from '../../utils/mock-api.js' + +const siteInfo = { + account_slug: 'test-account', + id: 'site_id', + name: 'site-name', + feature_flags: { + edge_functions_npm_support: true, + }, + functions_config: { timeout: 1 }, +} + +vi.mock('../../../../src/utils/command-helpers.js', async () => ({ + ...(await vi.importActual('../../../../src/utils/command-helpers.js')), + log: vi.fn(), +})) + +vi.mock('@netlify/blobs', () => ({ + getStore: vi.fn(), +})) + +const routes: Route[] = [ + { path: 'sites/site_id', response: siteInfo }, + + { path: 'sites/site_id/service-instances', response: [] }, + { + path: 'accounts', + response: [{ slug: siteInfo.account_slug }], + }, +] + +describe('blob:set command', () => { + const storeName = 'my-store' + const key = 'my-key' + const value = 'my-value' + const newValue = 'my-new-value' + + const warningMessage = `${chalk.redBright('Warning')}: The following blob key already exists in store ${chalk.cyan( + storeName, + )}:` + + const boldKey = chalk.bold(key) + const overWriteMe = `This operation will ${chalk.redBright('overwrite')} the existing value.` + const noticeMessage = `${chalk.yellowBright( + 'Notice', + )}: To overwrite without this warning, you can use the --force flag.` + + const successMessage = `${chalk.greenBright('Success')}: Blob ${chalk.yellow(key)} set in store ${chalk.yellow( + storeName, + )}` + + beforeEach(() => { + vi.resetAllMocks() + }) + + test('should not log warnings and prompt if blob key does not exist', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const mockGet = vi.fn().mockResolvedValue('') + const mockSet = vi.fn().mockResolvedValue('true') + + ;(getStore as any).mockReturnValue({ + get: mockGet, + set: mockSet, + }) + + const program = new BaseCommand('netlify') + createBlobsCommand(program) + + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: true }) + + await program.parseAsync(['', '', 'blob:set', storeName, key, value]) + + expect(promptSpy).not.toHaveBeenCalled() + expect(log).toHaveBeenCalledWith(successMessage) + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(boldKey) + expect(log).not.toHaveBeenCalledWith(overWriteMe) + expect(log).not.toHaveBeenCalledWith(noticeMessage) + }) + }) + + test('should log warnings and prompt if blob key already exists', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + // Mocking the store.get method to return a value (simulating that the key already exists) + const mockGet = vi.fn().mockResolvedValue(value) + const mockSet = vi.fn().mockResolvedValue('true') + + ;(getStore as any).mockReturnValue({ + get: mockGet, + set: mockSet, + }) + + const program = new BaseCommand('netlify') + createBlobsCommand(program) + + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: true }) + + await program.parseAsync(['', '', 'blob:set', storeName, key, newValue]) + + expect(promptSpy).toHaveBeenCalledWith({ + type: 'confirm', + name: 'wantsToSet', + message: expect.stringContaining('Do you want to proceed with overwriting this blob key existing value?'), + default: false, + }) + + expect(log).toHaveBeenCalledWith(successMessage) + expect(log).toHaveBeenCalledWith(warningMessage) + expect(log).toHaveBeenCalledWith(boldKey) + expect(log).toHaveBeenCalledWith(overWriteMe) + expect(log).toHaveBeenCalledWith(noticeMessage) + }) + }) + + test('should exit if user responds with no to confirmation prompt', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + // Mocking the store.get method to return a value (simulating that the key already exists) + const mockGet = vi.fn().mockResolvedValue('my-value') + const mockSet = vi.fn().mockResolvedValue('true') + + ;(getStore as any).mockReturnValue({ + get: mockGet, + set: mockSet, + }) + + const program = new BaseCommand('netlify') + createBlobsCommand(program) + + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: false }) + + try { + await program.parseAsync(['', '', 'blob:set', storeName, key, newValue]) + } catch (error) { + // We expect the process to exit, so this is fine + expect(error.message).toContain('process.exit unexpectedly called with "0"') + } + + expect(promptSpy).toHaveBeenCalledWith({ + type: 'confirm', + name: 'wantsToSet', + message: expect.stringContaining('Do you want to proceed with overwriting this blob key existing value?'), + default: false, + }) + + expect(log).toHaveBeenCalledWith(warningMessage) + expect(log).toHaveBeenCalledWith(boldKey) + expect(log).toHaveBeenCalledWith(overWriteMe) + expect(log).toHaveBeenCalledWith(noticeMessage) + expect(log).not.toHaveBeenCalledWith(successMessage) + }) + }) + + test('should not log warnings and prompt if blob key already exists and --force flag is passed', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + // Mocking the store.get method to return a value (simulating that the key already exists) + const mockGet = vi.fn().mockResolvedValue('my-value') + const mockSet = vi.fn().mockResolvedValue('true') + + ;(getStore as any).mockReturnValue({ + get: mockGet, + set: mockSet, + }) + + const program = new BaseCommand('netlify') + createBlobsCommand(program) + + const promptSpy = vi.spyOn(inquirer, 'prompt') + + await program.parseAsync(['', '', 'blob:set', storeName, key, newValue, '--force']) + + expect(promptSpy).not.toHaveBeenCalled() + + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(boldKey) + expect(log).not.toHaveBeenCalledWith(overWriteMe) + expect(log).not.toHaveBeenCalledWith(noticeMessage) + expect(log).toHaveBeenCalledWith(successMessage) + }) + }) + + test('should log error message if adding a key fails', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const mockDelete = vi.fn().mockRejectedValue('') + + ;(getStore as any).mockReturnValue({ + delete: mockDelete, + }) + + const program = new BaseCommand('netlify') + createBlobsCommand(program) + + const promptSpy = vi.spyOn(inquirer, 'prompt') + + try { + await program.parseAsync(['', '', 'blob:set', storeName, key, newValue, '--force']) + } catch (error) { + expect(error.message).toContain(`Could not set blob ${chalk.yellow(key)} in store ${chalk.yellow(storeName)}`) + } + + expect(promptSpy).not.toHaveBeenCalled() + + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(noticeMessage) + expect(log).not.toHaveBeenCalledWith(successMessage) + }) + }) +}) diff --git a/tests/integration/commands/blobs/blobs.test.ts b/tests/integration/commands/blobs/blobs.test.ts index 80491b72b14..2868582fc10 100644 --- a/tests/integration/commands/blobs/blobs.test.ts +++ b/tests/integration/commands/blobs/blobs.test.ts @@ -64,12 +64,14 @@ describe('blobs:* commands', () => { }) setupFixtureTests('empty-project', { mockApi: { routes } }, () => { + const expectedSucusesMessage = `Blob my-key set in store my-store` + test('should set, get, list, and delete blobs', async ({ fixture }) => { expect( - await fixture.callCli(['blobs:set', 'my-store', 'my-key', 'Hello world'], { + await fixture.callCli(['blobs:set', 'my-store', 'my-key', 'Hello world', '--force'], { offline: false, }), - ).toBe('') + ).toBe(expectedSucusesMessage) expect( await fixture.callCli(['blobs:get', 'my-store', 'my-key'], { diff --git a/tests/integration/commands/env/env-clone.test.ts b/tests/integration/commands/env/env-clone.test.ts index 6d7f7e4ab4a..ccce92e31f2 100644 --- a/tests/integration/commands/env/env-clone.test.ts +++ b/tests/integration/commands/env/env-clone.test.ts @@ -7,7 +7,6 @@ import { describe, expect, test, vi, beforeEach } from 'vitest' import BaseCommand from '../../../../src/commands/base-command.js' import { createEnvCommand } from '../../../../src/commands/env/env.js' import { log } from '../../../../src/utils/command-helpers.js' -import { FixtureTestContext, setupFixtureTests } from '../../utils/fixture.js' import { getEnvironmentVariables, withMockApi } from '../../utils/mock-api.js' import routes from './api-routes.js' @@ -18,152 +17,130 @@ vi.mock('../../../../src/utils/command-helpers.js', async () => ({ })) describe('env:clone command', () => { - setupFixtureTests('empty-project', { mockApi: { routes } }, () => { - test.only('should create and return new var in the dev context', async ({ - fixture, - mockApi, - }) => { - const cliResponse = await fixture.callCli(['env:clone', '-t', 'site_id_3', '--skip'], { - offline: false, - parseJson: false, - }) - - // console.log(cliResponse) - // const postRequest = mockApi?.requests.find( - // (request) => request.method === 'POST' && request.path === '/api/v1/accounts/test-account/env', - // ) - - // console.log(mockApi?.requests.map((request) => request.method)) - // // expect(postRequest.body[0].key).toBe('NEW_VAR') - // // expect(postRequest.body[0].values[0].context).toBe('dev') - // // expect(postRequest.body[0].values[0].value).toBe('new-value') + describe('user is prompted to confirm when setting an env var that already exists', () => { + const sharedEnvVars = 'EXISTING_VAR' + const siteIdTwo = 'site_id_2' + const warningMessage = `${chalk.redBright( + 'Warning', + )}: The following environment variables are already set on the site with ID ${chalk.bgBlueBright( + siteIdTwo, + )}. They will be overwritten!` + const expectedNoticeMessage = `${chalk.yellowBright('Notice')}: The following variables will be overwritten:` + + const expectedSkipMessage = `${chalk.yellowBright( + 'Notice', + )}: To overwrite the existing variables without confirmation prompts, pass the --force flag.` + const expectedSuccessMessage = `Successfully cloned environment variables from ${chalk.green( + 'site-name', + )} to ${chalk.green('site-name-2')}` + + beforeEach(() => { + vi.resetAllMocks() }) - }) -}) -describe('user is prompted to confirm when setting an env var that already exists', () => { - const sharedEnvVars = 'EXISTING_VAR' - const siteIdTwo = 'site_id_2' - const warningMessage = `${chalk.redBright( - 'Warning', - )}: The following environment variables are already set on the site with ID ${chalk.bgBlueBright( - siteIdTwo, - )}. They will be overwritten!` - const expectedNoticeMessage = `${chalk.yellowBright('Notice')}: The following variables will be overwritten:` - - const expectedSkipMessage = `${chalk.yellowBright( - 'Notice', - )}: To overwrite the existing variables without confirmation prompts, pass the -s or --skip flag.` - const expectedSuccessMessage = `Successfully cloned environment variables from ${chalk.green( - 'site-name', - )} to ${chalk.green('site-name-2')}` - - beforeEach(() => { - vi.resetAllMocks() - }) - - test('should log warnings and prompts if enviroment variable already exists', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + test('should log warnings and prompts if enviroment variable already exists', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const program = new BaseCommand('netlify') - createEnvCommand(program) + const program = new BaseCommand('netlify') + createEnvCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: true }) + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: true }) - await program.parseAsync(['', '', 'env:clone', '-t', 'site_id_2']) + await program.parseAsync(['', '', 'env:clone', '-t', 'site_id_2']) - expect(promptSpy).toHaveBeenCalledWith({ - type: 'confirm', - name: 'wantsToSet', - message: expect.stringContaining('Do you want to proceed with overwriting these variables?'), - default: false, + expect(promptSpy).toHaveBeenCalledWith({ + type: 'confirm', + name: 'wantsToSet', + message: expect.stringContaining('Do you want to proceed with overwriting these variables?'), + default: false, + }) + + expect(log).toHaveBeenCalledWith( + `${chalk.redBright( + 'Warning', + )}: The following environment variables are already set on the site with ID ${chalk.bgBlueBright( + 'site_id_2', + )}. They will be overwritten!`, + ) + + expect(log).toHaveBeenCalledWith(warningMessage) + expect(log).toHaveBeenCalledWith(expectedNoticeMessage) + expect(log).toHaveBeenCalledWith(sharedEnvVars) + expect(log).toHaveBeenCalledWith(expectedSkipMessage) + expect(log).toHaveBeenCalledWith(expectedSuccessMessage) }) - - expect(log).toHaveBeenCalledWith( - `${chalk.redBright( - 'Warning', - )}: The following environment variables are already set on the site with ID ${chalk.bgBlueBright( - 'site_id_2', - )}. They will be overwritten!`, - ) - - expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(expectedNoticeMessage) - expect(log).toHaveBeenCalledWith(sharedEnvVars) - expect(log).toHaveBeenCalledWith(expectedSkipMessage) - expect(log).toHaveBeenCalledWith(expectedSuccessMessage) }) - }) - test('should skip warnings and prompts if -s flag is passed', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + test('should skip warnings and prompts if --force flag is passed', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const program = new BaseCommand('netlify') - createEnvCommand(program) + const program = new BaseCommand('netlify') + createEnvCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt') + const promptSpy = vi.spyOn(inquirer, 'prompt') - await program.parseAsync(['', '', 'env:clone', '-s', '-t', 'site_id_2']) + await program.parseAsync(['', '', 'env:clone', '--force', '-t', 'site_id_2']) - expect(promptSpy).not.toHaveBeenCalled() + expect(promptSpy).not.toHaveBeenCalled() - expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(expectedNoticeMessage) - expect(log).not.toHaveBeenCalledWith(sharedEnvVars) - expect(log).not.toHaveBeenCalledWith(expectedSkipMessage) - expect(log).toHaveBeenCalledWith(expectedSuccessMessage) + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(expectedNoticeMessage) + expect(log).not.toHaveBeenCalledWith(sharedEnvVars) + expect(log).not.toHaveBeenCalledWith(expectedSkipMessage) + expect(log).toHaveBeenCalledWith(expectedSuccessMessage) + }) }) - }) - test('should exit user reponds is no to confirmatnion prompt', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + test('should exit user reponds is no to confirmatnion prompt', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const program = new BaseCommand('netlify') - createEnvCommand(program) + const program = new BaseCommand('netlify') + createEnvCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: false }) + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: false }) - try { - await program.parseAsync(['', '', 'env:clone', '-t', 'site_id_2']) - } catch (error) { - // We expect the process to exit, so this is fine - expect(error.message).toContain('process.exit unexpectedly called') - } - - expect(promptSpy).toHaveBeenCalled() - - expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(expectedNoticeMessage) - expect(log).toHaveBeenCalledWith(sharedEnvVars) - expect(log).toHaveBeenCalledWith(expectedSkipMessage) - expect(log).not.toHaveBeenCalledWith(expectedSuccessMessage) + try { + await program.parseAsync(['', '', 'env:clone', '-t', 'site_id_2']) + } catch (error) { + // We expect the process to exit, so this is fine + expect(error.message).toContain('process.exit unexpectedly called') + } + + expect(promptSpy).toHaveBeenCalled() + + expect(log).toHaveBeenCalledWith(warningMessage) + expect(log).toHaveBeenCalledWith(expectedNoticeMessage) + expect(log).toHaveBeenCalledWith(sharedEnvVars) + expect(log).toHaveBeenCalledWith(expectedSkipMessage) + expect(log).not.toHaveBeenCalledWith(expectedSuccessMessage) + }) }) - }) - test('should not run prompts if sites have no enviroment variables in common', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const expectedSuccessMessageSite3 = `Successfully cloned environment variables from ${chalk.green( - 'site-name', - )} to ${chalk.green('site-name-3')}` + test('should not run prompts if sites have no enviroment variables in common', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + const expectedSuccessMessageSite3 = `Successfully cloned environment variables from ${chalk.green( + 'site-name', + )} to ${chalk.green('site-name-3')}` - const program = new BaseCommand('netlify') - createEnvCommand(program) + const program = new BaseCommand('netlify') + createEnvCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt') + const promptSpy = vi.spyOn(inquirer, 'prompt') - await program.parseAsync(['', '', 'env:clone', '-t', 'site_id_3']) + await program.parseAsync(['', '', 'env:clone', '-t', 'site_id_3']) - expect(promptSpy).not.toHaveBeenCalled() + expect(promptSpy).not.toHaveBeenCalled() - expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(expectedNoticeMessage) - expect(log).not.toHaveBeenCalledWith(sharedEnvVars) - expect(log).not.toHaveBeenCalledWith(expectedSkipMessage) - expect(log).toHaveBeenCalledWith(expectedSuccessMessageSite3) + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(expectedNoticeMessage) + expect(log).not.toHaveBeenCalledWith(sharedEnvVars) + expect(log).not.toHaveBeenCalledWith(expectedSkipMessage) + expect(log).toHaveBeenCalledWith(expectedSuccessMessageSite3) + }) }) }) }) From 98a13993e27681ca59f440a61f68218a03f22a21 Mon Sep 17 00:00:00 2001 From: Will Conrad Date: Mon, 14 Oct 2024 16:35:36 -0500 Subject: [PATCH 12/39] fix: updated tests in file to reflect new prompts --- src/commands/base-command.ts | 3 +-- .../commands/blobs/blobs-set.test.ts | 4 ++-- .../integration/commands/blobs/blobs.test.ts | 6 ++--- .../commands/env/env-clone.test.ts | 2 +- tests/integration/commands/env/env.test.js | 22 ++++++++++++------- .../commands/envelope/envelope.test.js | 2 +- 6 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/commands/base-command.ts b/src/commands/base-command.ts index 592f464b755..a9071897730 100644 --- a/src/commands/base-command.ts +++ b/src/commands/base-command.ts @@ -1,5 +1,3 @@ -import { isCI } from 'ci-info' - import { existsSync } from 'fs' import { join, relative, resolve } from 'path' import process from 'process' @@ -8,6 +6,7 @@ import { format } from 'util' import { DefaultLogger, Project } from '@netlify/build-info' import { NodeFS, NoopLogger } from '@netlify/build-info/node' import { resolveConfig } from '@netlify/config' +import { isCI } from 'ci-info' import { Command, Help, Option } from 'commander' // @ts-expect-error TS(7016) FIXME: Could not find a declaration file for module 'debu... Remove this comment to see the full error message import debug from 'debug' diff --git a/tests/integration/commands/blobs/blobs-set.test.ts b/tests/integration/commands/blobs/blobs-set.test.ts index 0024bc1876b..ce022447f53 100644 --- a/tests/integration/commands/blobs/blobs-set.test.ts +++ b/tests/integration/commands/blobs/blobs-set.test.ts @@ -201,10 +201,10 @@ describe('blob:set command', () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const mockDelete = vi.fn().mockRejectedValue('') + const mockSet = vi.fn().mockRejectedValue('') ;(getStore as any).mockReturnValue({ - delete: mockDelete, + set: mockSet, }) const program = new BaseCommand('netlify') diff --git a/tests/integration/commands/blobs/blobs.test.ts b/tests/integration/commands/blobs/blobs.test.ts index 2868582fc10..493a01c8f01 100644 --- a/tests/integration/commands/blobs/blobs.test.ts +++ b/tests/integration/commands/blobs/blobs.test.ts @@ -64,7 +64,7 @@ describe('blobs:* commands', () => { }) setupFixtureTests('empty-project', { mockApi: { routes } }, () => { - const expectedSucusesMessage = `Blob my-key set in store my-store` + const expectedSucusesMessage = 'Success: Blob my-key set in store my-store' test('should set, get, list, and delete blobs', async ({ fixture }) => { expect( @@ -100,10 +100,10 @@ describe('blobs:* commands', () => { expect(listResult.directories).toEqual([]) expect( - await fixture.callCli(['blobs:delete', 'my-store', 'my-key'], { + await fixture.callCli(['blobs:delete', 'my-store', 'my-key', '--force'], { offline: false, }), - ).toBe('') + ).toBe('Success: Blob my-key deleted from store my-store') expect( await fixture.callCli(['blobs:list', 'my-store', '--json'], { diff --git a/tests/integration/commands/env/env-clone.test.ts b/tests/integration/commands/env/env-clone.test.ts index ccce92e31f2..bf3b10c8cfc 100644 --- a/tests/integration/commands/env/env-clone.test.ts +++ b/tests/integration/commands/env/env-clone.test.ts @@ -5,7 +5,7 @@ import inquirer from 'inquirer' import { describe, expect, test, vi, beforeEach } from 'vitest' import BaseCommand from '../../../../src/commands/base-command.js' -import { createEnvCommand } from '../../../../src/commands/env/env.js' +import { createEnvCommand } from '../../../../src/commands/env/index.js' import { log } from '../../../../src/utils/command-helpers.js' import { getEnvironmentVariables, withMockApi } from '../../utils/mock-api.js' diff --git a/tests/integration/commands/env/env.test.js b/tests/integration/commands/env/env.test.js index c1eba42850e..ad2f71486d5 100644 --- a/tests/integration/commands/env/env.test.js +++ b/tests/integration/commands/env/env.test.js @@ -244,7 +244,7 @@ describe('commands/env', () => { await withMockApi(setRoutes, async ({ apiUrl }) => { const cliResponse = await callCli( - ['env:set', '--json', 'NEW_VAR', 'new-value'], + ['env:set', '--json', 'NEW_VAR', 'new-value', '--force'], getCLIOptions({ builder, apiUrl }), true, ) @@ -270,7 +270,7 @@ describe('commands/env', () => { await withMockApi(setRoutes, async ({ apiUrl }) => { const cliResponse = await callCli( - ['env:set', '--json', 'EXISTING_VAR', 'new-value'], + ['env:set', '--json', 'EXISTING_VAR', 'new-value', '--force'], getCLIOptions({ builder, apiUrl }), true, ) @@ -297,7 +297,7 @@ describe('commands/env', () => { await withMockApi(unsetRoutes, async ({ apiUrl }) => { const cliResponse = await callCli( - ['env:unset', '--json', 'EXISTING_VAR'], + ['env:unset', '--json', 'EXISTING_VAR', '--force'], getCLIOptions({ builder, apiUrl }), true, ) @@ -396,7 +396,10 @@ describe('commands/env', () => { { path: 'sites/site_id_a', response: { ...siteInfo, build_settings: { env: {} } } }, ] await withMockApi(createRoutes, async ({ apiUrl }) => { - const cliResponse = await callCli(['env:clone', '--to', 'site_id_a'], getCLIOptions({ builder, apiUrl })) + const cliResponse = await callCli( + ['env:clone', '--to', 'site_id_a', '--force'], + getCLIOptions({ builder, apiUrl }), + ) t.expect(normalize(cliResponse)).toMatchSnapshot() }) @@ -409,7 +412,7 @@ describe('commands/env', () => { const createRoutes = [{ path: 'sites/site_id', response: { ...siteInfo, build_settings: { env: {} } } }] await withMockApi(createRoutes, async ({ apiUrl }) => { const { stderr: cliResponse } = await callCli( - ['env:clone', '--to', 'to-site'], + ['env:clone', '--to', 'to-site', '--force'], getCLIOptions({ builder, apiUrl }), ).catch((error) => error) @@ -423,7 +426,7 @@ describe('commands/env', () => { await builder.build() await withMockApi([], async ({ apiUrl }) => { const { stderr: cliResponse } = await callCli( - ['env:clone', '--from', 'from-site', '--to', 'to-site'], + ['env:clone', '--from', 'from-site', '--to', 'to-site', '--force'], getCLIOptions({ builder, apiUrl }), ).catch((error) => error) @@ -438,7 +441,7 @@ describe('commands/env', () => { await withSiteBuilder(t, async (builder) => { await builder.build() - const cliResponse = await callCli(['env:clone', '--to', 'site_id_a'], { + const cliResponse = await callCli(['env:clone', '--to', 'site_id_a', '--force'], { cwd: builder.directory, extendEnv: false, PATH: process.env.PATH, @@ -469,7 +472,10 @@ describe('commands/env', () => { await withSiteBuilder(t, async (builder) => { await builder.build() await withMockApi(cloneRoutes, async ({ apiUrl, requests }) => { - const cliResponse = await callCli(['env:clone', '--to', 'site_id_a'], getCLIOptions({ apiUrl, builder })) + const cliResponse = await callCli( + ['env:clone', '--to', 'site_id_a', '--force'], + getCLIOptions({ apiUrl, builder }), + ) t.expect(normalize(cliResponse)).toMatchSnapshot() diff --git a/tests/integration/commands/envelope/envelope.test.js b/tests/integration/commands/envelope/envelope.test.js index f020d69e9b7..d63eb898cc6 100644 --- a/tests/integration/commands/envelope/envelope.test.js +++ b/tests/integration/commands/envelope/envelope.test.js @@ -220,7 +220,7 @@ describe.concurrent('command/envelope', () => { await builder.build() await withMockApi(cloneRoutes, async ({ apiUrl, requests }) => { const cliResponse = await callCli( - ['env:clone', '--from', 'site_id_a', '--to', 'site_id_b'], + ['env:clone', '--from', 'site_id_a', '--to', 'site_id_b', '--force'], getCLIOptions({ apiUrl, builder }), ) From 31cd93e54bfd765f12b506c543725b11c553e03b Mon Sep 17 00:00:00 2001 From: Will Conrad Date: Tue, 15 Oct 2024 09:34:18 -0500 Subject: [PATCH 13/39] fix: updated documentation updated the documentation Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> --- docs/commands/blobs.md | 2 ++ docs/commands/env.md | 3 +++ package-lock.json | 14 +++++++++++++- src/commands/blobs/blobs.ts | 4 ++-- src/commands/env/env-set.ts | 2 +- src/commands/env/env.ts | 6 +++--- 6 files changed, 24 insertions(+), 7 deletions(-) diff --git a/docs/commands/blobs.md b/docs/commands/blobs.md index 77388036925..c7359cb8ca0 100644 --- a/docs/commands/blobs.md +++ b/docs/commands/blobs.md @@ -57,6 +57,7 @@ netlify blobs:delete **Flags** - `filter` (*string*) - For monorepos, specify the name of the application to run the command in +- `force` (*boolean*) - Force the operation to proceed without confirmation or warnings - `debug` (*boolean*) - Print debugging information --- @@ -124,6 +125,7 @@ netlify blobs:set **Flags** - `filter` (*string*) - For monorepos, specify the name of the application to run the command in +- `force` (*boolean*) - Force the operation to proceed without confirmation or warnings - `input` (*string*) - Defines the filesystem path where the blob data should be read from - `debug` (*boolean*) - Print debugging information diff --git a/docs/commands/env.md b/docs/commands/env.md index 2581f6d3179..1911d923714 100644 --- a/docs/commands/env.md +++ b/docs/commands/env.md @@ -54,6 +54,7 @@ netlify env:clone **Flags** - `filter` (*string*) - For monorepos, specify the name of the application to run the command in +- `force` (*boolean*) - Force the operation to proceed without confirmation or warnings - `from` (*string*) - Site ID (From) - `to` (*string*) - Site ID (To) - `debug` (*boolean*) - Print debugging information @@ -167,6 +168,7 @@ netlify env:set - `context` (*string*) - Specify a deploy context or branch (contexts: "production", "deploy-preview", "branch-deploy", "dev") (default: all contexts) - `filter` (*string*) - For monorepos, specify the name of the application to run the command in +- `force` (*boolean*) - Force the operation to proceed without confirmation or warnings - `scope` (*builds | functions | post-processing | runtime*) - Specify a scope (default: all scopes) - `secret` (*boolean*) - Indicate whether the environment variable value can be read again. - `debug` (*boolean*) - Print debugging information @@ -202,6 +204,7 @@ netlify env:unset - `context` (*string*) - Specify a deploy context or branch (contexts: "production", "deploy-preview", "branch-deploy", "dev") (default: all contexts) - `filter` (*string*) - For monorepos, specify the name of the application to run the command in +- `force` (*boolean*) - Force the operation to proceed without confirmation or warnings - `debug` (*boolean*) - Print debugging information **Examples** diff --git a/package-lock.json b/package-lock.json index 6d980f3a268..7c3788d6bb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8755,6 +8755,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", + "license": "MIT", "dependencies": { "restore-cursor": "^2.0.0" }, @@ -14216,6 +14217,7 @@ "version": "6.5.2", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", + "license": "MIT", "dependencies": { "ansi-escapes": "^3.2.0", "chalk": "^2.4.2", @@ -14338,6 +14340,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "license": "MIT", "engines": { "node": ">=4" } @@ -14386,6 +14389,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", + "license": "MIT", "dependencies": { "escape-string-regexp": "^1.0.5" }, @@ -14405,6 +14409,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "license": "MIT", "engines": { "node": ">=4" } @@ -14413,6 +14418,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "license": "MIT", "dependencies": { "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^4.0.0" @@ -14425,6 +14431,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "license": "MIT", "engines": { "node": ">=4" } @@ -14433,6 +14440,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "license": "MIT", "dependencies": { "ansi-regex": "^3.0.0" }, @@ -17067,7 +17075,8 @@ "node_modules/mute-stream": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==" + "integrity": "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==", + "license": "ISC" }, "node_modules/mv": { "version": "2.1.1", @@ -19958,6 +19967,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", + "license": "MIT", "dependencies": { "onetime": "^2.0.0", "signal-exit": "^3.0.2" @@ -19970,6 +19980,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "license": "MIT", "engines": { "node": ">=4" } @@ -19978,6 +19989,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", + "license": "MIT", "dependencies": { "mimic-fn": "^1.0.0" }, diff --git a/src/commands/blobs/blobs.ts b/src/commands/blobs/blobs.ts index e77e19cae2e..260ccc68b6c 100644 --- a/src/commands/blobs/blobs.ts +++ b/src/commands/blobs/blobs.ts @@ -19,7 +19,7 @@ export const createBlobsCommand = (program: BaseCommand) => { .description(`Deletes an object with a given key, if it exists, from a Netlify Blobs store`) .argument('', 'Name of the store') .argument('', 'Object key') - .option('-f, --force', 'Force the operation without warnings') + .option('-f, --force', 'Force the operation to proceed without confirmation or warnings') .alias('blob:delete') .hook('preAction', requiresSiteInfo) .action(async (storeName: string, key: string, _options: OptionValues, command: BaseCommand) => { @@ -71,7 +71,7 @@ export const createBlobsCommand = (program: BaseCommand) => { .argument('', 'Object key') .argument('[value...]', 'Object value') .option('-i, --input ', 'Defines the filesystem path where the blob data should be read from') - .option('-f, --force', 'Force the operation without warnings') + .option('-f, --force', 'Force the operation to proceed without confirmation or warnings') .alias('blob:set') .hook('preAction', requiresSiteInfo) diff --git a/src/commands/env/env-set.ts b/src/commands/env/env-set.ts index e10f495152f..b76cb847491 100644 --- a/src/commands/env/env-set.ts +++ b/src/commands/env/env-set.ts @@ -51,7 +51,7 @@ const setInEnvelope = async ({ api, context, force, key, scope, secret, siteInfo // @ts-expect-error TS(7006) FIXME: Parameter 'envVar' implicitly has an 'any' type. const existing = envelopeVariables.find((envVar) => envVar.key === key) - // Checks if -f is passed and if it is an existing variaible, then we need to prompt the user + // Checks if --force is passed and if it is an existing variaible, then we need to prompt the user if (Boolean(force) === false && existing) { await envSetPrompts(key) } diff --git a/src/commands/env/env.ts b/src/commands/env/env.ts index 7babb1d4eb7..af14d6ca122 100644 --- a/src/commands/env/env.ts +++ b/src/commands/env/env.ts @@ -103,7 +103,7 @@ export const createEnvCommand = (program: BaseCommand) => { 'runtime', ]), ) - .option('-f, --force', 'force the operation without warnings') + .option('-f, --force', 'Force the operation to proceed without confirmation or warnings') .option('--secret', 'Indicate whether the environment variable value can be read again.') .description('Set value of environment variable') .addExamples([ @@ -131,7 +131,7 @@ export const createEnvCommand = (program: BaseCommand) => { // @ts-expect-error TS(7006) FIXME: Parameter 'context' implicitly has an 'any' type. (context, previous = []) => [...previous, normalizeContext(context)], ) - .option('-f, --force', 'force the operation without warnings') + .option('-f, --force', 'Force the operation to proceed without confirmation or warnings') .addExamples([ 'netlify env:unset VAR_NAME # unset in all contexts', 'netlify env:unset VAR_NAME --context production', @@ -147,7 +147,7 @@ export const createEnvCommand = (program: BaseCommand) => { .command('env:clone') .alias('env:migrate') .option('-f, --from ', 'Site ID (From)') - .option('--force', 'force the operation without warnings') + .option('--force', 'Force the operation to proceed without confirmation or warnings') .requiredOption('-t, --to ', 'Site ID (To)') .description(`Clone environment variables from one site to another`) .addExamples(['netlify env:clone --to ', 'netlify env:clone --to --from ']) From cf6b27a3a978aee567d52c30859fd22651a5f541 Mon Sep 17 00:00:00 2001 From: Will Conrad Date: Tue, 15 Oct 2024 10:01:26 -0500 Subject: [PATCH 14/39] fix: updated error updated error handeling Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> Co-authored-by: Thomas Lane --- src/commands/env/env-set.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/commands/env/env-set.ts b/src/commands/env/env-set.ts index b76cb847491..13e2ea36af8 100644 --- a/src/commands/env/env-set.ts +++ b/src/commands/env/env-set.ts @@ -103,13 +103,6 @@ const setInEnvelope = async ({ api, context, force, key, scope, secret, siteInfo await api.createEnvVars({ ...params, body }) } } catch (error_) { - // if (error_.json && error_.json.status === 500) { - // log(`${chalk.redBright('ERROR')}: Environment variable ${key} not created`) - // if (scope) { - // log(`${chalk.yellowBright('Notice')}: Scope setting is only available to paid Netlify accounts`) - // } - // } - // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'. throw error_.json ? error_.json.msg : error_ } From 6bfb620ef689b9b82209292fc008135bf348786c Mon Sep 17 00:00:00 2001 From: Will Conrad Date: Tue, 15 Oct 2024 10:20:21 -0500 Subject: [PATCH 15/39] fix: updated new lines in messages for consistence updated prompts spacing for consistencey Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> --- src/utils/prompts/blob-delete-prompts.ts | 1 - src/utils/prompts/blob-set-prompt.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/utils/prompts/blob-delete-prompts.ts b/src/utils/prompts/blob-delete-prompts.ts index eccc4c14f37..301bf71cfb9 100644 --- a/src/utils/prompts/blob-delete-prompts.ts +++ b/src/utils/prompts/blob-delete-prompts.ts @@ -11,7 +11,6 @@ const generateBlobWarningMessage = (key: string, storeName: string): void => { ) log() log(`${chalk.yellowBright('Notice')}: To overwrite without this warning, you can use the --force flag.`) - log() } export const blobDeletePrompts = async (key: string, storeName: string): Promise => { diff --git a/src/utils/prompts/blob-set-prompt.ts b/src/utils/prompts/blob-set-prompt.ts index 5f5a1d1a46c..677839a2762 100644 --- a/src/utils/prompts/blob-set-prompt.ts +++ b/src/utils/prompts/blob-set-prompt.ts @@ -10,7 +10,6 @@ const generateBlobWarningMessage = (key: string, storeName: string): void => { log() log(`This operation will ${chalk.redBright('overwrite')} the existing value.`) log(`${chalk.yellowBright('Notice')}: To overwrite without this warning, you can use the --force flag.`) - log() } export const blobSetPrompts = async (key: string, storeName: string): Promise => { From 4f21d60f086987b16d94c5ad3c8518aa726d6f0b Mon Sep 17 00:00:00 2001 From: Will Conrad Date: Tue, 15 Oct 2024 10:25:14 -0500 Subject: [PATCH 16/39] fix: fixed prettier error Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> --- src/commands/env/env-set.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/env/env-set.ts b/src/commands/env/env-set.ts index 13e2ea36af8..98e31f024c3 100644 --- a/src/commands/env/env-set.ts +++ b/src/commands/env/env-set.ts @@ -1,6 +1,6 @@ import { OptionValues } from 'commander' -import { chalk, error, log, exit, logJson } from '../../utils/command-helpers.js' +import { chalk, error, log, logJson } from '../../utils/command-helpers.js' import { AVAILABLE_CONTEXTS, AVAILABLE_SCOPES, translateFromEnvelopeToMongo } from '../../utils/env/index.js' import { envSetPrompts } from '../../utils/prompts/env-set-prompts.js' import BaseCommand from '../base-command.js' From 7527f74665a406358f79927e141675394ae257aa Mon Sep 17 00:00:00 2001 From: Will Conrad Date: Mon, 21 Oct 2024 15:48:46 -0500 Subject: [PATCH 17/39] feat: env-set refactored refactored messages in env-set to a function that exports an object to be reused Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> --- src/commands/env/env-set.ts | 4 +- src/utils/prompts/env-set-prompts.ts | 39 +++++++++++++------ .../integration/commands/env/env-set.test.ts | 39 +++++++++---------- 3 files changed, 47 insertions(+), 35 deletions(-) diff --git a/src/commands/env/env-set.ts b/src/commands/env/env-set.ts index 98e31f024c3..9a7bd3a83b8 100644 --- a/src/commands/env/env-set.ts +++ b/src/commands/env/env-set.ts @@ -2,7 +2,7 @@ import { OptionValues } from 'commander' import { chalk, error, log, logJson } from '../../utils/command-helpers.js' import { AVAILABLE_CONTEXTS, AVAILABLE_SCOPES, translateFromEnvelopeToMongo } from '../../utils/env/index.js' -import { envSetPrompts } from '../../utils/prompts/env-set-prompts.js' +import { promptOverwriteEnvVariable } from '../../utils/prompts/env-set-prompts.js' import BaseCommand from '../base-command.js' /** @@ -53,7 +53,7 @@ const setInEnvelope = async ({ api, context, force, key, scope, secret, siteInfo // Checks if --force is passed and if it is an existing variaible, then we need to prompt the user if (Boolean(force) === false && existing) { - await envSetPrompts(key) + await promptOverwriteEnvVariable(key) } const params = { accountId, siteId, key } diff --git a/src/utils/prompts/env-set-prompts.ts b/src/utils/prompts/env-set-prompts.ts index 306761f1d19..f99a73cedf0 100644 --- a/src/utils/prompts/env-set-prompts.ts +++ b/src/utils/prompts/env-set-prompts.ts @@ -2,18 +2,33 @@ import { chalk, log } from '../command-helpers.js' import { confirmPrompt } from './confirm-prompt.js' -const generateSetMessage = (variableName: string): void => { +export const generateSetMessage = (variableName: string) => ({ + warningMessage: `${chalk.redBright('Warning')}: The environment variable ${chalk.bgBlueBright( + variableName, + )} already exists!`, + noticeMessage: `${chalk.yellowBright( + 'Notice', + )}: To overwrite the existing variable without confirmation, pass the -f or --force flag.`, + confirmMessage: 'The environment variable already exists. Do you want to overwrite it?', +}) + +/** + * Generates warning, notice and confirm messages when trying to set an env variable + * that already exists. + * + * @param {string} key - The key of the environment variable that already exists + * @returns {Object} An object with the following properties: + * - warning: A warning message to be displayed to the user + * - notice: A notice message to be displayed to the user + * - confirm: A confirmation prompt to ask the user if they want to overwrite the existing variable + */ +export const promptOverwriteEnvVariable = async (key: string): Promise => { + const { confirmMessage, noticeMessage, warningMessage } = generateSetMessage(key) + log() - log(`${chalk.redBright('Warning')}: The environment variable ${chalk.bgBlueBright(variableName)} already exists!`) + log(warningMessage) log() - log( - `${chalk.yellowBright( - 'Notice', - )}: To overwrite the existing variable without confirmation, pass the -f or --force flag.`, - ) -} - -export const envSetPrompts = async (key: string): Promise => { - generateSetMessage(key) - await confirmPrompt('The environment variable already exists. Do you want to overwrite it?') + log(noticeMessage) + log() + await confirmPrompt(confirmMessage) } diff --git a/tests/integration/commands/env/env-set.test.ts b/tests/integration/commands/env/env-set.test.ts index 7c711292157..2938d250a43 100644 --- a/tests/integration/commands/env/env-set.test.ts +++ b/tests/integration/commands/env/env-set.test.ts @@ -7,6 +7,7 @@ import { describe, expect, test, vi, beforeEach } from 'vitest' import BaseCommand from '../../../../src/commands/base-command.js' import { createEnvCommand } from '../../../../src/commands/env/env.js' import { log } from '../../../../src/utils/command-helpers.js' +import { generateSetMessage } from '../../../../src/utils/prompts/env-set-prompts.js' import { FixtureTestContext, setupFixtureTests } from '../../utils/fixture.js' import { getEnvironmentVariables, withMockApi } from '../../utils/mock-api.js' @@ -273,15 +274,9 @@ describe('env:set command', () => { const existingVar = 'EXISTING_VAR' const newEnvValue = 'value' - const expectedMessageAlreadyExists = `${chalk.redBright('Warning')}: The environment variable ${chalk.bgBlueBright( - existingVar, - )} already exists!` + const { confirmMessage, noticeMessage, warningMessage } = generateSetMessage(existingVar) - const expectedNoticeMessage = `${chalk.yellowBright( - 'Notice', - )}: To overwrite the existing variable without confirmation, pass the -f or --force flag.` - - const expectedSuccessMessage = `Set environment variable ${chalk.yellow( + const successMessage = `Set environment variable ${chalk.yellow( `${existingVar}=${newEnvValue}`, )} in the ${chalk.magenta('all')} context` @@ -304,13 +299,13 @@ describe('env:set command', () => { expect(promptSpy).toHaveBeenCalledWith({ type: 'confirm', name: 'wantsToSet', - message: expect.stringContaining('The environment variable already exists. Do you want to overwrite it?'), + message: expect.stringContaining(confirmMessage), default: false, }) - expect(log).toHaveBeenCalledWith(expectedMessageAlreadyExists) - expect(log).toHaveBeenCalledWith(expectedNoticeMessage) - expect(log).toHaveBeenCalledWith(expectedSuccessMessage) + expect(log).toHaveBeenCalledWith(warningMessage) + expect(log).toHaveBeenCalledWith(noticeMessage) + expect(log).toHaveBeenCalledWith(successMessage) }) }) @@ -327,10 +322,12 @@ describe('env:set command', () => { expect(promptSpy).not.toHaveBeenCalled() - expect(log).not.toHaveBeenCalledWith(expectedMessageAlreadyExists) - expect(log).not.toHaveBeenCalledWith(expectedNoticeMessage) + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(noticeMessage) expect(log).toHaveBeenCalledWith( - `Set environment variable ${chalk.yellow(`NEW_ENV_VAR=NEW_VALUE`)} in the ${chalk.magenta('all')} context`, + `Set environment variable ${chalk.yellow(`${'NEW_ENV_VAR'}=${'NEW_VALUE'}`)} in the ${chalk.magenta( + 'all', + )} context`, ) }) }) @@ -348,9 +345,9 @@ describe('env:set command', () => { expect(promptSpy).not.toHaveBeenCalled() - expect(log).not.toHaveBeenCalledWith(expectedMessageAlreadyExists) - expect(log).not.toHaveBeenCalledWith(expectedNoticeMessage) - expect(log).toHaveBeenCalledWith(expectedSuccessMessage) + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(noticeMessage) + expect(log).toHaveBeenCalledWith(successMessage) }) }) @@ -372,9 +369,9 @@ describe('env:set command', () => { expect(promptSpy).toHaveBeenCalled() - expect(log).toHaveBeenCalledWith(expectedMessageAlreadyExists) - expect(log).toHaveBeenCalledWith(expectedNoticeMessage) - expect(log).not.toHaveBeenCalledWith(expectedSuccessMessage) + expect(log).toHaveBeenCalledWith(warningMessage) + expect(log).toHaveBeenCalledWith(noticeMessage) + expect(log).not.toHaveBeenCalledWith(successMessage) }) }) }) From 469691c381f618f8d0a1c5b65d82ec05032e4639 Mon Sep 17 00:00:00 2001 From: Will Conrad Date: Mon, 21 Oct 2024 15:54:14 -0500 Subject: [PATCH 18/39] fix: reactored env:unset prompts Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> --- src/commands/env/env-unset.ts | 4 +-- src/utils/prompts/unset-set-prompts.ts | 34 ++++++++++++------- .../commands/env/env-unset.test.ts | 27 ++++++--------- 3 files changed, 34 insertions(+), 31 deletions(-) diff --git a/src/commands/env/env-unset.ts b/src/commands/env/env-unset.ts index 958df29e310..d8d1311615e 100644 --- a/src/commands/env/env-unset.ts +++ b/src/commands/env/env-unset.ts @@ -2,7 +2,7 @@ import { OptionValues } from 'commander' import { chalk, log, logJson, exit } from '../../utils/command-helpers.js' import { AVAILABLE_CONTEXTS, translateFromEnvelopeToMongo } from '../../utils/env/index.js' -import { envUnsetPrompts } from '../../utils/prompts/unset-set-prompts.js' +import { promptOverwriteEnvVariable } from '../../utils/prompts/unset-set-prompts.js' import BaseCommand from '../base-command.js' /** * Deletes a given key from the env of a site configured with Envelope @@ -27,7 +27,7 @@ const unsetInEnvelope = async ({ api, context, force, key, siteInfo }) => { } if (Boolean(force) === false) { - await envUnsetPrompts(key) + await promptOverwriteEnvVariable(key) } const params = { accountId, siteId, key } diff --git a/src/utils/prompts/unset-set-prompts.ts b/src/utils/prompts/unset-set-prompts.ts index bf99557c316..79b89f316ec 100644 --- a/src/utils/prompts/unset-set-prompts.ts +++ b/src/utils/prompts/unset-set-prompts.ts @@ -2,18 +2,26 @@ import { chalk, log } from '../command-helpers.js' import { confirmPrompt } from './confirm-prompt.js' -const generateUnsetMessage = (variableName: string): void => { - log() - log( - `${chalk.redBright('Warning')}: The environment variable ${chalk.bgBlueBright( - variableName, - )} will be unset (deleted)!`, - ) - log() - log(`${chalk.yellowBright('Notice')}: To unset the variable without confirmation, pass the -f or --force flag.`) -} +export const generateUnsetMessage = (variableName: string) => ({ + warningMessage: `${chalk.redBright('Warning')}: The environment variable ${chalk.bgBlueBright( + variableName, + )} already exists!`, + noticeMessage: `${chalk.yellowBright( + 'Notice', + )}: To overwrite the existing variable without confirmation, pass the -f or --force flag.`, + confirmMessage: 'The environment variable already exists. Do you want to overwrite it?', +}) -export const envUnsetPrompts = async (key: string): Promise => { - generateUnsetMessage(key) - await confirmPrompt('Are you sure you want to unset (delete) the environment variable?') +/** + * Logs a warning and prompts user to confirm overwriting an existing environment variable + * + * @param {string} key - The key of the environment variable that already exists + * @returns {Promise} A promise that resolves when the user has confirmed overwriting the variable + */ +export const promptOverwriteEnvVariable = async (key: string): Promise => { + const { confirmMessage, noticeMessage, warningMessage } = generateUnsetMessage(key) + log(warningMessage) + log() + log(noticeMessage) + await confirmPrompt(confirmMessage) } diff --git a/tests/integration/commands/env/env-unset.test.ts b/tests/integration/commands/env/env-unset.test.ts index ecb0e1fab53..11c7e0bfd8e 100644 --- a/tests/integration/commands/env/env-unset.test.ts +++ b/tests/integration/commands/env/env-unset.test.ts @@ -7,6 +7,7 @@ import { describe, expect, test, vi, beforeEach } from 'vitest' import BaseCommand from '../../../../src/commands/base-command.js' import { createEnvCommand } from '../../../../src/commands/env/env.js' import { log } from '../../../../src/utils/command-helpers.js' +import { generateUnsetMessage } from '../../../../src/utils/prompts/unset-set-prompts.js' import { FixtureTestContext, setupFixtureTests } from '../../utils/fixture.js' import { getEnvironmentVariables, withMockApi } from '../../utils/mock-api.js' @@ -79,13 +80,7 @@ describe('env:unset command', () => { // already exists as value in withMockApi const existingVar = 'EXISTING_VAR' - const expectedWarningMessage = `${chalk.redBright('Warning')}: The environment variable ${chalk.bgBlueBright( - existingVar, - )} will be unset (deleted)!` - - const expectedNoticeMessage = `${chalk.yellowBright( - 'Notice', - )}: To unset the variable without confirmation, pass the -f or --force flag.` + const { confirmMessage, noticeMessage, warningMessage } = generateUnsetMessage(existingVar) const expectedSuccessMessage = `Unset environment variable ${chalk.yellow(`${existingVar}`)} in the ${chalk.magenta( 'all', @@ -109,12 +104,12 @@ describe('env:unset command', () => { expect(promptSpy).toHaveBeenCalledWith({ type: 'confirm', name: 'wantsToSet', - message: expect.stringContaining('Are you sure you want to unset (delete) the environment variable?'), + message: expect.stringContaining(confirmMessage), default: false, }) - expect(log).toHaveBeenCalledWith(expectedWarningMessage) - expect(log).toHaveBeenCalledWith(expectedNoticeMessage) + expect(log).toHaveBeenCalledWith(warningMessage) + expect(log).toHaveBeenCalledWith(noticeMessage) expect(log).toHaveBeenCalledWith(expectedSuccessMessage) }) }) @@ -132,8 +127,8 @@ describe('env:unset command', () => { expect(promptSpy).not.toHaveBeenCalled() - expect(log).not.toHaveBeenCalledWith(expectedWarningMessage) - expect(log).not.toHaveBeenCalledWith(expectedNoticeMessage) + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(noticeMessage) expect(log).toHaveBeenCalledWith(expectedSuccessMessage) }) }) @@ -156,8 +151,8 @@ describe('env:unset command', () => { expect(promptSpy).toHaveBeenCalled() - expect(log).toHaveBeenCalledWith(expectedWarningMessage) - expect(log).toHaveBeenCalledWith(expectedNoticeMessage) + expect(log).toHaveBeenCalledWith(warningMessage) + expect(log).toHaveBeenCalledWith(noticeMessage) expect(log).not.toHaveBeenCalledWith(expectedSuccessMessage) }) }) @@ -175,8 +170,8 @@ describe('env:unset command', () => { expect(promptSpy).not.toHaveBeenCalled() - expect(log).not.toHaveBeenCalledWith(expectedWarningMessage) - expect(log).not.toHaveBeenCalledWith(expectedNoticeMessage) + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(noticeMessage) expect(log).not.toHaveBeenCalledWith(expectedSuccessMessage) }) }) From f6aa58d7b0ac7fa7a081edcbc454368c21773ffe Mon Sep 17 00:00:00 2001 From: Will Conrad Date: Mon, 21 Oct 2024 20:20:17 -0500 Subject: [PATCH 19/39] fix: refactored prompts and tests messages Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> Co-authored-by: Thomas Lane --- src/commands/base-command.ts | 6 ++ src/commands/blobs/blobs-set.ts | 12 ++- src/commands/env/env-clone.ts | 10 ++- src/commands/env/env-set.ts | 6 ++ src/commands/env/env-unset.ts | 6 ++ src/utils/prompts/blob-set-prompt.ts | 19 ++--- src/utils/prompts/env-clone-prompt.ts | 61 ++++++--------- src/utils/prompts/env-set-prompts.ts | 35 +++------ src/utils/prompts/prompt-messages.ts | 37 ++++++++++ src/utils/prompts/unset-set-prompts.ts | 25 +++---- src/utils/types.ts | 14 ++++ .../commands/blobs/blobs-set.test.ts | 30 +++----- tests/integration/commands/env/api-routes.ts | 9 ++- .../commands/env/env-clone.test.ts | 74 +++++++++---------- .../commands/env/env-unset.test.ts | 17 +++-- 15 files changed, 206 insertions(+), 155 deletions(-) create mode 100644 src/utils/prompts/prompt-messages.ts diff --git a/src/commands/base-command.ts b/src/commands/base-command.ts index 406d8a546b2..13d7e11b95a 100644 --- a/src/commands/base-command.ts +++ b/src/commands/base-command.ts @@ -167,6 +167,12 @@ export default class BaseCommand extends Command { // eslint-disable-next-line workspace/no-process-cwd workingDir = process.cwd() + /** + * Determines if the command is scripted or not. + * If the command is scripted (SHLVL is greater than 1 or CI/CONTINUOUS_INTEGRATION is true) then some commands + * might behave differently. + */ + scriptedCommand = Boolean(process.env.SHLVL !== '1' || process.env.CI || process.env.CONTINUOUS_INTEGRATION) /** * The workspace root if inside a mono repository. * Must not be the repository root! diff --git a/src/commands/blobs/blobs-set.ts b/src/commands/blobs/blobs-set.ts index da81e5a14f3..df096000d76 100644 --- a/src/commands/blobs/blobs-set.ts +++ b/src/commands/blobs/blobs-set.ts @@ -5,12 +5,12 @@ import { getStore } from '@netlify/blobs' import { OptionValues } from 'commander' import { chalk, error as printError, isNodeError, log } from '../../utils/command-helpers.js' -import { blobSetPrompts } from '../../utils/prompts/blob-set-prompt.js' +import { promptBlobSetOverwrite } from '../../utils/prompts/blob-set-prompt.js' import BaseCommand from '../base-command.js' interface Options extends OptionValues { input?: string - force?: string + force?: string | boolean } export const blobsSet = async ( @@ -20,6 +20,12 @@ export const blobsSet = async ( options: Options, command: BaseCommand, ) => { + + // Prevents prompts from blocking scripted commands + if (command.scriptedCommand){ + options.force = true + } + const { api, siteInfo } = command.netlify const { force, input } = options const store = getStore({ @@ -61,7 +67,7 @@ export const blobsSet = async ( const existingValue = await store.get(key) if (existingValue) { - await blobSetPrompts(key, storeName) + await promptBlobSetOverwrite(key, storeName) } } diff --git a/src/commands/env/env-clone.ts b/src/commands/env/env-clone.ts index 78d14a8d05a..c8a450e78d9 100644 --- a/src/commands/env/env-clone.ts +++ b/src/commands/env/env-clone.ts @@ -1,7 +1,7 @@ import { OptionValues } from 'commander' import { chalk, log, error as logError } from '../../utils/command-helpers.js' -import { envClonePrompts } from '../../utils/prompts/env-clone-prompt.js' +import { promptEnvCloneOverwrite } from '../../utils/prompts/env-clone-prompt.js' import BaseCommand from '../base-command.js' // @ts-expect-error TS(7006) FIXME: Parameter 'api' implicitly has an 'any' type. @@ -39,7 +39,7 @@ const cloneEnvVars = async ({ api, force, siteFrom, siteTo }): Promise const envVarsToDelete = envelopeTo.filter(({ key }) => keysFrom.includes(key)) if (envVarsToDelete.length !== 0 && Boolean(force) === false) { - await envClonePrompts(siteTo.id, envVarsToDelete) + await promptEnvCloneOverwrite(siteTo.id, envVarsToDelete) } // delete marked env vars in parallel // @ts-expect-error TS(7031) FIXME: Binding element 'key' implicitly has an 'any' type... Remove this comment to see the full error message @@ -56,6 +56,12 @@ const cloneEnvVars = async ({ api, force, siteFrom, siteTo }): Promise } export const envClone = async (options: OptionValues, command: BaseCommand) => { + + // Prevents prompts from blocking scripted commands + if (command.scriptedCommand){ + options.force = true + } + const { api, site } = command.netlify const { force } = options diff --git a/src/commands/env/env-set.ts b/src/commands/env/env-set.ts index 9a7bd3a83b8..01a1460179a 100644 --- a/src/commands/env/env-set.ts +++ b/src/commands/env/env-set.ts @@ -116,6 +116,12 @@ const setInEnvelope = async ({ api, context, force, key, scope, secret, siteInfo } export const envSet = async (key: string, value: string, options: OptionValues, command: BaseCommand) => { + + // Prevents prompts from blocking scripted commands + if (command.scriptedCommand){ + options.force = true + } + const { context, force, scope, secret } = options const { api, cachedConfig, site } = command.netlify const siteId = site.id diff --git a/src/commands/env/env-unset.ts b/src/commands/env/env-unset.ts index d8d1311615e..30131287d55 100644 --- a/src/commands/env/env-unset.ts +++ b/src/commands/env/env-unset.ts @@ -68,6 +68,12 @@ const unsetInEnvelope = async ({ api, context, force, key, siteInfo }) => { } export const envUnset = async (key: string, options: OptionValues, command: BaseCommand) => { + + // Prevents prompts from blocking scripted commands + if (command.scriptedCommand){ + options.force = true + } + const { context, force } = options const { api, cachedConfig, site } = command.netlify const siteId = site.id diff --git a/src/utils/prompts/blob-set-prompt.ts b/src/utils/prompts/blob-set-prompt.ts index 677839a2762..2162d23ffe8 100644 --- a/src/utils/prompts/blob-set-prompt.ts +++ b/src/utils/prompts/blob-set-prompt.ts @@ -1,18 +1,19 @@ import { chalk, log } from '../command-helpers.js' import { confirmPrompt } from './confirm-prompt.js' +import { destructiveCommandMessages } from './prompt-messages.js' + +export const promptBlobSetOverwrite = async (key: string, storeName: string): Promise => { + const { overwriteNoticeMessage } = destructiveCommandMessages + const { generateWarningMessage, overwriteConfirmationMessage } = destructiveCommandMessages.blobSet + + const warningMessage = generateWarningMessage(storeName) -const generateBlobWarningMessage = (key: string, storeName: string): void => { log() - log(`${chalk.redBright('Warning')}: The following blob key already exists in store ${chalk.cyan(storeName)}:`) + log(warningMessage) log() log(`${chalk.bold(key)}`) log() - log(`This operation will ${chalk.redBright('overwrite')} the existing value.`) - log(`${chalk.yellowBright('Notice')}: To overwrite without this warning, you can use the --force flag.`) -} - -export const blobSetPrompts = async (key: string, storeName: string): Promise => { - generateBlobWarningMessage(key, storeName) - await confirmPrompt('Do you want to proceed with overwriting this blob key existing value?') + log(overwriteNoticeMessage) + await confirmPrompt(overwriteConfirmationMessage) } diff --git a/src/utils/prompts/env-clone-prompt.ts b/src/utils/prompts/env-clone-prompt.ts index fbcf67000e6..66cb2c3c804 100644 --- a/src/utils/prompts/env-clone-prompt.ts +++ b/src/utils/prompts/env-clone-prompt.ts @@ -1,49 +1,34 @@ -import { chalk, log } from '../command-helpers.js' +import { log } from '../command-helpers.js' +import { EnvVar } from '../types.js' import { confirmPrompt } from './confirm-prompt.js' +import { destructiveCommandMessages } from './prompt-messages.js' -type User = { - id: string - email: string - avatar_url: string - full_name: string -} +export const generateEnvVarsList = (envVarsToDelete: EnvVar[]) => envVarsToDelete.map((envVar) => envVar.key) -type EnvVar = { - key: string - scopes: string[] - values: Record[] - updated_at: string - updated_by: User - is_secret: boolean -} +/** + * Prompts the user to confirm overwriting environment variables on a site. + * + * @param {string} siteId - The ID of the site. + * @param {EnvVar[]} existingEnvVars - The environment variables that already exist on the site. + * @returns {Promise} A promise that resolves when the user has confirmed the overwriting of the variables. + */ +export async function promptEnvCloneOverwrite(siteId: string, existingEnvVars: EnvVar[]): Promise { + const { overwriteNoticeMessage } = destructiveCommandMessages + const { generateWarningMessage, noticeEnvVarsMessage, overwriteConfirmationMessage } = + destructiveCommandMessages.envClone + + const existingEnvVarKeys = generateEnvVarsList(existingEnvVars) + const warningMessage = generateWarningMessage(siteId) -const generateSetMessage = (envVarsToDelete: EnvVar[], siteId: string): void => { log() - log( - `${chalk.redBright( - 'Warning', - )}: The following environment variables are already set on the site with ID ${chalk.bgBlueBright( - siteId, - )}. They will be overwritten!`, - ) + log(warningMessage) log() - - log(`${chalk.yellowBright('Notice')}: The following variables will be overwritten:`) + log(noticeEnvVarsMessage) log() - envVarsToDelete.forEach((envVar) => { - log(envVar.key) - }) - + existingEnvVarKeys.forEach(log) log() - log( - `${chalk.yellowBright( - 'Notice', - )}: To overwrite the existing variables without confirmation prompts, pass the --force flag.`, - ) -} + log(overwriteNoticeMessage) -export const envClonePrompts = async (siteId: string, envVarsToDelete: EnvVar[]): Promise => { - generateSetMessage(envVarsToDelete, siteId) - await confirmPrompt('Do you want to proceed with overwriting these variables?') + await confirmPrompt(overwriteConfirmationMessage) } diff --git a/src/utils/prompts/env-set-prompts.ts b/src/utils/prompts/env-set-prompts.ts index f99a73cedf0..e256b333b4d 100644 --- a/src/utils/prompts/env-set-prompts.ts +++ b/src/utils/prompts/env-set-prompts.ts @@ -1,34 +1,23 @@ -import { chalk, log } from '../command-helpers.js' +import { log } from '../command-helpers.js' import { confirmPrompt } from './confirm-prompt.js' - -export const generateSetMessage = (variableName: string) => ({ - warningMessage: `${chalk.redBright('Warning')}: The environment variable ${chalk.bgBlueBright( - variableName, - )} already exists!`, - noticeMessage: `${chalk.yellowBright( - 'Notice', - )}: To overwrite the existing variable without confirmation, pass the -f or --force flag.`, - confirmMessage: 'The environment variable already exists. Do you want to overwrite it?', -}) +import { destructiveCommandMessages } from './prompt-messages.js' /** - * Generates warning, notice and confirm messages when trying to set an env variable - * that already exists. + * Prompts the user to confirm overwriting an existing environment variable. * - * @param {string} key - The key of the environment variable that already exists - * @returns {Object} An object with the following properties: - * - warning: A warning message to be displayed to the user - * - notice: A notice message to be displayed to the user - * - confirm: A confirmation prompt to ask the user if they want to overwrite the existing variable + * @param {string} existingKey - The key of the existing environment variable. + * @returns {Promise} A promise that resolves when the user confirms overwriting the variable. */ -export const promptOverwriteEnvVariable = async (key: string): Promise => { - const { confirmMessage, noticeMessage, warningMessage } = generateSetMessage(key) +export const promptOverwriteEnvVariable = async (existingKey: string): Promise => { + const { overwriteNoticeMessage } = destructiveCommandMessages + const { generateWarningMessage, overwriteConfirmationMessage } = destructiveCommandMessages.envSet + + const warningMessage = generateWarningMessage(existingKey) log() log(warningMessage) log() - log(noticeMessage) - log() - await confirmPrompt(confirmMessage) + log(overwriteNoticeMessage) + await confirmPrompt(overwriteConfirmationMessage) } diff --git a/src/utils/prompts/prompt-messages.ts b/src/utils/prompts/prompt-messages.ts new file mode 100644 index 00000000000..663706a6190 --- /dev/null +++ b/src/utils/prompts/prompt-messages.ts @@ -0,0 +1,37 @@ +import { chalk } from '../command-helpers.js' +import { EnvVar } from '../types.js' + +export const destructiveCommandMessages = { + overwriteNoticeMessage: `${chalk.yellowBright( + 'Notice', + )}: To overwrite without this warning, you can use the --force flag.`, + + blobSet: { + generateWarningMessage: (storeName: string) => + `${chalk.redBright('Warning')}: The blob key already exists in store ${chalk.cyan(storeName)}.`, + overwriteConfirmationMessage: 'Do you want to proceed with overwriting this blob key existing value?', + }, + + envSet: { + generateWarningMessage: (variableName: string) => + `${chalk.redBright('Warning')}: The environment variable ${chalk.bgBlueBright(variableName)} already exists.`, + overwriteConfirmationMessage: 'The environment variable already exists. Do you want to overwrite it?', + }, + + envUnset: { + generateWarningMessage: (variableName: string) => + `${chalk.redBright('Warning')}: The environment variable ${chalk.bgBlueBright(variableName)} already exists!`, + overwriteConfirmationMessage: 'The environment variable already exists. Do you want to overwrite it?', + }, + + envClone: { + generateWarningMessage: (siteId: string) => + `${chalk.redBright( + 'Warning', + )}: The following environment variables are already set on the site with ID ${chalk.bgBlueBright( + siteId, + )}. They will be overwritten!`, + noticeEnvVarsMessage: `${chalk.yellowBright('Notice')}: The following variables will be overwritten:`, + overwriteConfirmationMessage: 'The environment variables already exist. Do you want to overwrite them?', + }, +} diff --git a/src/utils/prompts/unset-set-prompts.ts b/src/utils/prompts/unset-set-prompts.ts index 79b89f316ec..8128922636e 100644 --- a/src/utils/prompts/unset-set-prompts.ts +++ b/src/utils/prompts/unset-set-prompts.ts @@ -1,16 +1,7 @@ -import { chalk, log } from '../command-helpers.js' +import { log } from '../command-helpers.js' import { confirmPrompt } from './confirm-prompt.js' - -export const generateUnsetMessage = (variableName: string) => ({ - warningMessage: `${chalk.redBright('Warning')}: The environment variable ${chalk.bgBlueBright( - variableName, - )} already exists!`, - noticeMessage: `${chalk.yellowBright( - 'Notice', - )}: To overwrite the existing variable without confirmation, pass the -f or --force flag.`, - confirmMessage: 'The environment variable already exists. Do you want to overwrite it?', -}) +import { destructiveCommandMessages } from './prompt-messages.js' /** * Logs a warning and prompts user to confirm overwriting an existing environment variable @@ -18,10 +9,14 @@ export const generateUnsetMessage = (variableName: string) => ({ * @param {string} key - The key of the environment variable that already exists * @returns {Promise} A promise that resolves when the user has confirmed overwriting the variable */ -export const promptOverwriteEnvVariable = async (key: string): Promise => { - const { confirmMessage, noticeMessage, warningMessage } = generateUnsetMessage(key) +export const promptOverwriteEnvVariable = async (existingKey: string): Promise => { + const { overwriteNoticeMessage } = destructiveCommandMessages + const { generateWarningMessage, overwriteConfirmationMessage } = destructiveCommandMessages.envUnset + + const warningMessage = generateWarningMessage(existingKey) + log(warningMessage) log() - log(noticeMessage) - await confirmPrompt(confirmMessage) + log(overwriteNoticeMessage) + await confirmPrompt(overwriteConfirmationMessage) } diff --git a/src/utils/types.ts b/src/utils/types.ts index 38ab8682b2b..2444f337353 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -64,3 +64,17 @@ export interface Request extends IncomingMessage { export type Rewriter = (req: Request) => Match | null export type TokenLocation = 'env' | 'flag' | 'config' | 'not found' + +export type EnvVar = { + key: string + scopes: string[] + values: EnvVarValue[] + updated_at: string + is_secret: boolean +} + +type EnvVarValue = { + id: string + context: string + value: string +} diff --git a/tests/integration/commands/blobs/blobs-set.test.ts b/tests/integration/commands/blobs/blobs-set.test.ts index ce022447f53..2a74228ef59 100644 --- a/tests/integration/commands/blobs/blobs-set.test.ts +++ b/tests/integration/commands/blobs/blobs-set.test.ts @@ -8,6 +8,7 @@ import { describe, expect, test, vi, beforeEach } from 'vitest' import BaseCommand from '../../../../src/commands/base-command.js' import { createBlobsCommand } from '../../../../src/commands/blobs/blobs.js' import { log } from '../../../../src/utils/command-helpers.js' +import { destructiveCommandMessages } from '../../../../src/utils/prompts/prompt-messages.js' import { Route } from '../../utils/mock-api-vitest.js' import { getEnvironmentVariables, withMockApi } from '../../utils/mock-api.js' @@ -46,15 +47,12 @@ describe('blob:set command', () => { const value = 'my-value' const newValue = 'my-new-value' - const warningMessage = `${chalk.redBright('Warning')}: The following blob key already exists in store ${chalk.cyan( - storeName, - )}:` + const { overwriteNoticeMessage } = destructiveCommandMessages + const { generateWarningMessage, overwriteConfirmationMessage } = destructiveCommandMessages.blobSet + + const warningMessage = generateWarningMessage(storeName) const boldKey = chalk.bold(key) - const overWriteMe = `This operation will ${chalk.redBright('overwrite')} the existing value.` - const noticeMessage = `${chalk.yellowBright( - 'Notice', - )}: To overwrite without this warning, you can use the --force flag.` const successMessage = `${chalk.greenBright('Success')}: Blob ${chalk.yellow(key)} set in store ${chalk.yellow( storeName, @@ -87,8 +85,7 @@ describe('blob:set command', () => { expect(log).toHaveBeenCalledWith(successMessage) expect(log).not.toHaveBeenCalledWith(warningMessage) expect(log).not.toHaveBeenCalledWith(boldKey) - expect(log).not.toHaveBeenCalledWith(overWriteMe) - expect(log).not.toHaveBeenCalledWith(noticeMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) }) }) @@ -115,15 +112,14 @@ describe('blob:set command', () => { expect(promptSpy).toHaveBeenCalledWith({ type: 'confirm', name: 'wantsToSet', - message: expect.stringContaining('Do you want to proceed with overwriting this blob key existing value?'), + message: expect.stringContaining(overwriteConfirmationMessage), default: false, }) expect(log).toHaveBeenCalledWith(successMessage) expect(log).toHaveBeenCalledWith(warningMessage) expect(log).toHaveBeenCalledWith(boldKey) - expect(log).toHaveBeenCalledWith(overWriteMe) - expect(log).toHaveBeenCalledWith(noticeMessage) + expect(log).toHaveBeenCalledWith(overwriteNoticeMessage) }) }) @@ -155,14 +151,13 @@ describe('blob:set command', () => { expect(promptSpy).toHaveBeenCalledWith({ type: 'confirm', name: 'wantsToSet', - message: expect.stringContaining('Do you want to proceed with overwriting this blob key existing value?'), + message: expect.stringContaining(overwriteConfirmationMessage), default: false, }) expect(log).toHaveBeenCalledWith(warningMessage) expect(log).toHaveBeenCalledWith(boldKey) - expect(log).toHaveBeenCalledWith(overWriteMe) - expect(log).toHaveBeenCalledWith(noticeMessage) + expect(log).toHaveBeenCalledWith(overwriteNoticeMessage) expect(log).not.toHaveBeenCalledWith(successMessage) }) }) @@ -191,8 +186,7 @@ describe('blob:set command', () => { expect(log).not.toHaveBeenCalledWith(warningMessage) expect(log).not.toHaveBeenCalledWith(boldKey) - expect(log).not.toHaveBeenCalledWith(overWriteMe) - expect(log).not.toHaveBeenCalledWith(noticeMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) expect(log).toHaveBeenCalledWith(successMessage) }) }) @@ -221,7 +215,7 @@ describe('blob:set command', () => { expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(noticeMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) expect(log).not.toHaveBeenCalledWith(successMessage) }) }) diff --git a/tests/integration/commands/env/api-routes.ts b/tests/integration/commands/env/api-routes.ts index b0394b5d7b8..d5913deb974 100644 --- a/tests/integration/commands/env/api-routes.ts +++ b/tests/integration/commands/env/api-routes.ts @@ -1,3 +1,4 @@ +import { EnvVar } from '../../../../src/utils/types' import { HTTPMethod } from '../../utils/mock-api-vitest' const siteInfo = { @@ -9,7 +10,7 @@ const siteInfo = { name: 'site-name', } -const secondSiteInfo = { +export const secondSiteInfo = { account_slug: 'test-account-2', build_settings: { env: {}, @@ -27,7 +28,7 @@ const thirdSiteInfo = { name: 'site-name-3', } -const existingVar = { +export const existingVar: EnvVar = { key: 'EXISTING_VAR', scopes: ['builds', 'functions'], values: [ @@ -42,6 +43,8 @@ const existingVar = { value: 'envelope-dev-value', }, ], + updated_at: '2020-01-01T00:00:00Z', + is_secret: false, } const otherVar = { @@ -59,7 +62,7 @@ const otherVar = { const response = [existingVar, otherVar] const secondSiteResponse = [existingVar] -const routes = [ +export const routes = [ { path: 'sites/site_id', response: siteInfo }, { path: 'sites/site_id_2', response: secondSiteInfo }, { path: 'sites/site_id_3', response: thirdSiteInfo }, diff --git a/tests/integration/commands/env/env-clone.test.ts b/tests/integration/commands/env/env-clone.test.ts index bf3b10c8cfc..79890018cb6 100644 --- a/tests/integration/commands/env/env-clone.test.ts +++ b/tests/integration/commands/env/env-clone.test.ts @@ -7,9 +7,11 @@ import { describe, expect, test, vi, beforeEach } from 'vitest' import BaseCommand from '../../../../src/commands/base-command.js' import { createEnvCommand } from '../../../../src/commands/env/index.js' import { log } from '../../../../src/utils/command-helpers.js' +import { generateEnvVarsList } from '../../../../src/utils/prompts/env-clone-prompt.js' +import { destructiveCommandMessages } from '../../../../src/utils/prompts/prompt-messages.js' import { getEnvironmentVariables, withMockApi } from '../../utils/mock-api.js' -import routes from './api-routes.js' +import { existingVar, routes, secondSiteInfo } from './api-routes.js' vi.mock('../../../../src/utils/command-helpers.js', async () => ({ ...(await vi.importActual('../../../../src/utils/command-helpers.js')), @@ -18,18 +20,16 @@ vi.mock('../../../../src/utils/command-helpers.js', async () => ({ describe('env:clone command', () => { describe('user is prompted to confirm when setting an env var that already exists', () => { - const sharedEnvVars = 'EXISTING_VAR' - const siteIdTwo = 'site_id_2' - const warningMessage = `${chalk.redBright( - 'Warning', - )}: The following environment variables are already set on the site with ID ${chalk.bgBlueBright( - siteIdTwo, - )}. They will be overwritten!` - const expectedNoticeMessage = `${chalk.yellowBright('Notice')}: The following variables will be overwritten:` - - const expectedSkipMessage = `${chalk.yellowBright( - 'Notice', - )}: To overwrite the existing variables without confirmation prompts, pass the --force flag.` + const sharedEnvVars = [existingVar, existingVar] + const siteIdTwo = secondSiteInfo.id + + const { overwriteNoticeMessage } = destructiveCommandMessages + const { generateWarningMessage, noticeEnvVarsMessage, overwriteConfirmationMessage } = + destructiveCommandMessages.envClone + + const envVarsList = generateEnvVarsList(sharedEnvVars) + const warningMessage = generateWarningMessage(siteIdTwo) + const expectedSuccessMessage = `Successfully cloned environment variables from ${chalk.green( 'site-name', )} to ${chalk.green('site-name-2')}` @@ -47,27 +47,21 @@ describe('env:clone command', () => { const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: true }) - await program.parseAsync(['', '', 'env:clone', '-t', 'site_id_2']) + await program.parseAsync(['', '', 'env:clone', '-t', siteIdTwo]) expect(promptSpy).toHaveBeenCalledWith({ type: 'confirm', name: 'wantsToSet', - message: expect.stringContaining('Do you want to proceed with overwriting these variables?'), + message: expect.stringContaining(overwriteConfirmationMessage), default: false, }) - expect(log).toHaveBeenCalledWith( - `${chalk.redBright( - 'Warning', - )}: The following environment variables are already set on the site with ID ${chalk.bgBlueBright( - 'site_id_2', - )}. They will be overwritten!`, - ) - expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(expectedNoticeMessage) - expect(log).toHaveBeenCalledWith(sharedEnvVars) - expect(log).toHaveBeenCalledWith(expectedSkipMessage) + expect(log).toHaveBeenCalledWith(noticeEnvVarsMessage) + envVarsList.forEach((envVar) => { + expect(log).toHaveBeenCalledWith(envVar) + }) + expect(log).toHaveBeenCalledWith(overwriteNoticeMessage) expect(log).toHaveBeenCalledWith(expectedSuccessMessage) }) }) @@ -81,14 +75,16 @@ describe('env:clone command', () => { const promptSpy = vi.spyOn(inquirer, 'prompt') - await program.parseAsync(['', '', 'env:clone', '--force', '-t', 'site_id_2']) + await program.parseAsync(['', '', 'env:clone', '--force', '-t', siteIdTwo]) expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(expectedNoticeMessage) - expect(log).not.toHaveBeenCalledWith(sharedEnvVars) - expect(log).not.toHaveBeenCalledWith(expectedSkipMessage) + envVarsList.forEach((envVar) => { + expect(log).not.toHaveBeenCalledWith(envVar) + }) + expect(log).not.toHaveBeenCalledWith(noticeEnvVarsMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) expect(log).toHaveBeenCalledWith(expectedSuccessMessage) }) }) @@ -103,7 +99,7 @@ describe('env:clone command', () => { const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: false }) try { - await program.parseAsync(['', '', 'env:clone', '-t', 'site_id_2']) + await program.parseAsync(['', '', 'env:clone', '-t', siteIdTwo]) } catch (error) { // We expect the process to exit, so this is fine expect(error.message).toContain('process.exit unexpectedly called') @@ -112,9 +108,11 @@ describe('env:clone command', () => { expect(promptSpy).toHaveBeenCalled() expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(expectedNoticeMessage) - expect(log).toHaveBeenCalledWith(sharedEnvVars) - expect(log).toHaveBeenCalledWith(expectedSkipMessage) + expect(log).toHaveBeenCalledWith(noticeEnvVarsMessage) + envVarsList.forEach((envVar) => { + expect(log).toHaveBeenCalledWith(envVar) + }) + expect(log).toHaveBeenCalledWith(overwriteNoticeMessage) expect(log).not.toHaveBeenCalledWith(expectedSuccessMessage) }) }) @@ -136,9 +134,11 @@ describe('env:clone command', () => { expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(expectedNoticeMessage) - expect(log).not.toHaveBeenCalledWith(sharedEnvVars) - expect(log).not.toHaveBeenCalledWith(expectedSkipMessage) + expect(log).not.toHaveBeenCalledWith(noticeEnvVarsMessage) + envVarsList.forEach((envVar) => { + expect(log).not.toHaveBeenCalledWith(envVar) + }) + expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) expect(log).toHaveBeenCalledWith(expectedSuccessMessageSite3) }) }) diff --git a/tests/integration/commands/env/env-unset.test.ts b/tests/integration/commands/env/env-unset.test.ts index 11c7e0bfd8e..35f2837f890 100644 --- a/tests/integration/commands/env/env-unset.test.ts +++ b/tests/integration/commands/env/env-unset.test.ts @@ -7,7 +7,7 @@ import { describe, expect, test, vi, beforeEach } from 'vitest' import BaseCommand from '../../../../src/commands/base-command.js' import { createEnvCommand } from '../../../../src/commands/env/env.js' import { log } from '../../../../src/utils/command-helpers.js' -import { generateUnsetMessage } from '../../../../src/utils/prompts/unset-set-prompts.js' +import { destructiveCommandMessages } from '../../../../src/utils/prompts/prompt-messages.js' import { FixtureTestContext, setupFixtureTests } from '../../utils/fixture.js' import { getEnvironmentVariables, withMockApi } from '../../utils/mock-api.js' @@ -80,7 +80,10 @@ describe('env:unset command', () => { // already exists as value in withMockApi const existingVar = 'EXISTING_VAR' - const { confirmMessage, noticeMessage, warningMessage } = generateUnsetMessage(existingVar) + const { overwriteNoticeMessage } = destructiveCommandMessages + const { generateWarningMessage, overwriteConfirmationMessage } = destructiveCommandMessages.envUnset + + const warningMessage = generateWarningMessage(existingVar) const expectedSuccessMessage = `Unset environment variable ${chalk.yellow(`${existingVar}`)} in the ${chalk.magenta( 'all', @@ -104,12 +107,12 @@ describe('env:unset command', () => { expect(promptSpy).toHaveBeenCalledWith({ type: 'confirm', name: 'wantsToSet', - message: expect.stringContaining(confirmMessage), + message: expect.stringContaining(overwriteConfirmationMessage), default: false, }) expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(noticeMessage) + expect(log).toHaveBeenCalledWith(overwriteNoticeMessage) expect(log).toHaveBeenCalledWith(expectedSuccessMessage) }) }) @@ -128,7 +131,7 @@ describe('env:unset command', () => { expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(noticeMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) expect(log).toHaveBeenCalledWith(expectedSuccessMessage) }) }) @@ -152,7 +155,7 @@ describe('env:unset command', () => { expect(promptSpy).toHaveBeenCalled() expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(noticeMessage) + expect(log).toHaveBeenCalledWith(overwriteNoticeMessage) expect(log).not.toHaveBeenCalledWith(expectedSuccessMessage) }) }) @@ -171,7 +174,7 @@ describe('env:unset command', () => { expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(noticeMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) expect(log).not.toHaveBeenCalledWith(expectedSuccessMessage) }) }) From 9ba5ada22c89075527ed52a2997d5db9abdafb38 Mon Sep 17 00:00:00 2001 From: Will Conrad Date: Mon, 21 Oct 2024 20:45:46 -0500 Subject: [PATCH 20/39] fix: another pass of refactoring env and blob commands Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> Co-authored-by: Thomas Lane --- src/commands/blobs/blobs-delete.ts | 9 ++++-- src/commands/blobs/blobs-set.ts | 3 +- src/commands/env/env-clone.ts | 3 +- src/commands/env/env-set.ts | 3 +- src/commands/env/env-unset.ts | 5 ++-- src/utils/prompts/blob-delete-prompts.ts | 24 +++++++-------- src/utils/prompts/confirm-prompt.ts | 6 ++-- src/utils/prompts/env-clone-prompt.ts | 4 ++- ...et-set-prompts.ts => env-unset-prompts.ts} | 0 src/utils/prompts/prompt-messages.ts | 9 +++++- .../commands/blobs/blobs-delete.test.ts | 30 +++++++++---------- .../commands/blobs/blobs-set.test.ts | 8 ++--- .../commands/env/env-clone.test.ts | 6 ++-- .../integration/commands/env/env-set.test.ts | 28 +++++++++-------- .../commands/env/env-unset.test.ts | 6 ++-- tests/integration/utils/mock-api.js | 4 +++ 16 files changed, 80 insertions(+), 68 deletions(-) rename src/utils/prompts/{unset-set-prompts.ts => env-unset-prompts.ts} (100%) diff --git a/src/commands/blobs/blobs-delete.ts b/src/commands/blobs/blobs-delete.ts index c430a947a26..c15d1b13965 100644 --- a/src/commands/blobs/blobs-delete.ts +++ b/src/commands/blobs/blobs-delete.ts @@ -1,12 +1,17 @@ import { getStore } from '@netlify/blobs' import { chalk, error as printError, log } from '../../utils/command-helpers.js' -import { blobDeletePrompts } from '../../utils/prompts/blob-delete-prompts.js' +import { promptBlobDelete } from '../../utils/prompts/blob-delete-prompts.js' /** * The blobs:delete command */ export const blobsDelete = async (storeName: string, key: string, _options: Record, command: any) => { + // Prevents prompts from blocking scripted commands + if (command.scriptedCommand) { + _options.force = true + } + const { api, siteInfo } = command.netlify const { force } = _options @@ -18,7 +23,7 @@ export const blobsDelete = async (storeName: string, key: string, _options: Reco }) if (force === undefined) { - await blobDeletePrompts(key, storeName) + await promptBlobDelete(key, storeName) } try { diff --git a/src/commands/blobs/blobs-set.ts b/src/commands/blobs/blobs-set.ts index df096000d76..326b512238f 100644 --- a/src/commands/blobs/blobs-set.ts +++ b/src/commands/blobs/blobs-set.ts @@ -20,9 +20,8 @@ export const blobsSet = async ( options: Options, command: BaseCommand, ) => { - // Prevents prompts from blocking scripted commands - if (command.scriptedCommand){ + if (command.scriptedCommand) { options.force = true } diff --git a/src/commands/env/env-clone.ts b/src/commands/env/env-clone.ts index c8a450e78d9..9a7184c2861 100644 --- a/src/commands/env/env-clone.ts +++ b/src/commands/env/env-clone.ts @@ -56,9 +56,8 @@ const cloneEnvVars = async ({ api, force, siteFrom, siteTo }): Promise } export const envClone = async (options: OptionValues, command: BaseCommand) => { - // Prevents prompts from blocking scripted commands - if (command.scriptedCommand){ + if (command.scriptedCommand) { options.force = true } diff --git a/src/commands/env/env-set.ts b/src/commands/env/env-set.ts index 01a1460179a..1d43d400946 100644 --- a/src/commands/env/env-set.ts +++ b/src/commands/env/env-set.ts @@ -116,9 +116,8 @@ const setInEnvelope = async ({ api, context, force, key, scope, secret, siteInfo } export const envSet = async (key: string, value: string, options: OptionValues, command: BaseCommand) => { - // Prevents prompts from blocking scripted commands - if (command.scriptedCommand){ + if (command.scriptedCommand) { options.force = true } diff --git a/src/commands/env/env-unset.ts b/src/commands/env/env-unset.ts index 30131287d55..3a7c756bdf7 100644 --- a/src/commands/env/env-unset.ts +++ b/src/commands/env/env-unset.ts @@ -2,7 +2,7 @@ import { OptionValues } from 'commander' import { chalk, log, logJson, exit } from '../../utils/command-helpers.js' import { AVAILABLE_CONTEXTS, translateFromEnvelopeToMongo } from '../../utils/env/index.js' -import { promptOverwriteEnvVariable } from '../../utils/prompts/unset-set-prompts.js' +import { promptOverwriteEnvVariable } from '../../utils/prompts/env-unset-prompts.js' import BaseCommand from '../base-command.js' /** * Deletes a given key from the env of a site configured with Envelope @@ -68,9 +68,8 @@ const unsetInEnvelope = async ({ api, context, force, key, siteInfo }) => { } export const envUnset = async (key: string, options: OptionValues, command: BaseCommand) => { - // Prevents prompts from blocking scripted commands - if (command.scriptedCommand){ + if (command.scriptedCommand) { options.force = true } diff --git a/src/utils/prompts/blob-delete-prompts.ts b/src/utils/prompts/blob-delete-prompts.ts index 301bf71cfb9..42e37b8428a 100644 --- a/src/utils/prompts/blob-delete-prompts.ts +++ b/src/utils/prompts/blob-delete-prompts.ts @@ -1,19 +1,17 @@ -import { chalk, log } from '../command-helpers.js' +import { log } from '../command-helpers.js' import { confirmPrompt } from './confirm-prompt.js' +import { destructiveCommandMessages } from './prompt-messages.js' + +export const promptBlobDelete = async (key: string, storeName: string): Promise => { + const { overwriteNoticeMessage } = destructiveCommandMessages + const { generateWarningMessage, overwriteConfirmationMessage } = destructiveCommandMessages.blobDelete + + const warningMessage = generateWarningMessage(key, storeName) -const generateBlobWarningMessage = (key: string, storeName: string): void => { log() - log( - `${chalk.redBright('Warning')}: The following blob key ${chalk.cyan(key)} will be deleted from store ${chalk.cyan( - storeName, - )}:`, - ) + log(warningMessage) log() - log(`${chalk.yellowBright('Notice')}: To overwrite without this warning, you can use the --force flag.`) -} - -export const blobDeletePrompts = async (key: string, storeName: string): Promise => { - generateBlobWarningMessage(key, storeName) - await confirmPrompt('Do you want to proceed with deleting the value at this key?') + log(overwriteNoticeMessage) + await confirmPrompt(overwriteConfirmationMessage) } diff --git a/src/utils/prompts/confirm-prompt.ts b/src/utils/prompts/confirm-prompt.ts index dc3c36426c6..fa6ea6dc89f 100644 --- a/src/utils/prompts/confirm-prompt.ts +++ b/src/utils/prompts/confirm-prompt.ts @@ -4,14 +4,14 @@ import { log, exit } from '../command-helpers.js' export const confirmPrompt = async (message: string): Promise => { try { - const { wantsToSet } = await inquirer.prompt({ + const { confirm } = await inquirer.prompt({ type: 'confirm', - name: 'wantsToSet', + name: 'confirm', message, default: false, }) log() - if (!wantsToSet) { + if (!confirm) { exit() } } catch (error) { diff --git a/src/utils/prompts/env-clone-prompt.ts b/src/utils/prompts/env-clone-prompt.ts index 66cb2c3c804..b72d2c73d26 100644 --- a/src/utils/prompts/env-clone-prompt.ts +++ b/src/utils/prompts/env-clone-prompt.ts @@ -26,7 +26,9 @@ export async function promptEnvCloneOverwrite(siteId: string, existingEnvVars: E log() log(noticeEnvVarsMessage) log() - existingEnvVarKeys.forEach(log) + existingEnvVarKeys.forEach((envVar) => { + log(envVar) + }) log() log(overwriteNoticeMessage) diff --git a/src/utils/prompts/unset-set-prompts.ts b/src/utils/prompts/env-unset-prompts.ts similarity index 100% rename from src/utils/prompts/unset-set-prompts.ts rename to src/utils/prompts/env-unset-prompts.ts diff --git a/src/utils/prompts/prompt-messages.ts b/src/utils/prompts/prompt-messages.ts index 663706a6190..5b6479a70ce 100644 --- a/src/utils/prompts/prompt-messages.ts +++ b/src/utils/prompts/prompt-messages.ts @@ -1,5 +1,4 @@ import { chalk } from '../command-helpers.js' -import { EnvVar } from '../types.js' export const destructiveCommandMessages = { overwriteNoticeMessage: `${chalk.yellowBright( @@ -12,6 +11,14 @@ export const destructiveCommandMessages = { overwriteConfirmationMessage: 'Do you want to proceed with overwriting this blob key existing value?', }, + blobDelete: { + generateWarningMessage: (key: string, storeName: string) => + `${chalk.redBright('Warning')}: The following blob key ${chalk.cyan(key)} will be deleted from store ${chalk.cyan( + storeName, + )}:`, + overwriteConfirmationMessage: 'Do you want to proceed with deleting the value at this key?', + }, + envSet: { generateWarningMessage: (variableName: string) => `${chalk.redBright('Warning')}: The environment variable ${chalk.bgBlueBright(variableName)} already exists.`, diff --git a/tests/integration/commands/blobs/blobs-delete.test.ts b/tests/integration/commands/blobs/blobs-delete.test.ts index dc387c0bd7f..fbe3766ebf2 100644 --- a/tests/integration/commands/blobs/blobs-delete.test.ts +++ b/tests/integration/commands/blobs/blobs-delete.test.ts @@ -8,6 +8,7 @@ import { describe, expect, test, vi, beforeEach } from 'vitest' import BaseCommand from '../../../../src/commands/base-command.js' import { createBlobsCommand } from '../../../../src/commands/blobs/blobs.js' import { log } from '../../../../src/utils/command-helpers.js' +import { destructiveCommandMessages } from '../../../../src/utils/prompts/prompt-messages.js' import { Route } from '../../utils/mock-api-vitest.js' import { getEnvironmentVariables, withMockApi } from '../../utils/mock-api.js' @@ -44,13 +45,10 @@ describe('blob:delete command', () => { const storeName = 'my-store' const key = 'my-key' - const warningMessage = `${chalk.redBright('Warning')}: The following blob key ${chalk.cyan( - key, - )} will be deleted from store ${chalk.cyan(storeName)}:` + const { overwriteNoticeMessage } = destructiveCommandMessages + const { generateWarningMessage, overwriteConfirmationMessage } = destructiveCommandMessages.blobDelete - const noticeMessage = `${chalk.yellowBright( - 'Notice', - )}: To overwrite without this warning, you can use the --force flag.` + const warningMessage = generateWarningMessage(key, storeName) const successMessage = `${chalk.greenBright('Success')}: Blob ${chalk.yellow(key)} deleted from store ${chalk.yellow( storeName, @@ -73,19 +71,19 @@ describe('blob:delete command', () => { const program = new BaseCommand('netlify') createBlobsCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: true }) + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: true }) await program.parseAsync(['', '', 'blob:delete', storeName, key]) expect(promptSpy).toHaveBeenCalledWith({ type: 'confirm', - name: 'wantsToSet', - message: expect.stringContaining('Do you want to proceed with deleting the value at this key?'), + name: 'confirm', + message: expect.stringContaining(overwriteConfirmationMessage), default: false, }) expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(noticeMessage) + expect(log).toHaveBeenCalledWith(overwriteNoticeMessage) expect(log).toHaveBeenCalledWith(successMessage) }) }) @@ -103,7 +101,7 @@ describe('blob:delete command', () => { const program = new BaseCommand('netlify') createBlobsCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: false }) + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: false }) try { await program.parseAsync(['', '', 'blob:delete', storeName, key]) @@ -114,13 +112,13 @@ describe('blob:delete command', () => { expect(promptSpy).toHaveBeenCalledWith({ type: 'confirm', - name: 'wantsToSet', - message: expect.stringContaining('Do you want to proceed with deleting the value at this key?'), + name: 'confirm', + message: expect.stringContaining(overwriteConfirmationMessage), default: false, }) expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(noticeMessage) + expect(log).toHaveBeenCalledWith(overwriteNoticeMessage) expect(log).not.toHaveBeenCalledWith(successMessage) }) }) @@ -145,7 +143,7 @@ describe('blob:delete command', () => { expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(noticeMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) expect(log).toHaveBeenCalledWith(successMessage) }) }) @@ -176,7 +174,7 @@ describe('blob:delete command', () => { expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(noticeMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) expect(log).not.toHaveBeenCalledWith(successMessage) }) }) diff --git a/tests/integration/commands/blobs/blobs-set.test.ts b/tests/integration/commands/blobs/blobs-set.test.ts index 2a74228ef59..17650b122a1 100644 --- a/tests/integration/commands/blobs/blobs-set.test.ts +++ b/tests/integration/commands/blobs/blobs-set.test.ts @@ -105,13 +105,13 @@ describe('blob:set command', () => { const program = new BaseCommand('netlify') createBlobsCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: true }) + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: true }) await program.parseAsync(['', '', 'blob:set', storeName, key, newValue]) expect(promptSpy).toHaveBeenCalledWith({ type: 'confirm', - name: 'wantsToSet', + name: 'confirm', message: expect.stringContaining(overwriteConfirmationMessage), default: false, }) @@ -139,7 +139,7 @@ describe('blob:set command', () => { const program = new BaseCommand('netlify') createBlobsCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: false }) + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: false }) try { await program.parseAsync(['', '', 'blob:set', storeName, key, newValue]) @@ -150,7 +150,7 @@ describe('blob:set command', () => { expect(promptSpy).toHaveBeenCalledWith({ type: 'confirm', - name: 'wantsToSet', + name: 'confirm', message: expect.stringContaining(overwriteConfirmationMessage), default: false, }) diff --git a/tests/integration/commands/env/env-clone.test.ts b/tests/integration/commands/env/env-clone.test.ts index 79890018cb6..984a74e2ef2 100644 --- a/tests/integration/commands/env/env-clone.test.ts +++ b/tests/integration/commands/env/env-clone.test.ts @@ -45,13 +45,13 @@ describe('env:clone command', () => { const program = new BaseCommand('netlify') createEnvCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: true }) + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: true }) await program.parseAsync(['', '', 'env:clone', '-t', siteIdTwo]) expect(promptSpy).toHaveBeenCalledWith({ type: 'confirm', - name: 'wantsToSet', + name: 'confirm', message: expect.stringContaining(overwriteConfirmationMessage), default: false, }) @@ -96,7 +96,7 @@ describe('env:clone command', () => { const program = new BaseCommand('netlify') createEnvCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: false }) + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: false }) try { await program.parseAsync(['', '', 'env:clone', '-t', siteIdTwo]) diff --git a/tests/integration/commands/env/env-set.test.ts b/tests/integration/commands/env/env-set.test.ts index 2938d250a43..b540b1c1152 100644 --- a/tests/integration/commands/env/env-set.test.ts +++ b/tests/integration/commands/env/env-set.test.ts @@ -7,11 +7,11 @@ import { describe, expect, test, vi, beforeEach } from 'vitest' import BaseCommand from '../../../../src/commands/base-command.js' import { createEnvCommand } from '../../../../src/commands/env/env.js' import { log } from '../../../../src/utils/command-helpers.js' -import { generateSetMessage } from '../../../../src/utils/prompts/env-set-prompts.js' +import { destructiveCommandMessages } from '../../../../src/utils/prompts/prompt-messages.js' import { FixtureTestContext, setupFixtureTests } from '../../utils/fixture.js' import { getEnvironmentVariables, withMockApi } from '../../utils/mock-api.js' -import routes from './api-routes.js' +import { routes } from './api-routes.js' vi.mock('../../../../src/utils/command-helpers.js', async () => ({ ...(await vi.importActual('../../../../src/utils/command-helpers.js')), @@ -269,12 +269,14 @@ describe('env:set command', () => { }) }) - describe('user is prompted to confirm when setting an env var that already exists', () => { + describe.only('user is prompted to confirmOverwrite when setting an env var that already exists', () => { // already exists as value in withMockApi const existingVar = 'EXISTING_VAR' const newEnvValue = 'value' + const { overwriteNoticeMessage } = destructiveCommandMessages + const { generateWarningMessage, overwriteConfirmationMessage } = destructiveCommandMessages.envSet - const { confirmMessage, noticeMessage, warningMessage } = generateSetMessage(existingVar) + const warningMessage = generateWarningMessage(existingVar) const successMessage = `Set environment variable ${chalk.yellow( `${existingVar}=${newEnvValue}`, @@ -287,10 +289,10 @@ describe('env:set command', () => { test('should log warnings and prompts if enviroment variable already exists', async () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - + console.log(process.env.SHLVL) const program = new BaseCommand('netlify') - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: true }) + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: true }) createEnvCommand(program) @@ -298,13 +300,13 @@ describe('env:set command', () => { expect(promptSpy).toHaveBeenCalledWith({ type: 'confirm', - name: 'wantsToSet', - message: expect.stringContaining(confirmMessage), + name: 'confirm', + message: expect.stringContaining(overwriteConfirmationMessage), default: false, }) expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(noticeMessage) + expect(log).toHaveBeenCalledWith(overwriteNoticeMessage) expect(log).toHaveBeenCalledWith(successMessage) }) }) @@ -323,7 +325,7 @@ describe('env:set command', () => { expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(noticeMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) expect(log).toHaveBeenCalledWith( `Set environment variable ${chalk.yellow(`${'NEW_ENV_VAR'}=${'NEW_VALUE'}`)} in the ${chalk.magenta( 'all', @@ -346,7 +348,7 @@ describe('env:set command', () => { expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(noticeMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) expect(log).toHaveBeenCalledWith(successMessage) }) }) @@ -358,7 +360,7 @@ describe('env:set command', () => { const program = new BaseCommand('netlify') createEnvCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: false }) + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: false }) try { await program.parseAsync(['', '', 'env:set', existingVar, newEnvValue]) @@ -370,7 +372,7 @@ describe('env:set command', () => { expect(promptSpy).toHaveBeenCalled() expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(noticeMessage) + expect(log).toHaveBeenCalledWith(overwriteNoticeMessage) expect(log).not.toHaveBeenCalledWith(successMessage) }) }) diff --git a/tests/integration/commands/env/env-unset.test.ts b/tests/integration/commands/env/env-unset.test.ts index 35f2837f890..4af9adea62a 100644 --- a/tests/integration/commands/env/env-unset.test.ts +++ b/tests/integration/commands/env/env-unset.test.ts @@ -100,13 +100,13 @@ describe('env:unset command', () => { const program = new BaseCommand('netlify') createEnvCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: true }) + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: true }) await program.parseAsync(['', '', 'env:unset', existingVar]) expect(promptSpy).toHaveBeenCalledWith({ type: 'confirm', - name: 'wantsToSet', + name: 'confirm', message: expect.stringContaining(overwriteConfirmationMessage), default: false, }) @@ -143,7 +143,7 @@ describe('env:unset command', () => { const program = new BaseCommand('netlify') createEnvCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: false }) + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: false }) try { await program.parseAsync(['', '', 'env:unset', existingVar]) diff --git a/tests/integration/utils/mock-api.js b/tests/integration/utils/mock-api.js index d330902a80b..a03f84fc7cd 100644 --- a/tests/integration/utils/mock-api.js +++ b/tests/integration/utils/mock-api.js @@ -78,10 +78,14 @@ export const withMockApi = async (routes, testHandler, silent = false) => { } } +// `SHLVL` set to "1" to mock commands run from terminal command line +// `SHLVL` used to overwrite prompts for scripted commands in production/dev +// environments see `scriptedCommand` property of `BaseCommand` export const getEnvironmentVariables = ({ apiUrl }) => ({ NETLIFY_AUTH_TOKEN: 'fake-token', NETLIFY_SITE_ID: 'site_id', NETLIFY_API_URL: apiUrl, + SHLVL: '1', }) /** From a51fe6e1252b287b4345b3d2a85182bd984a5866 Mon Sep 17 00:00:00 2001 From: Will Conrad Date: Mon, 21 Oct 2024 22:07:54 -0500 Subject: [PATCH 21/39] feat: added skip for non interactive shell and CI Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> Co-authored-by: Thomas Lane --- src/commands/base-command.ts | 3 +- src/commands/env/env-set.ts | 5 +- src/utils/prompts/blob-set-prompt.ts | 6 +- src/utils/prompts/prompt-messages.ts | 18 +++--- .../commands/blobs/blobs-set.test.ts | 2 +- .../integration/commands/env/env-set.test.ts | 62 ++++++++++++++++++- tests/integration/utils/mock-api.js | 3 +- 7 files changed, 81 insertions(+), 18 deletions(-) diff --git a/src/commands/base-command.ts b/src/commands/base-command.ts index 13d7e11b95a..f21c4d0f2e6 100644 --- a/src/commands/base-command.ts +++ b/src/commands/base-command.ts @@ -172,7 +172,8 @@ export default class BaseCommand extends Command { * If the command is scripted (SHLVL is greater than 1 or CI/CONTINUOUS_INTEGRATION is true) then some commands * might behave differently. */ - scriptedCommand = Boolean(process.env.SHLVL !== '1' || process.env.CI || process.env.CONTINUOUS_INTEGRATION) + scriptedCommand = Boolean(!process.stdin.isTTY || process.env.CI || process.env.CONTINUOUS_INTEGRATION) + /** * The workspace root if inside a mono repository. * Must not be the repository root! diff --git a/src/commands/env/env-set.ts b/src/commands/env/env-set.ts index 1d43d400946..b860fe9ea42 100644 --- a/src/commands/env/env-set.ts +++ b/src/commands/env/env-set.ts @@ -1,3 +1,5 @@ +import process from 'process' + import { OptionValues } from 'commander' import { chalk, error, log, logJson } from '../../utils/command-helpers.js' @@ -116,7 +118,7 @@ const setInEnvelope = async ({ api, context, force, key, scope, secret, siteInfo } export const envSet = async (key: string, value: string, options: OptionValues, command: BaseCommand) => { - // Prevents prompts from blocking scripted commands + // Prevents prompts from ci and non ineractive shells if (command.scriptedCommand) { options.force = true } @@ -128,7 +130,6 @@ export const envSet = async (key: string, value: string, options: OptionValues, log('No site id found, please run inside a site folder or `netlify link`') return false } - const { siteInfo } = cachedConfig // Get current environment variables set in the UI diff --git a/src/utils/prompts/blob-set-prompt.ts b/src/utils/prompts/blob-set-prompt.ts index 2162d23ffe8..7d7ee58b12e 100644 --- a/src/utils/prompts/blob-set-prompt.ts +++ b/src/utils/prompts/blob-set-prompt.ts @@ -1,4 +1,4 @@ -import { chalk, log } from '../command-helpers.js' +import { log } from '../command-helpers.js' import { confirmPrompt } from './confirm-prompt.js' import { destructiveCommandMessages } from './prompt-messages.js' @@ -7,13 +7,11 @@ export const promptBlobSetOverwrite = async (key: string, storeName: string): Pr const { overwriteNoticeMessage } = destructiveCommandMessages const { generateWarningMessage, overwriteConfirmationMessage } = destructiveCommandMessages.blobSet - const warningMessage = generateWarningMessage(storeName) + const warningMessage = generateWarningMessage(key, storeName) log() log(warningMessage) log() - log(`${chalk.bold(key)}`) - log() log(overwriteNoticeMessage) await confirmPrompt(overwriteConfirmationMessage) } diff --git a/src/utils/prompts/prompt-messages.ts b/src/utils/prompts/prompt-messages.ts index 5b6479a70ce..f8b8fcf02d0 100644 --- a/src/utils/prompts/prompt-messages.ts +++ b/src/utils/prompts/prompt-messages.ts @@ -6,8 +6,10 @@ export const destructiveCommandMessages = { )}: To overwrite without this warning, you can use the --force flag.`, blobSet: { - generateWarningMessage: (storeName: string) => - `${chalk.redBright('Warning')}: The blob key already exists in store ${chalk.cyan(storeName)}.`, + generateWarningMessage: (key: string, storeName: string) => + `${chalk.redBright('Warning')}: The blob key ${chalk.cyan(key)} already exists in store ${chalk.cyan( + storeName, + )}!`, overwriteConfirmationMessage: 'Do you want to proceed with overwriting this blob key existing value?', }, @@ -15,20 +17,22 @@ export const destructiveCommandMessages = { generateWarningMessage: (key: string, storeName: string) => `${chalk.redBright('Warning')}: The following blob key ${chalk.cyan(key)} will be deleted from store ${chalk.cyan( storeName, - )}:`, + )}!`, overwriteConfirmationMessage: 'Do you want to proceed with deleting the value at this key?', }, envSet: { generateWarningMessage: (variableName: string) => - `${chalk.redBright('Warning')}: The environment variable ${chalk.bgBlueBright(variableName)} already exists.`, - overwriteConfirmationMessage: 'The environment variable already exists. Do you want to overwrite it?', + `${chalk.redBright('Warning')}: The environment variable ${chalk.bgBlueBright(variableName)} already exists!`, + overwriteConfirmationMessage: 'Do you want to overwrite it?', }, envUnset: { generateWarningMessage: (variableName: string) => - `${chalk.redBright('Warning')}: The environment variable ${chalk.bgBlueBright(variableName)} already exists!`, - overwriteConfirmationMessage: 'The environment variable already exists. Do you want to overwrite it?', + `${chalk.redBright('Warning')}: The environment variable ${chalk.bgBlueBright( + variableName, + )} will be removed from all contexts!`, + overwriteConfirmationMessage: 'Do you want to remove it?', }, envClone: { diff --git a/tests/integration/commands/blobs/blobs-set.test.ts b/tests/integration/commands/blobs/blobs-set.test.ts index 17650b122a1..c04c94ec1d8 100644 --- a/tests/integration/commands/blobs/blobs-set.test.ts +++ b/tests/integration/commands/blobs/blobs-set.test.ts @@ -50,7 +50,7 @@ describe('blob:set command', () => { const { overwriteNoticeMessage } = destructiveCommandMessages const { generateWarningMessage, overwriteConfirmationMessage } = destructiveCommandMessages.blobSet - const warningMessage = generateWarningMessage(storeName) + const warningMessage = generateWarningMessage(key, storeName) const boldKey = chalk.bold(key) diff --git a/tests/integration/commands/env/env-set.test.ts b/tests/integration/commands/env/env-set.test.ts index b540b1c1152..5c9bc3990da 100644 --- a/tests/integration/commands/env/env-set.test.ts +++ b/tests/integration/commands/env/env-set.test.ts @@ -283,13 +283,14 @@ describe('env:set command', () => { )} in the ${chalk.magenta('all')} context` beforeEach(() => { + process.stdin.isTTY = true vi.resetAllMocks() }) test('should log warnings and prompts if enviroment variable already exists', async () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - console.log(process.env.SHLVL) + const program = new BaseCommand('netlify') const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: true }) @@ -377,4 +378,63 @@ describe('env:set command', () => { }) }) }) + + describe('prompts should not show is in interactie shell or in a ci/cd enviroment', () => { + const existingVar = 'EXISTING_VAR' + const newEnvValue = 'value' + + const { generateWarningMessage } = destructiveCommandMessages.envSet + const { overwriteNoticeMessage } = destructiveCommandMessages + const warningMessage = generateWarningMessage('EXISTING_VAR') + + const successMessage = `Set environment variable ${chalk.yellow( + `${existingVar}=${newEnvValue}`, + )} in the ${chalk.magenta('all')} context` + + beforeEach(() => { + vi.resetAllMocks() + }) + + test('should not show is in interactie shell or in a ci/cd enviroment', async () => { + process.stdin.isTTY = false + + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const program = new BaseCommand('netlify') + createEnvCommand(program) + + const promptSpy = vi.spyOn(inquirer, 'prompt') + + await program.parseAsync(['', '', 'env:set', existingVar, newEnvValue]) + + expect(promptSpy).not.toHaveBeenCalled() + + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).toHaveBeenCalledWith(successMessage) + }) + }) + + test('should not show prompt in a ci/cd enviroment', async () => { + process.env.CI = true + + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const program = new BaseCommand('netlify') + createEnvCommand(program) + + const promptSpy = vi.spyOn(inquirer, 'prompt') + + await program.parseAsync(['', '', 'env:set', existingVar, newEnvValue]) + + expect(promptSpy).not.toHaveBeenCalled() + + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).toHaveBeenCalledWith(successMessage) + }) + }) + }) }) diff --git a/tests/integration/utils/mock-api.js b/tests/integration/utils/mock-api.js index a03f84fc7cd..8b4566bc688 100644 --- a/tests/integration/utils/mock-api.js +++ b/tests/integration/utils/mock-api.js @@ -78,14 +78,13 @@ export const withMockApi = async (routes, testHandler, silent = false) => { } } -// `SHLVL` set to "1" to mock commands run from terminal command line +// `CI` set to "true" to mock commands run from terminal command line // `SHLVL` used to overwrite prompts for scripted commands in production/dev // environments see `scriptedCommand` property of `BaseCommand` export const getEnvironmentVariables = ({ apiUrl }) => ({ NETLIFY_AUTH_TOKEN: 'fake-token', NETLIFY_SITE_ID: 'site_id', NETLIFY_API_URL: apiUrl, - SHLVL: '1', }) /** From fe4bc7f8c1883957a2959dd9ebcbd12bca509e76 Mon Sep 17 00:00:00 2001 From: Will Conrad Date: Tue, 22 Oct 2024 12:55:58 -0500 Subject: [PATCH 22/39] feat: refactored code for tests realted to ci and prompts Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> Co-authored-by: Thomas Lane --- src/commands/base-command.ts | 2 +- .../commands/blobs/blobs-delete.test.ts | 252 ++++++++++----- .../commands/blobs/blobs-set.test.ts | 305 +++++++++++------- .../commands/env/env-clone.test.ts | 90 ++++-- .../integration/commands/env/env-set.test.ts | 57 ++-- .../commands/env/env-unset.test.ts | 82 ++++- tests/integration/utils/mock-api.js | 19 ++ 7 files changed, 539 insertions(+), 268 deletions(-) diff --git a/src/commands/base-command.ts b/src/commands/base-command.ts index f21c4d0f2e6..6ee0033101c 100644 --- a/src/commands/base-command.ts +++ b/src/commands/base-command.ts @@ -172,7 +172,7 @@ export default class BaseCommand extends Command { * If the command is scripted (SHLVL is greater than 1 or CI/CONTINUOUS_INTEGRATION is true) then some commands * might behave differently. */ - scriptedCommand = Boolean(!process.stdin.isTTY || process.env.CI || process.env.CONTINUOUS_INTEGRATION) + scriptedCommand = Boolean(!process.stdin.isTTY || isCI || process.env.CI ) /** * The workspace root if inside a mono repository. diff --git a/tests/integration/commands/blobs/blobs-delete.test.ts b/tests/integration/commands/blobs/blobs-delete.test.ts index fbe3766ebf2..739670c8834 100644 --- a/tests/integration/commands/blobs/blobs-delete.test.ts +++ b/tests/integration/commands/blobs/blobs-delete.test.ts @@ -3,14 +3,15 @@ import process from 'process' import { getStore } from '@netlify/blobs' import chalk from 'chalk' import inquirer from 'inquirer' -import { describe, expect, test, vi, beforeEach } from 'vitest' +import { describe, expect, test, vi, beforeEach, afterEach, beforeAll } from 'vitest' import BaseCommand from '../../../../src/commands/base-command.js' import { createBlobsCommand } from '../../../../src/commands/blobs/blobs.js' import { log } from '../../../../src/utils/command-helpers.js' import { destructiveCommandMessages } from '../../../../src/utils/prompts/prompt-messages.js' +import { reportError } from '../../../../src/utils/telemetry/report-error.js' import { Route } from '../../utils/mock-api-vitest.js' -import { getEnvironmentVariables, withMockApi } from '../../utils/mock-api.js' +import { getEnvironmentVariables, withMockApi, setTTYMode, setCI } from '../../utils/mock-api.js' const siteInfo = { account_slug: 'test-account', @@ -25,12 +26,17 @@ const siteInfo = { vi.mock('../../../../src/utils/command-helpers.js', async () => ({ ...(await vi.importActual('../../../../src/utils/command-helpers.js')), log: vi.fn(), + printError: vi.fn(), })) vi.mock('@netlify/blobs', () => ({ getStore: vi.fn(), })) +vi.mock('../../../../src/utils/telemetry/report-error.js', () => ({ + reportError: vi.fn(), +})) + const routes: Route[] = [ { path: 'sites/site_id', response: siteInfo }, @@ -54,128 +60,204 @@ describe('blob:delete command', () => { storeName, )}` - beforeEach(() => { - vi.resetAllMocks() - }) + describe('user is prompted to confirm when deleting a blob key', () => { + beforeAll(() => { + setTTYMode(true) + }) - test('should log warning message and prompt for confirmation', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + beforeEach(() => { + vi.resetAllMocks() + }) - const mockDelete = vi.fn().mockResolvedValue('true') + test('should log warning message and prompt for confirmation', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - ;(getStore as any).mockReturnValue({ - delete: mockDelete, - }) + const mockDelete = vi.fn().mockResolvedValue('true') - const program = new BaseCommand('netlify') - createBlobsCommand(program) + ;(getStore as any).mockReturnValue({ + delete: mockDelete, + }) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: true }) + const program = new BaseCommand('netlify') + createBlobsCommand(program) - await program.parseAsync(['', '', 'blob:delete', storeName, key]) + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: true }) - expect(promptSpy).toHaveBeenCalledWith({ - type: 'confirm', - name: 'confirm', - message: expect.stringContaining(overwriteConfirmationMessage), - default: false, - }) + await program.parseAsync(['', '', 'blob:delete', storeName, key]) + + expect(promptSpy).toHaveBeenCalledWith({ + type: 'confirm', + name: 'confirm', + message: expect.stringContaining(overwriteConfirmationMessage), + default: false, + }) - expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(overwriteNoticeMessage) - expect(log).toHaveBeenCalledWith(successMessage) + expect(log).toHaveBeenCalledWith(warningMessage) + expect(log).toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).toHaveBeenCalledWith(successMessage) + }) }) - }) - test('should exit if user responds with no to confirmation prompt', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + test('should exit if user responds with no to confirmation prompt', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const mockDelete = vi.fn().mockResolvedValue('true') + + ;(getStore as any).mockReturnValue({ + delete: mockDelete, + }) - const mockDelete = vi.fn().mockResolvedValue('true') + const program = new BaseCommand('netlify') + createBlobsCommand(program) - ;(getStore as any).mockReturnValue({ - delete: mockDelete, + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: false }) + + try { + await program.parseAsync(['', '', 'blob:delete', storeName, key]) + } catch (error) { + // We expect the process to exit, so this is fine + expect(error.message).toContain('process.exit unexpectedly called') + } + + expect(promptSpy).toHaveBeenCalledWith({ + type: 'confirm', + name: 'confirm', + message: expect.stringContaining(overwriteConfirmationMessage), + default: false, + }) + + expect(log).toHaveBeenCalledWith(warningMessage) + expect(log).toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).not.toHaveBeenCalledWith(successMessage) }) + }) + + test('should not log warning message and prompt for confirmation if --force flag is passed', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const mockDelete = vi.fn().mockResolvedValue('true') + + ;(getStore as any).mockReturnValue({ + delete: mockDelete, + }) - const program = new BaseCommand('netlify') - createBlobsCommand(program) + const program = new BaseCommand('netlify') + createBlobsCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: false }) + const promptSpy = vi.spyOn(inquirer, 'prompt') + + await program.parseAsync(['', '', 'blob:delete', storeName, key, '--force']) + + expect(promptSpy).not.toHaveBeenCalled() + + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).toHaveBeenCalledWith(successMessage) + }) + }) + test('should log error message if delete fails', async () => { try { - await program.parseAsync(['', '', 'blob:delete', storeName, key]) + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + vi.mocked(reportError).mockResolvedValue() + + const mockDelete = vi.fn().mockRejectedValue(new Error('Could not delete blob')) + + ;(getStore as any).mockReturnValue({ + delete: mockDelete, + }) + + const program = new BaseCommand('netlify') + createBlobsCommand(program) + + const promptSpy = vi.spyOn(inquirer, 'prompt') + + try { + await program.parseAsync(['', '', 'blob:delete', storeName, key, '--force']) + } catch (error) { + expect(error.message).toContain( + `Could not delete blob ${chalk.yellow(key)} from store ${chalk.yellow(storeName)}`, + ) + } + + expect(promptSpy).not.toHaveBeenCalled() + + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).not.toHaveBeenCalledWith(successMessage) + }) } catch (error) { - // We expect the process to exit, so this is fine - expect(error.message).toContain('process.exit unexpectedly called') + console.error(error) } + }) + }) - expect(promptSpy).toHaveBeenCalledWith({ - type: 'confirm', - name: 'confirm', - message: expect.stringContaining(overwriteConfirmationMessage), - default: false, - }) + describe('should not show prompts if in non-interactive shell or CI/CD', () => { + beforeEach(() => { + vi.resetAllMocks() + }) - expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(overwriteNoticeMessage) - expect(log).not.toHaveBeenCalledWith(successMessage) + afterEach(() => { + setTTYMode(true) + setCI('') }) - }) - test('should not log warning message and prompt for confirmation if --force flag is passed', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + test('should not show prompt for non-interactive shell', async () => { + setTTYMode(false) - const mockDelete = vi.fn().mockResolvedValue('true') + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - ;(getStore as any).mockReturnValue({ - delete: mockDelete, - }) + const mockDelete = vi.fn().mockResolvedValue('true') - const program = new BaseCommand('netlify') - createBlobsCommand(program) + ;(getStore as any).mockReturnValue({ + delete: mockDelete, + }) - const promptSpy = vi.spyOn(inquirer, 'prompt') + const program = new BaseCommand('netlify') + createBlobsCommand(program) - await program.parseAsync(['', '', 'blob:delete', storeName, key, '--force']) + const promptSpy = vi.spyOn(inquirer, 'prompt') + + await program.parseAsync(['', '', 'blob:delete', storeName, key]) - expect(promptSpy).not.toHaveBeenCalled() + expect(promptSpy).not.toHaveBeenCalled() - expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) - expect(log).toHaveBeenCalledWith(successMessage) + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).toHaveBeenCalledWith(successMessage) + }) }) - }) - test('should log error message if delete fails', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + test('should not show prompt for CI/CD', async () => { + setCI(true) + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const mockDelete = vi.fn().mockRejectedValue('') + const mockDelete = vi.fn().mockResolvedValue('true') - ;(getStore as any).mockReturnValue({ - delete: mockDelete, - }) + ;(getStore as any).mockReturnValue({ + delete: mockDelete, + }) - const program = new BaseCommand('netlify') - createBlobsCommand(program) + const program = new BaseCommand('netlify') + createBlobsCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt') + const promptSpy = vi.spyOn(inquirer, 'prompt') - try { - await program.parseAsync(['', '', 'blob:delete', storeName, key, '--force']) - } catch (error) { - expect(error.message).toContain( - `Could not delete blob ${chalk.yellow(key)} from store ${chalk.yellow(storeName)}`, - ) - } + await program.parseAsync(['', '', 'blob:delete', storeName, key]) - expect(promptSpy).not.toHaveBeenCalled() + expect(promptSpy).not.toHaveBeenCalled() - expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) - expect(log).not.toHaveBeenCalledWith(successMessage) + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).toHaveBeenCalledWith(successMessage) + }) }) }) }) diff --git a/tests/integration/commands/blobs/blobs-set.test.ts b/tests/integration/commands/blobs/blobs-set.test.ts index c04c94ec1d8..206fbdd0f1c 100644 --- a/tests/integration/commands/blobs/blobs-set.test.ts +++ b/tests/integration/commands/blobs/blobs-set.test.ts @@ -3,14 +3,15 @@ import process from 'process' import { getStore } from '@netlify/blobs' import chalk from 'chalk' import inquirer from 'inquirer' -import { describe, expect, test, vi, beforeEach } from 'vitest' +import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest' import BaseCommand from '../../../../src/commands/base-command.js' import { createBlobsCommand } from '../../../../src/commands/blobs/blobs.js' import { log } from '../../../../src/utils/command-helpers.js' import { destructiveCommandMessages } from '../../../../src/utils/prompts/prompt-messages.js' +import { reportError } from '../../../../src/utils/telemetry/report-error.js' import { Route } from '../../utils/mock-api-vitest.js' -import { getEnvironmentVariables, withMockApi } from '../../utils/mock-api.js' +import { getEnvironmentVariables, withMockApi, setTTYMode, setCI } from '../../utils/mock-api.js' const siteInfo = { account_slug: 'test-account', @@ -31,6 +32,10 @@ vi.mock('@netlify/blobs', () => ({ getStore: vi.fn(), })) +vi.mock('../../../../src/utils/telemetry/report-error.js', () => ({ + reportError: vi.fn(), +})) + const routes: Route[] = [ { path: 'sites/site_id', response: siteInfo }, @@ -52,171 +57,237 @@ describe('blob:set command', () => { const warningMessage = generateWarningMessage(key, storeName) - const boldKey = chalk.bold(key) - const successMessage = `${chalk.greenBright('Success')}: Blob ${chalk.yellow(key)} set in store ${chalk.yellow( storeName, )}` - beforeEach(() => { - vi.resetAllMocks() - }) + describe('user is prompted to confirm when setting a a blob key that already exists', () => { + beforeEach(() => { + vi.resetAllMocks() + setTTYMode(true) + }) - test('should not log warnings and prompt if blob key does not exist', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + test('should not log warnings and prompt if blob key does not exist', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const mockGet = vi.fn().mockResolvedValue('') - const mockSet = vi.fn().mockResolvedValue('true') + const mockGet = vi.fn().mockResolvedValue('') + const mockSet = vi.fn().mockResolvedValue('true') - ;(getStore as any).mockReturnValue({ - get: mockGet, - set: mockSet, - }) + ;(getStore as any).mockReturnValue({ + get: mockGet, + set: mockSet, + }) - const program = new BaseCommand('netlify') - createBlobsCommand(program) + const program = new BaseCommand('netlify') + createBlobsCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: true }) + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: true }) - await program.parseAsync(['', '', 'blob:set', storeName, key, value]) + await program.parseAsync(['', '', 'blob:set', storeName, key, value]) - expect(promptSpy).not.toHaveBeenCalled() - expect(log).toHaveBeenCalledWith(successMessage) - expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(boldKey) - expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(promptSpy).not.toHaveBeenCalled() + expect(log).toHaveBeenCalledWith(successMessage) + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + }) }) - }) - test('should log warnings and prompt if blob key already exists', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + test('should log warnings and prompt if blob key already exists', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - // Mocking the store.get method to return a value (simulating that the key already exists) - const mockGet = vi.fn().mockResolvedValue(value) - const mockSet = vi.fn().mockResolvedValue('true') + // Mocking the store.get method to return a value (simulating that the key already exists) + const mockGet = vi.fn().mockResolvedValue(value) + const mockSet = vi.fn().mockResolvedValue('true') - ;(getStore as any).mockReturnValue({ - get: mockGet, - set: mockSet, - }) + ;(getStore as any).mockReturnValue({ + get: mockGet, + set: mockSet, + }) - const program = new BaseCommand('netlify') - createBlobsCommand(program) + const program = new BaseCommand('netlify') + createBlobsCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: true }) + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: true }) - await program.parseAsync(['', '', 'blob:set', storeName, key, newValue]) + await program.parseAsync(['', '', 'blob:set', storeName, key, newValue]) + + expect(promptSpy).toHaveBeenCalledWith({ + type: 'confirm', + name: 'confirm', + message: expect.stringContaining(overwriteConfirmationMessage), + default: false, + }) - expect(promptSpy).toHaveBeenCalledWith({ - type: 'confirm', - name: 'confirm', - message: expect.stringContaining(overwriteConfirmationMessage), - default: false, + expect(log).toHaveBeenCalledWith(successMessage) + expect(log).toHaveBeenCalledWith(warningMessage) + expect(log).toHaveBeenCalledWith(overwriteNoticeMessage) }) + }) - expect(log).toHaveBeenCalledWith(successMessage) - expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(boldKey) - expect(log).toHaveBeenCalledWith(overwriteNoticeMessage) + test('should exit if user responds with no to confirmation prompt', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + // Mocking the store.get method to return a value (simulating that the key already exists) + const mockGet = vi.fn().mockResolvedValue('my-value') + const mockSet = vi.fn().mockResolvedValue('true') + + ;(getStore as any).mockReturnValue({ + get: mockGet, + set: mockSet, + }) + + const program = new BaseCommand('netlify') + createBlobsCommand(program) + + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: false }) + + try { + await program.parseAsync(['', '', 'blob:set', storeName, key, newValue]) + } catch (error) { + // We expect the process to exit, so this is fine + expect(error.message).toContain('process.exit unexpectedly called with "0"') + } + + expect(promptSpy).toHaveBeenCalledWith({ + type: 'confirm', + name: 'confirm', + message: expect.stringContaining(overwriteConfirmationMessage), + default: false, + }) + + expect(log).toHaveBeenCalledWith(warningMessage) + expect(log).toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).not.toHaveBeenCalledWith(successMessage) + }) }) - }) - test('should exit if user responds with no to confirmation prompt', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + test('should not log warnings and prompt if blob key already exists and --force flag is passed', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - // Mocking the store.get method to return a value (simulating that the key already exists) - const mockGet = vi.fn().mockResolvedValue('my-value') - const mockSet = vi.fn().mockResolvedValue('true') + // Mocking the store.get method to return a value (simulating that the key already exists) + const mockGet = vi.fn().mockResolvedValue('my-value') + const mockSet = vi.fn().mockResolvedValue('true') - ;(getStore as any).mockReturnValue({ - get: mockGet, - set: mockSet, - }) + ;(getStore as any).mockReturnValue({ + get: mockGet, + set: mockSet, + }) - const program = new BaseCommand('netlify') - createBlobsCommand(program) + const program = new BaseCommand('netlify') + createBlobsCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: false }) + const promptSpy = vi.spyOn(inquirer, 'prompt') - try { - await program.parseAsync(['', '', 'blob:set', storeName, key, newValue]) - } catch (error) { - // We expect the process to exit, so this is fine - expect(error.message).toContain('process.exit unexpectedly called with "0"') - } - - expect(promptSpy).toHaveBeenCalledWith({ - type: 'confirm', - name: 'confirm', - message: expect.stringContaining(overwriteConfirmationMessage), - default: false, + await program.parseAsync(['', '', 'blob:set', storeName, key, newValue, '--force']) + + expect(promptSpy).not.toHaveBeenCalled() + + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).toHaveBeenCalledWith(successMessage) }) + }) + + test('should log error message if adding a key fails', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const mockSet = vi.fn().mockRejectedValue('') + vi.mocked(reportError).mockResolvedValue() + ;(getStore as any).mockReturnValue({ + set: mockSet, + }) + + const program = new BaseCommand('netlify') + createBlobsCommand(program) + + const promptSpy = vi.spyOn(inquirer, 'prompt') - expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(boldKey) - expect(log).toHaveBeenCalledWith(overwriteNoticeMessage) - expect(log).not.toHaveBeenCalledWith(successMessage) + try { + await program.parseAsync(['', '', 'blob:set', storeName, key, newValue, '--force']) + } catch (error) { + expect(error.message).toContain(`Could not set blob ${chalk.yellow(key)} in store ${chalk.yellow(storeName)}`) + } + + expect(promptSpy).not.toHaveBeenCalled() + + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).not.toHaveBeenCalledWith(successMessage) + }) }) }) - test('should not log warnings and prompt if blob key already exists and --force flag is passed', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + beforeEach(() => { + vi.resetAllMocks() + }) + + describe('prompts should not show in a non-interactive shell or in a ci/cd enviroment', () => { + afterEach(() => { + setTTYMode(true) + setCI('') + }) + test('should not show prompt in an non-interactive shell', async () => { + setTTYMode(false) - // Mocking the store.get method to return a value (simulating that the key already exists) - const mockGet = vi.fn().mockResolvedValue('my-value') - const mockSet = vi.fn().mockResolvedValue('true') + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - ;(getStore as any).mockReturnValue({ - get: mockGet, - set: mockSet, - }) + // Mocking the store.get method to return a value (simulating that the key already exists) + const mockGet = vi.fn().mockResolvedValue('my-value') + const mockSet = vi.fn().mockResolvedValue('true') + + ;(getStore as any).mockReturnValue({ + get: mockGet, + set: mockSet, + }) - const program = new BaseCommand('netlify') - createBlobsCommand(program) + const program = new BaseCommand('netlify') + createBlobsCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt') + const promptSpy = vi.spyOn(inquirer, 'prompt') - await program.parseAsync(['', '', 'blob:set', storeName, key, newValue, '--force']) + await program.parseAsync(['', '', 'blob:set', storeName, key, newValue, '--force']) - expect(promptSpy).not.toHaveBeenCalled() + expect(promptSpy).not.toHaveBeenCalled() - expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(boldKey) - expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) - expect(log).toHaveBeenCalledWith(successMessage) + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).toHaveBeenCalledWith(successMessage) + }) }) - }) - test('should log error message if adding a key fails', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + test('should not show prompt in a ci/cd environment', async () => { + setCI(true) + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const mockSet = vi.fn().mockRejectedValue('') + // Mocking the store.get method to return a value (simulating that the key already exists) + const mockGet = vi.fn().mockResolvedValue('my-value') + const mockSet = vi.fn().mockResolvedValue('true') - ;(getStore as any).mockReturnValue({ - set: mockSet, - }) + ;(getStore as any).mockReturnValue({ + get: mockGet, + set: mockSet, + }) - const program = new BaseCommand('netlify') - createBlobsCommand(program) + const program = new BaseCommand('netlify') + createBlobsCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt') + const promptSpy = vi.spyOn(inquirer, 'prompt') - try { await program.parseAsync(['', '', 'blob:set', storeName, key, newValue, '--force']) - } catch (error) { - expect(error.message).toContain(`Could not set blob ${chalk.yellow(key)} in store ${chalk.yellow(storeName)}`) - } - expect(promptSpy).not.toHaveBeenCalled() + expect(promptSpy).not.toHaveBeenCalled() - expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) - expect(log).not.toHaveBeenCalledWith(successMessage) + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).toHaveBeenCalledWith(successMessage) + }) }) }) }) diff --git a/tests/integration/commands/env/env-clone.test.ts b/tests/integration/commands/env/env-clone.test.ts index 984a74e2ef2..3ae6d0a5433 100644 --- a/tests/integration/commands/env/env-clone.test.ts +++ b/tests/integration/commands/env/env-clone.test.ts @@ -2,14 +2,14 @@ import process from 'process' import chalk from 'chalk' import inquirer from 'inquirer' -import { describe, expect, test, vi, beforeEach } from 'vitest' +import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest' import BaseCommand from '../../../../src/commands/base-command.js' import { createEnvCommand } from '../../../../src/commands/env/index.js' import { log } from '../../../../src/utils/command-helpers.js' import { generateEnvVarsList } from '../../../../src/utils/prompts/env-clone-prompt.js' import { destructiveCommandMessages } from '../../../../src/utils/prompts/prompt-messages.js' -import { getEnvironmentVariables, withMockApi } from '../../utils/mock-api.js' +import { getEnvironmentVariables, withMockApi, setTTYMode, setCI } from '../../utils/mock-api.js' import { existingVar, routes, secondSiteInfo } from './api-routes.js' @@ -19,22 +19,23 @@ vi.mock('../../../../src/utils/command-helpers.js', async () => ({ })) describe('env:clone command', () => { - describe('user is prompted to confirm when setting an env var that already exists', () => { - const sharedEnvVars = [existingVar, existingVar] - const siteIdTwo = secondSiteInfo.id + const sharedEnvVars = [existingVar, existingVar] + const siteIdTwo = secondSiteInfo.id - const { overwriteNoticeMessage } = destructiveCommandMessages - const { generateWarningMessage, noticeEnvVarsMessage, overwriteConfirmationMessage } = - destructiveCommandMessages.envClone + const { overwriteNoticeMessage } = destructiveCommandMessages + const { generateWarningMessage, noticeEnvVarsMessage, overwriteConfirmationMessage } = + destructiveCommandMessages.envClone - const envVarsList = generateEnvVarsList(sharedEnvVars) - const warningMessage = generateWarningMessage(siteIdTwo) + const envVarsList = generateEnvVarsList(sharedEnvVars) + const warningMessage = generateWarningMessage(siteIdTwo) - const expectedSuccessMessage = `Successfully cloned environment variables from ${chalk.green( - 'site-name', - )} to ${chalk.green('site-name-2')}` + const successMessage = `Successfully cloned environment variables from ${chalk.green('site-name')} to ${chalk.green( + 'site-name-2', + )}` + describe('user is prompted to confirm when setting an env var that already exists', () => { beforeEach(() => { + setTTYMode(true) vi.resetAllMocks() }) @@ -62,7 +63,7 @@ describe('env:clone command', () => { expect(log).toHaveBeenCalledWith(envVar) }) expect(log).toHaveBeenCalledWith(overwriteNoticeMessage) - expect(log).toHaveBeenCalledWith(expectedSuccessMessage) + expect(log).toHaveBeenCalledWith(successMessage) }) }) @@ -85,7 +86,7 @@ describe('env:clone command', () => { }) expect(log).not.toHaveBeenCalledWith(noticeEnvVarsMessage) expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) - expect(log).toHaveBeenCalledWith(expectedSuccessMessage) + expect(log).toHaveBeenCalledWith(successMessage) }) }) @@ -113,14 +114,14 @@ describe('env:clone command', () => { expect(log).toHaveBeenCalledWith(envVar) }) expect(log).toHaveBeenCalledWith(overwriteNoticeMessage) - expect(log).not.toHaveBeenCalledWith(expectedSuccessMessage) + expect(log).not.toHaveBeenCalledWith(successMessage) }) }) test('should not run prompts if sites have no enviroment variables in common', async () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const expectedSuccessMessageSite3 = `Successfully cloned environment variables from ${chalk.green( + const successMessageSite3 = `Successfully cloned environment variables from ${chalk.green( 'site-name', )} to ${chalk.green('site-name-3')}` @@ -139,7 +140,60 @@ describe('env:clone command', () => { expect(log).not.toHaveBeenCalledWith(envVar) }) expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) - expect(log).toHaveBeenCalledWith(expectedSuccessMessageSite3) + expect(log).toHaveBeenCalledWith(successMessageSite3) + }) + }) + }) + + describe('should not run prompts if in non-interactive shell or CI/CD environment', async () => { + beforeEach(() => { + vi.resetAllMocks() + }) + + afterEach(() => { + setTTYMode(true) + setCI('') + }) + + test('should not show prompt in an non-interactive shell', async () => { + setTTYMode(false) + + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const program = new BaseCommand('netlify') + createEnvCommand(program) + + const promptSpy = vi.spyOn(inquirer, 'prompt') + + await program.parseAsync(['', '', 'env:clone', '-t', siteIdTwo]) + + expect(promptSpy).not.toHaveBeenCalled() + + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).toHaveBeenCalledWith(successMessage) + }) + }) + + test('should not show prompt in a ci/cd enviroment', async () => { + setCI(true) + + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const program = new BaseCommand('netlify') + createEnvCommand(program) + + const promptSpy = vi.spyOn(inquirer, 'prompt') + + await program.parseAsync(['', '', 'env:clone', '-t', siteIdTwo]) + + expect(promptSpy).not.toHaveBeenCalled() + + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).toHaveBeenCalledWith(successMessage) }) }) }) diff --git a/tests/integration/commands/env/env-set.test.ts b/tests/integration/commands/env/env-set.test.ts index 5c9bc3990da..26b11f9bafb 100644 --- a/tests/integration/commands/env/env-set.test.ts +++ b/tests/integration/commands/env/env-set.test.ts @@ -2,14 +2,14 @@ import process from 'process' import chalk from 'chalk' import inquirer from 'inquirer' -import { describe, expect, test, vi, beforeEach } from 'vitest' +import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest' import BaseCommand from '../../../../src/commands/base-command.js' import { createEnvCommand } from '../../../../src/commands/env/env.js' import { log } from '../../../../src/utils/command-helpers.js' import { destructiveCommandMessages } from '../../../../src/utils/prompts/prompt-messages.js' import { FixtureTestContext, setupFixtureTests } from '../../utils/fixture.js' -import { getEnvironmentVariables, withMockApi } from '../../utils/mock-api.js' +import { getEnvironmentVariables, withMockApi, setTTYMode, setCI } from '../../utils/mock-api.js' import { routes } from './api-routes.js' @@ -19,6 +19,18 @@ vi.mock('../../../../src/utils/command-helpers.js', async () => ({ })) describe('env:set command', () => { + // already exists as value in withMockApi + const existingVar = 'EXISTING_VAR' + const newEnvValue = 'value' + const { overwriteNoticeMessage } = destructiveCommandMessages + const { generateWarningMessage, overwriteConfirmationMessage } = destructiveCommandMessages.envSet + + const warningMessage = generateWarningMessage(existingVar) + + const successMessage = `Set environment variable ${chalk.yellow( + `${existingVar}=${newEnvValue}`, + )} in the ${chalk.magenta('all')} context` + setupFixtureTests('empty-project', { mockApi: { routes } }, () => { test('should create and return new var in the dev context', async ({ fixture, mockApi }) => { const cliResponse = await fixture.callCli( @@ -269,21 +281,9 @@ describe('env:set command', () => { }) }) - describe.only('user is prompted to confirmOverwrite when setting an env var that already exists', () => { - // already exists as value in withMockApi - const existingVar = 'EXISTING_VAR' - const newEnvValue = 'value' - const { overwriteNoticeMessage } = destructiveCommandMessages - const { generateWarningMessage, overwriteConfirmationMessage } = destructiveCommandMessages.envSet - - const warningMessage = generateWarningMessage(existingVar) - - const successMessage = `Set environment variable ${chalk.yellow( - `${existingVar}=${newEnvValue}`, - )} in the ${chalk.magenta('all')} context` - + describe('user is prompted to confirmOverwrite when setting an env var that already exists', () => { beforeEach(() => { - process.stdin.isTTY = true + setTTYMode(true) vi.resetAllMocks() }) @@ -354,7 +354,7 @@ describe('env:set command', () => { }) }) - test('should exit user reponds is no to confirmatnion prompt', async () => { + test('should exit user responds is no to confirmatnion prompt', async () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) @@ -379,24 +379,17 @@ describe('env:set command', () => { }) }) - describe('prompts should not show is in interactie shell or in a ci/cd enviroment', () => { - const existingVar = 'EXISTING_VAR' - const newEnvValue = 'value' - - const { generateWarningMessage } = destructiveCommandMessages.envSet - const { overwriteNoticeMessage } = destructiveCommandMessages - const warningMessage = generateWarningMessage('EXISTING_VAR') - - const successMessage = `Set environment variable ${chalk.yellow( - `${existingVar}=${newEnvValue}`, - )} in the ${chalk.magenta('all')} context` - + describe('prompts should not show in an non-interactive shell or in a ci/cd enviroment', () => { beforeEach(() => { vi.resetAllMocks() }) + afterEach(() => { + setTTYMode(true) + setCI('') + }) - test('should not show is in interactie shell or in a ci/cd enviroment', async () => { - process.stdin.isTTY = false + test('should not show prompt in an non-interactive shell', async () => { + setTTYMode(false) await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) @@ -417,7 +410,7 @@ describe('env:set command', () => { }) test('should not show prompt in a ci/cd enviroment', async () => { - process.env.CI = true + setCI(true) await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) diff --git a/tests/integration/commands/env/env-unset.test.ts b/tests/integration/commands/env/env-unset.test.ts index 4af9adea62a..d91c9c94a2d 100644 --- a/tests/integration/commands/env/env-unset.test.ts +++ b/tests/integration/commands/env/env-unset.test.ts @@ -2,16 +2,16 @@ import process from 'process' import chalk from 'chalk' import inquirer from 'inquirer' -import { describe, expect, test, vi, beforeEach } from 'vitest' +import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest' import BaseCommand from '../../../../src/commands/base-command.js' import { createEnvCommand } from '../../../../src/commands/env/env.js' import { log } from '../../../../src/utils/command-helpers.js' import { destructiveCommandMessages } from '../../../../src/utils/prompts/prompt-messages.js' import { FixtureTestContext, setupFixtureTests } from '../../utils/fixture.js' -import { getEnvironmentVariables, withMockApi } from '../../utils/mock-api.js' +import { getEnvironmentVariables, withMockApi, setTTYMode, setCI } from '../../utils/mock-api.js' -import routes from './api-routes.js' +import { routes } from './api-routes.js' vi.mock('../../../../src/utils/command-helpers.js', async () => ({ ...(await vi.importActual('../../../../src/utils/command-helpers.js')), @@ -19,6 +19,16 @@ vi.mock('../../../../src/utils/command-helpers.js', async () => ({ })) describe('env:unset command', () => { + const { overwriteNoticeMessage } = destructiveCommandMessages + const { generateWarningMessage, overwriteConfirmationMessage } = destructiveCommandMessages.envUnset + + // already exists as value in withMockApi + const existingVar = 'EXISTING_VAR' + const warningMessage = generateWarningMessage(existingVar) + const expectedSuccessMessage = `Unset environment variable ${chalk.yellow(`${existingVar}`)} in the ${chalk.magenta( + 'all', + )} context` + setupFixtureTests('empty-project', { mockApi: { routes } }, () => { test('should remove existing variable', async ({ fixture, mockApi }) => { const cliResponse = await fixture.callCli(['env:unset', '--json', 'EXISTING_VAR', '--force'], { @@ -77,19 +87,8 @@ describe('env:unset command', () => { }) describe('user is prompted to confirm when unsetting an env var that already exists', () => { - // already exists as value in withMockApi - const existingVar = 'EXISTING_VAR' - - const { overwriteNoticeMessage } = destructiveCommandMessages - const { generateWarningMessage, overwriteConfirmationMessage } = destructiveCommandMessages.envUnset - - const warningMessage = generateWarningMessage(existingVar) - - const expectedSuccessMessage = `Unset environment variable ${chalk.yellow(`${existingVar}`)} in the ${chalk.magenta( - 'all', - )} context` - beforeEach(() => { + setTTYMode(true) vi.resetAllMocks() }) @@ -179,4 +178,57 @@ describe('env:unset command', () => { }) }) }) + + describe('prompts should not show in an non-interactive shell or in a ci/cd enviroment', () => { + beforeEach(() => { + vi.resetAllMocks() + }) + + afterEach(() => { + setTTYMode(true) + setCI('') + }) + + test('prompts should not show in an non-interactive shell', async () => { + setTTYMode(false) + + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const program = new BaseCommand('netlify') + createEnvCommand(program) + + const promptSpy = vi.spyOn(inquirer, 'prompt') + + await program.parseAsync(['', '', 'env:unset', existingVar]) + + expect(promptSpy).not.toHaveBeenCalled() + + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).toHaveBeenCalledWith(expectedSuccessMessage) + }) + }) + + test('prompts should not show in a ci/cd enviroment', async () => { + setCI(true) + + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const program = new BaseCommand('netlify') + createEnvCommand(program) + + const promptSpy = vi.spyOn(inquirer, 'prompt') + + await program.parseAsync(['', '', 'env:unset', existingVar]) + + expect(promptSpy).not.toHaveBeenCalled() + + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).toHaveBeenCalledWith(expectedSuccessMessage) + }) + }) + }) }) diff --git a/tests/integration/utils/mock-api.js b/tests/integration/utils/mock-api.js index 8b4566bc688..eaf13487cfc 100644 --- a/tests/integration/utils/mock-api.js +++ b/tests/integration/utils/mock-api.js @@ -1,3 +1,4 @@ +import process from 'process' import { isDeepStrictEqual, promisify } from 'util' import express from 'express' @@ -87,6 +88,24 @@ export const getEnvironmentVariables = ({ apiUrl }) => ({ NETLIFY_API_URL: apiUrl, }) +/** + * Set the `isTTY` property of `process.stdin` to the given boolean value. + * This function is used to establish flexible testing environments. + * Falsey value is for noninteractive shell (-force flags overide user prompts) + * Truthy value is for interactive shell + */ +export const setTTYMode = (bool) => { + process.stdin.isTTY = bool +} + +/** + * Simulates a Continuous Integration environment by toggling the `CI` + * environment variable. Truthy value is + */ +export const setCI = (value) => { + process.env.CI = value +} + /** * * @param {*} param0 From fea9bb4e0408e8077f99f4cc718d7abbf2d4c66f Mon Sep 17 00:00:00 2001 From: Will Conrad Date: Tue, 22 Oct 2024 12:57:29 -0500 Subject: [PATCH 23/39] fix: prettier fix Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> --- src/commands/base-command.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/base-command.ts b/src/commands/base-command.ts index 6ee0033101c..60615af9902 100644 --- a/src/commands/base-command.ts +++ b/src/commands/base-command.ts @@ -172,7 +172,7 @@ export default class BaseCommand extends Command { * If the command is scripted (SHLVL is greater than 1 or CI/CONTINUOUS_INTEGRATION is true) then some commands * might behave differently. */ - scriptedCommand = Boolean(!process.stdin.isTTY || isCI || process.env.CI ) + scriptedCommand = Boolean(!process.stdin.isTTY || isCI || process.env.CI) /** * The workspace root if inside a mono repository. From 8c652260f87dd21aa41ad6ff051885614b81c06c Mon Sep 17 00:00:00 2001 From: Will Conrad Date: Tue, 22 Oct 2024 14:33:45 -0500 Subject: [PATCH 24/39] fix: removed console.log statements Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> --- src/commands/env/env-set.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/commands/env/env-set.ts b/src/commands/env/env-set.ts index b860fe9ea42..9c4b670c674 100644 --- a/src/commands/env/env-set.ts +++ b/src/commands/env/env-set.ts @@ -32,8 +32,6 @@ const setInEnvelope = async ({ api, context, force, key, scope, secret, siteInfo } // fetch envelope env vars - // const userData = await api.getAccount({accountId}) - // log(userData) const envelopeVariables = await api.getEnvVars({ accountId, siteId }) const contexts = context || ['all'] let scopes = scope || AVAILABLE_SCOPES From 0ef4b7b288cc6ae8e4c278ba0e8fc73cebbfc4cd Mon Sep 17 00:00:00 2001 From: Will Conrad Date: Thu, 24 Oct 2024 10:46:44 -0500 Subject: [PATCH 25/39] fix: updated prompts based on pr feedback Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> --- src/commands/env/env-set.ts | 2 -- src/utils/prompts/blob-delete-prompts.ts | 9 ++---- src/utils/prompts/blob-set-prompt.ts | 9 ++---- src/utils/prompts/env-clone-prompt.ts | 12 ++++---- src/utils/prompts/env-set-prompts.ts | 17 +++-------- src/utils/prompts/env-unset-prompts.ts | 9 +++--- src/utils/prompts/prompt-messages.ts | 26 ++++++++--------- .../commands/blobs/blobs-delete.test.ts | 22 +++++++------- .../commands/blobs/blobs-set.test.ts | 24 +++++++-------- .../commands/env/env-clone.test.ts | 29 +++++++++---------- .../integration/commands/env/env-set.test.ts | 20 ++++++------- .../commands/env/env-unset.test.ts | 20 ++++++------- 12 files changed, 88 insertions(+), 111 deletions(-) diff --git a/src/commands/env/env-set.ts b/src/commands/env/env-set.ts index 9c4b670c674..d1cf426cdea 100644 --- a/src/commands/env/env-set.ts +++ b/src/commands/env/env-set.ts @@ -1,5 +1,3 @@ -import process from 'process' - import { OptionValues } from 'commander' import { chalk, error, log, logJson } from '../../utils/command-helpers.js' diff --git a/src/utils/prompts/blob-delete-prompts.ts b/src/utils/prompts/blob-delete-prompts.ts index 42e37b8428a..cf319586208 100644 --- a/src/utils/prompts/blob-delete-prompts.ts +++ b/src/utils/prompts/blob-delete-prompts.ts @@ -4,14 +4,11 @@ import { confirmPrompt } from './confirm-prompt.js' import { destructiveCommandMessages } from './prompt-messages.js' export const promptBlobDelete = async (key: string, storeName: string): Promise => { - const { overwriteNoticeMessage } = destructiveCommandMessages - const { generateWarningMessage, overwriteConfirmationMessage } = destructiveCommandMessages.blobDelete - - const warningMessage = generateWarningMessage(key, storeName) + const warningMessage = destructiveCommandMessages.blobDelete.generateWarning(key, storeName) log() log(warningMessage) log() - log(overwriteNoticeMessage) - await confirmPrompt(overwriteConfirmationMessage) + log(destructiveCommandMessages.overwriteNotice) + await confirmPrompt(destructiveCommandMessages.blobDelete.overwriteConfirmation) } diff --git a/src/utils/prompts/blob-set-prompt.ts b/src/utils/prompts/blob-set-prompt.ts index 7d7ee58b12e..df84022db60 100644 --- a/src/utils/prompts/blob-set-prompt.ts +++ b/src/utils/prompts/blob-set-prompt.ts @@ -4,14 +4,11 @@ import { confirmPrompt } from './confirm-prompt.js' import { destructiveCommandMessages } from './prompt-messages.js' export const promptBlobSetOverwrite = async (key: string, storeName: string): Promise => { - const { overwriteNoticeMessage } = destructiveCommandMessages - const { generateWarningMessage, overwriteConfirmationMessage } = destructiveCommandMessages.blobSet - - const warningMessage = generateWarningMessage(key, storeName) + const warningMessage = destructiveCommandMessages.blobDelete.generateWarning(key, storeName) log() log(warningMessage) log() - log(overwriteNoticeMessage) - await confirmPrompt(overwriteConfirmationMessage) + log(destructiveCommandMessages.overwriteNotice) + await confirmPrompt(destructiveCommandMessages.blobDelete.overwriteConfirmation) } diff --git a/src/utils/prompts/env-clone-prompt.ts b/src/utils/prompts/env-clone-prompt.ts index b72d2c73d26..69a5719afc6 100644 --- a/src/utils/prompts/env-clone-prompt.ts +++ b/src/utils/prompts/env-clone-prompt.ts @@ -14,23 +14,21 @@ export const generateEnvVarsList = (envVarsToDelete: EnvVar[]) => envVarsToDelet * @returns {Promise} A promise that resolves when the user has confirmed the overwriting of the variables. */ export async function promptEnvCloneOverwrite(siteId: string, existingEnvVars: EnvVar[]): Promise { - const { overwriteNoticeMessage } = destructiveCommandMessages - const { generateWarningMessage, noticeEnvVarsMessage, overwriteConfirmationMessage } = - destructiveCommandMessages.envClone + const { generateWarning } = destructiveCommandMessages.envClone const existingEnvVarKeys = generateEnvVarsList(existingEnvVars) - const warningMessage = generateWarningMessage(siteId) + const warningMessage = generateWarning(siteId) log() log(warningMessage) log() - log(noticeEnvVarsMessage) + log(destructiveCommandMessages.envClone.noticeEnvVars) log() existingEnvVarKeys.forEach((envVar) => { log(envVar) }) log() - log(overwriteNoticeMessage) + log(destructiveCommandMessages.overwriteNotice) - await confirmPrompt(overwriteConfirmationMessage) + await confirmPrompt(destructiveCommandMessages.envClone.overwriteConfirmation) } diff --git a/src/utils/prompts/env-set-prompts.ts b/src/utils/prompts/env-set-prompts.ts index e256b333b4d..981867bced7 100644 --- a/src/utils/prompts/env-set-prompts.ts +++ b/src/utils/prompts/env-set-prompts.ts @@ -3,21 +3,12 @@ import { log } from '../command-helpers.js' import { confirmPrompt } from './confirm-prompt.js' import { destructiveCommandMessages } from './prompt-messages.js' -/** - * Prompts the user to confirm overwriting an existing environment variable. - * - * @param {string} existingKey - The key of the existing environment variable. - * @returns {Promise} A promise that resolves when the user confirms overwriting the variable. - */ -export const promptOverwriteEnvVariable = async (existingKey: string): Promise => { - const { overwriteNoticeMessage } = destructiveCommandMessages - const { generateWarningMessage, overwriteConfirmationMessage } = destructiveCommandMessages.envSet - - const warningMessage = generateWarningMessage(existingKey) +export const promptOverwriteEnvVariable = async (key: string): Promise => { + const warningMessage = destructiveCommandMessages.envSet.generateWarning(key) log() log(warningMessage) log() - log(overwriteNoticeMessage) - await confirmPrompt(overwriteConfirmationMessage) + log(destructiveCommandMessages.overwriteNotice) + await confirmPrompt(destructiveCommandMessages.envSet.overwriteConfirmation) } diff --git a/src/utils/prompts/env-unset-prompts.ts b/src/utils/prompts/env-unset-prompts.ts index 8128922636e..73af7886889 100644 --- a/src/utils/prompts/env-unset-prompts.ts +++ b/src/utils/prompts/env-unset-prompts.ts @@ -10,13 +10,12 @@ import { destructiveCommandMessages } from './prompt-messages.js' * @returns {Promise} A promise that resolves when the user has confirmed overwriting the variable */ export const promptOverwriteEnvVariable = async (existingKey: string): Promise => { - const { overwriteNoticeMessage } = destructiveCommandMessages - const { generateWarningMessage, overwriteConfirmationMessage } = destructiveCommandMessages.envUnset + const { generateWarning } = destructiveCommandMessages.envUnset - const warningMessage = generateWarningMessage(existingKey) + const warningMessage = generateWarning(existingKey) log(warningMessage) log() - log(overwriteNoticeMessage) - await confirmPrompt(overwriteConfirmationMessage) + log(destructiveCommandMessages.overwriteNotice) + await confirmPrompt(destructiveCommandMessages.envUnset.overwriteConfirmation) } diff --git a/src/utils/prompts/prompt-messages.ts b/src/utils/prompts/prompt-messages.ts index f8b8fcf02d0..29014136499 100644 --- a/src/utils/prompts/prompt-messages.ts +++ b/src/utils/prompts/prompt-messages.ts @@ -1,48 +1,46 @@ import { chalk } from '../command-helpers.js' export const destructiveCommandMessages = { - overwriteNoticeMessage: `${chalk.yellowBright( - 'Notice', - )}: To overwrite without this warning, you can use the --force flag.`, + overwriteNotice: `${chalk.yellowBright('Notice')}: To overwrite without this warning, you can use the --force flag.`, blobSet: { - generateWarningMessage: (key: string, storeName: string) => + generateWarning: (key: string, storeName: string) => `${chalk.redBright('Warning')}: The blob key ${chalk.cyan(key)} already exists in store ${chalk.cyan( storeName, )}!`, - overwriteConfirmationMessage: 'Do you want to proceed with overwriting this blob key existing value?', + overwriteConfirmation: 'Do you want to proceed with overwriting this blob key existing value?', }, blobDelete: { - generateWarningMessage: (key: string, storeName: string) => + generateWarning: (key: string, storeName: string) => `${chalk.redBright('Warning')}: The following blob key ${chalk.cyan(key)} will be deleted from store ${chalk.cyan( storeName, )}!`, - overwriteConfirmationMessage: 'Do you want to proceed with deleting the value at this key?', + overwriteConfirmation: 'Do you want to proceed with deleting the value at this key?', }, envSet: { - generateWarningMessage: (variableName: string) => + generateWarning: (variableName: string) => `${chalk.redBright('Warning')}: The environment variable ${chalk.bgBlueBright(variableName)} already exists!`, - overwriteConfirmationMessage: 'Do you want to overwrite it?', + overwriteConfirmation: 'Do you want to overwrite it?', }, envUnset: { - generateWarningMessage: (variableName: string) => + generateWarning: (variableName: string) => `${chalk.redBright('Warning')}: The environment variable ${chalk.bgBlueBright( variableName, )} will be removed from all contexts!`, - overwriteConfirmationMessage: 'Do you want to remove it?', + overwriteConfirmation: 'Do you want to remove it?', }, envClone: { - generateWarningMessage: (siteId: string) => + generateWarning: (siteId: string) => `${chalk.redBright( 'Warning', )}: The following environment variables are already set on the site with ID ${chalk.bgBlueBright( siteId, )}. They will be overwritten!`, - noticeEnvVarsMessage: `${chalk.yellowBright('Notice')}: The following variables will be overwritten:`, - overwriteConfirmationMessage: 'The environment variables already exist. Do you want to overwrite them?', + noticeEnvVars: `${chalk.yellowBright('Notice')}: The following variables will be overwritten:`, + overwriteConfirmation: 'The environment variables already exist. Do you want to overwrite them?', }, } diff --git a/tests/integration/commands/blobs/blobs-delete.test.ts b/tests/integration/commands/blobs/blobs-delete.test.ts index 739670c8834..d4652830161 100644 --- a/tests/integration/commands/blobs/blobs-delete.test.ts +++ b/tests/integration/commands/blobs/blobs-delete.test.ts @@ -51,10 +51,10 @@ describe('blob:delete command', () => { const storeName = 'my-store' const key = 'my-key' - const { overwriteNoticeMessage } = destructiveCommandMessages - const { generateWarningMessage, overwriteConfirmationMessage } = destructiveCommandMessages.blobDelete + const { overwriteNotice } = destructiveCommandMessages + const { generateWarning, overwriteConfirmation } = destructiveCommandMessages.blobDelete - const warningMessage = generateWarningMessage(key, storeName) + const warningMessage = generateWarning(key, storeName) const successMessage = `${chalk.greenBright('Success')}: Blob ${chalk.yellow(key)} deleted from store ${chalk.yellow( storeName, @@ -89,12 +89,12 @@ describe('blob:delete command', () => { expect(promptSpy).toHaveBeenCalledWith({ type: 'confirm', name: 'confirm', - message: expect.stringContaining(overwriteConfirmationMessage), + message: expect.stringContaining(overwriteConfirmation), default: false, }) expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).toHaveBeenCalledWith(overwriteNotice) expect(log).toHaveBeenCalledWith(successMessage) }) }) @@ -124,12 +124,12 @@ describe('blob:delete command', () => { expect(promptSpy).toHaveBeenCalledWith({ type: 'confirm', name: 'confirm', - message: expect.stringContaining(overwriteConfirmationMessage), + message: expect.stringContaining(overwriteConfirmation), default: false, }) expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).toHaveBeenCalledWith(overwriteNotice) expect(log).not.toHaveBeenCalledWith(successMessage) }) }) @@ -154,7 +154,7 @@ describe('blob:delete command', () => { expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) expect(log).toHaveBeenCalledWith(successMessage) }) }) @@ -188,7 +188,7 @@ describe('blob:delete command', () => { expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) expect(log).not.toHaveBeenCalledWith(successMessage) }) } catch (error) { @@ -229,7 +229,7 @@ describe('blob:delete command', () => { expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) expect(log).toHaveBeenCalledWith(successMessage) }) }) @@ -255,7 +255,7 @@ describe('blob:delete command', () => { expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) expect(log).toHaveBeenCalledWith(successMessage) }) }) diff --git a/tests/integration/commands/blobs/blobs-set.test.ts b/tests/integration/commands/blobs/blobs-set.test.ts index 206fbdd0f1c..4e043a843fc 100644 --- a/tests/integration/commands/blobs/blobs-set.test.ts +++ b/tests/integration/commands/blobs/blobs-set.test.ts @@ -52,10 +52,10 @@ describe('blob:set command', () => { const value = 'my-value' const newValue = 'my-new-value' - const { overwriteNoticeMessage } = destructiveCommandMessages - const { generateWarningMessage, overwriteConfirmationMessage } = destructiveCommandMessages.blobSet + const { overwriteNotice } = destructiveCommandMessages + const { generateWarning, overwriteConfirmation } = destructiveCommandMessages.blobSet - const warningMessage = generateWarningMessage(key, storeName) + const warningMessage = generateWarning(key, storeName) const successMessage = `${chalk.greenBright('Success')}: Blob ${chalk.yellow(key)} set in store ${chalk.yellow( storeName, @@ -89,7 +89,7 @@ describe('blob:set command', () => { expect(promptSpy).not.toHaveBeenCalled() expect(log).toHaveBeenCalledWith(successMessage) expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) }) }) @@ -116,13 +116,13 @@ describe('blob:set command', () => { expect(promptSpy).toHaveBeenCalledWith({ type: 'confirm', name: 'confirm', - message: expect.stringContaining(overwriteConfirmationMessage), + message: expect.stringContaining(overwriteConfirmation), default: false, }) expect(log).toHaveBeenCalledWith(successMessage) expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).toHaveBeenCalledWith(overwriteNotice) }) }) @@ -154,12 +154,12 @@ describe('blob:set command', () => { expect(promptSpy).toHaveBeenCalledWith({ type: 'confirm', name: 'confirm', - message: expect.stringContaining(overwriteConfirmationMessage), + message: expect.stringContaining(overwriteConfirmation), default: false, }) expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).toHaveBeenCalledWith(overwriteNotice) expect(log).not.toHaveBeenCalledWith(successMessage) }) }) @@ -187,7 +187,7 @@ describe('blob:set command', () => { expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) expect(log).toHaveBeenCalledWith(successMessage) }) }) @@ -216,7 +216,7 @@ describe('blob:set command', () => { expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) expect(log).not.toHaveBeenCalledWith(successMessage) }) }) @@ -256,7 +256,7 @@ describe('blob:set command', () => { expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) expect(log).toHaveBeenCalledWith(successMessage) }) }) @@ -285,7 +285,7 @@ describe('blob:set command', () => { expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) expect(log).toHaveBeenCalledWith(successMessage) }) }) diff --git a/tests/integration/commands/env/env-clone.test.ts b/tests/integration/commands/env/env-clone.test.ts index 3ae6d0a5433..17fe47f6632 100644 --- a/tests/integration/commands/env/env-clone.test.ts +++ b/tests/integration/commands/env/env-clone.test.ts @@ -22,12 +22,11 @@ describe('env:clone command', () => { const sharedEnvVars = [existingVar, existingVar] const siteIdTwo = secondSiteInfo.id - const { overwriteNoticeMessage } = destructiveCommandMessages - const { generateWarningMessage, noticeEnvVarsMessage, overwriteConfirmationMessage } = - destructiveCommandMessages.envClone + const { overwriteNotice } = destructiveCommandMessages + const { generateWarning, noticeEnvVars, overwriteConfirmation } = destructiveCommandMessages.envClone const envVarsList = generateEnvVarsList(sharedEnvVars) - const warningMessage = generateWarningMessage(siteIdTwo) + const warningMessage = generateWarning(siteIdTwo) const successMessage = `Successfully cloned environment variables from ${chalk.green('site-name')} to ${chalk.green( 'site-name-2', @@ -53,16 +52,16 @@ describe('env:clone command', () => { expect(promptSpy).toHaveBeenCalledWith({ type: 'confirm', name: 'confirm', - message: expect.stringContaining(overwriteConfirmationMessage), + message: expect.stringContaining(overwriteConfirmation), default: false, }) expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(noticeEnvVarsMessage) + expect(log).toHaveBeenCalledWith(noticeEnvVars) envVarsList.forEach((envVar) => { expect(log).toHaveBeenCalledWith(envVar) }) - expect(log).toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).toHaveBeenCalledWith(overwriteNotice) expect(log).toHaveBeenCalledWith(successMessage) }) }) @@ -84,8 +83,8 @@ describe('env:clone command', () => { envVarsList.forEach((envVar) => { expect(log).not.toHaveBeenCalledWith(envVar) }) - expect(log).not.toHaveBeenCalledWith(noticeEnvVarsMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).not.toHaveBeenCalledWith(noticeEnvVars) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) expect(log).toHaveBeenCalledWith(successMessage) }) }) @@ -109,11 +108,11 @@ describe('env:clone command', () => { expect(promptSpy).toHaveBeenCalled() expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(noticeEnvVarsMessage) + expect(log).toHaveBeenCalledWith(noticeEnvVars) envVarsList.forEach((envVar) => { expect(log).toHaveBeenCalledWith(envVar) }) - expect(log).toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).toHaveBeenCalledWith(overwriteNotice) expect(log).not.toHaveBeenCalledWith(successMessage) }) }) @@ -135,11 +134,11 @@ describe('env:clone command', () => { expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(noticeEnvVarsMessage) + expect(log).not.toHaveBeenCalledWith(noticeEnvVars) envVarsList.forEach((envVar) => { expect(log).not.toHaveBeenCalledWith(envVar) }) - expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) expect(log).toHaveBeenCalledWith(successMessageSite3) }) }) @@ -171,7 +170,7 @@ describe('env:clone command', () => { expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) expect(log).toHaveBeenCalledWith(successMessage) }) }) @@ -192,7 +191,7 @@ describe('env:clone command', () => { expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) expect(log).toHaveBeenCalledWith(successMessage) }) }) diff --git a/tests/integration/commands/env/env-set.test.ts b/tests/integration/commands/env/env-set.test.ts index 26b11f9bafb..e01f399ed0e 100644 --- a/tests/integration/commands/env/env-set.test.ts +++ b/tests/integration/commands/env/env-set.test.ts @@ -22,10 +22,10 @@ describe('env:set command', () => { // already exists as value in withMockApi const existingVar = 'EXISTING_VAR' const newEnvValue = 'value' - const { overwriteNoticeMessage } = destructiveCommandMessages - const { generateWarningMessage, overwriteConfirmationMessage } = destructiveCommandMessages.envSet + const { overwriteNotice } = destructiveCommandMessages + const { generateWarning, overwriteConfirmation } = destructiveCommandMessages.envSet - const warningMessage = generateWarningMessage(existingVar) + const warningMessage = generateWarning(existingVar) const successMessage = `Set environment variable ${chalk.yellow( `${existingVar}=${newEnvValue}`, @@ -302,12 +302,12 @@ describe('env:set command', () => { expect(promptSpy).toHaveBeenCalledWith({ type: 'confirm', name: 'confirm', - message: expect.stringContaining(overwriteConfirmationMessage), + message: expect.stringContaining(overwriteConfirmation), default: false, }) expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).toHaveBeenCalledWith(overwriteNotice) expect(log).toHaveBeenCalledWith(successMessage) }) }) @@ -326,7 +326,7 @@ describe('env:set command', () => { expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) expect(log).toHaveBeenCalledWith( `Set environment variable ${chalk.yellow(`${'NEW_ENV_VAR'}=${'NEW_VALUE'}`)} in the ${chalk.magenta( 'all', @@ -349,7 +349,7 @@ describe('env:set command', () => { expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) expect(log).toHaveBeenCalledWith(successMessage) }) }) @@ -373,7 +373,7 @@ describe('env:set command', () => { expect(promptSpy).toHaveBeenCalled() expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).toHaveBeenCalledWith(overwriteNotice) expect(log).not.toHaveBeenCalledWith(successMessage) }) }) @@ -404,7 +404,7 @@ describe('env:set command', () => { expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) expect(log).toHaveBeenCalledWith(successMessage) }) }) @@ -425,7 +425,7 @@ describe('env:set command', () => { expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) expect(log).toHaveBeenCalledWith(successMessage) }) }) diff --git a/tests/integration/commands/env/env-unset.test.ts b/tests/integration/commands/env/env-unset.test.ts index d91c9c94a2d..00b8c11a329 100644 --- a/tests/integration/commands/env/env-unset.test.ts +++ b/tests/integration/commands/env/env-unset.test.ts @@ -19,12 +19,12 @@ vi.mock('../../../../src/utils/command-helpers.js', async () => ({ })) describe('env:unset command', () => { - const { overwriteNoticeMessage } = destructiveCommandMessages - const { generateWarningMessage, overwriteConfirmationMessage } = destructiveCommandMessages.envUnset + const { overwriteNotice } = destructiveCommandMessages + const { generateWarning, overwriteConfirmation } = destructiveCommandMessages.envUnset // already exists as value in withMockApi const existingVar = 'EXISTING_VAR' - const warningMessage = generateWarningMessage(existingVar) + const warningMessage = generateWarning(existingVar) const expectedSuccessMessage = `Unset environment variable ${chalk.yellow(`${existingVar}`)} in the ${chalk.magenta( 'all', )} context` @@ -106,12 +106,12 @@ describe('env:unset command', () => { expect(promptSpy).toHaveBeenCalledWith({ type: 'confirm', name: 'confirm', - message: expect.stringContaining(overwriteConfirmationMessage), + message: expect.stringContaining(overwriteConfirmation), default: false, }) expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).toHaveBeenCalledWith(overwriteNotice) expect(log).toHaveBeenCalledWith(expectedSuccessMessage) }) }) @@ -130,7 +130,7 @@ describe('env:unset command', () => { expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) expect(log).toHaveBeenCalledWith(expectedSuccessMessage) }) }) @@ -154,7 +154,7 @@ describe('env:unset command', () => { expect(promptSpy).toHaveBeenCalled() expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).toHaveBeenCalledWith(overwriteNotice) expect(log).not.toHaveBeenCalledWith(expectedSuccessMessage) }) }) @@ -173,7 +173,7 @@ describe('env:unset command', () => { expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) expect(log).not.toHaveBeenCalledWith(expectedSuccessMessage) }) }) @@ -205,7 +205,7 @@ describe('env:unset command', () => { expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) expect(log).toHaveBeenCalledWith(expectedSuccessMessage) }) }) @@ -226,7 +226,7 @@ describe('env:unset command', () => { expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNoticeMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) expect(log).toHaveBeenCalledWith(expectedSuccessMessage) }) }) From 2ad45c2b979e3f55bc76b775ab149f43d865d84b Mon Sep 17 00:00:00 2001 From: t Date: Fri, 25 Oct 2024 14:58:03 -0400 Subject: [PATCH 26/39] feat: added force flag option to all commands scripted commands automatically given the force flag Co-authored-by: Will --- bin/run.js | 29 +++++++++++++++++++ src/commands/addons/addons.ts | 2 +- src/commands/base-command.ts | 1 + src/commands/blobs/blobs-delete.ts | 3 -- src/commands/blobs/blobs-set.ts | 3 -- src/commands/blobs/blobs.ts | 2 -- src/commands/env/env-clone.ts | 3 -- src/commands/env/env-set.ts | 5 +--- src/commands/env/env-unset.ts | 4 +-- src/commands/env/env.ts | 3 -- src/commands/init/index.ts | 2 +- src/commands/lm/lm.ts | 4 +-- src/commands/sites/sites.ts | 2 +- .../commands/addons/addons.test.js | 2 +- .../integration/commands/env/env-set.test.ts | 7 ++--- .../commands/env/env-unset.test.ts | 4 +-- .../help/__snapshots__/help.test.ts.snap | 1 + 17 files changed, 44 insertions(+), 33 deletions(-) diff --git a/bin/run.js b/bin/run.js index 0ab97550858..235fb84a969 100755 --- a/bin/run.js +++ b/bin/run.js @@ -23,8 +23,37 @@ try { const program = createMainCommand() + try { + + // Is the command run in a non-interactive shell or CI/CD environment? + const scriptedCommand = program.scriptedCommand + + // Is not the base `netlify command w/o any flags + const notNetlifyCommand = argv.length > 2 + + // Is not the base `netlify` command w/ flags + const notNetlifyCommandWithFlags = argv[2] && !(argv[2].startsWith('-')) + + // is not the `netlify help` command + const notNetlifyHelpCommand = argv[2] && !(argv[2] === 'help') + + // Is the `--force` flag not already present? + const noForceFlag = !argv.includes('--force') + + // Prevents prompts from blocking scripted commands + if ( + scriptedCommand && + notNetlifyCommand && + notNetlifyCommandWithFlags && + notNetlifyHelpCommand && + noForceFlag + ) { + argv.push("--force") + } + await program.parseAsync(argv) + program.onEnd() } catch (error_) { program.onEnd(error_) diff --git a/src/commands/addons/addons.ts b/src/commands/addons/addons.ts index 8795f4484f3..489b6d61c48 100644 --- a/src/commands/addons/addons.ts +++ b/src/commands/addons/addons.ts @@ -51,7 +51,7 @@ Add-ons are a way to extend the functionality of your Netlify site`, .description( `Remove an add-on extension to your site\nAdd-ons are a way to extend the functionality of your Netlify site`, ) - .option('-f, --force', 'delete without prompting (useful for CI)') + // .option('-f, --force', 'delete without prompting (useful for CI)') .action(async (addonName: string, options: OptionValues, command: BaseCommand) => { const { addonsDelete } = await import('./addons-delete.js') await addonsDelete(addonName, options, command) diff --git a/src/commands/base-command.ts b/src/commands/base-command.ts index 60615af9902..fbdba97f741 100644 --- a/src/commands/base-command.ts +++ b/src/commands/base-command.ts @@ -193,6 +193,7 @@ export default class BaseCommand extends Command { createCommand(name: string): BaseCommand { const base = new BaseCommand(name) // If --silent or --json flag passed disable logger + .addOption(new Option('--force', 'Force command to run. Bypasses prompts for certain destructive commands.')) .addOption(new Option('--json', 'Output return values as JSON').hideHelp(true)) .addOption(new Option('--silent', 'Silence CLI output').hideHelp(true)) .addOption(new Option('--cwd ').hideHelp(true)) diff --git a/src/commands/blobs/blobs-delete.ts b/src/commands/blobs/blobs-delete.ts index c15d1b13965..b037f923012 100644 --- a/src/commands/blobs/blobs-delete.ts +++ b/src/commands/blobs/blobs-delete.ts @@ -8,9 +8,6 @@ import { promptBlobDelete } from '../../utils/prompts/blob-delete-prompts.js' */ export const blobsDelete = async (storeName: string, key: string, _options: Record, command: any) => { // Prevents prompts from blocking scripted commands - if (command.scriptedCommand) { - _options.force = true - } const { api, siteInfo } = command.netlify const { force } = _options diff --git a/src/commands/blobs/blobs-set.ts b/src/commands/blobs/blobs-set.ts index 326b512238f..8f6c5ae2cd6 100644 --- a/src/commands/blobs/blobs-set.ts +++ b/src/commands/blobs/blobs-set.ts @@ -21,9 +21,6 @@ export const blobsSet = async ( command: BaseCommand, ) => { // Prevents prompts from blocking scripted commands - if (command.scriptedCommand) { - options.force = true - } const { api, siteInfo } = command.netlify const { force, input } = options diff --git a/src/commands/blobs/blobs.ts b/src/commands/blobs/blobs.ts index 260ccc68b6c..f3a3a1ddeff 100644 --- a/src/commands/blobs/blobs.ts +++ b/src/commands/blobs/blobs.ts @@ -19,7 +19,6 @@ export const createBlobsCommand = (program: BaseCommand) => { .description(`Deletes an object with a given key, if it exists, from a Netlify Blobs store`) .argument('', 'Name of the store') .argument('', 'Object key') - .option('-f, --force', 'Force the operation to proceed without confirmation or warnings') .alias('blob:delete') .hook('preAction', requiresSiteInfo) .action(async (storeName: string, key: string, _options: OptionValues, command: BaseCommand) => { @@ -71,7 +70,6 @@ export const createBlobsCommand = (program: BaseCommand) => { .argument('', 'Object key') .argument('[value...]', 'Object value') .option('-i, --input ', 'Defines the filesystem path where the blob data should be read from') - .option('-f, --force', 'Force the operation to proceed without confirmation or warnings') .alias('blob:set') .hook('preAction', requiresSiteInfo) diff --git a/src/commands/env/env-clone.ts b/src/commands/env/env-clone.ts index 9a7184c2861..aed02756b1f 100644 --- a/src/commands/env/env-clone.ts +++ b/src/commands/env/env-clone.ts @@ -57,9 +57,6 @@ const cloneEnvVars = async ({ api, force, siteFrom, siteTo }): Promise export const envClone = async (options: OptionValues, command: BaseCommand) => { // Prevents prompts from blocking scripted commands - if (command.scriptedCommand) { - options.force = true - } const { api, site } = command.netlify const { force } = options diff --git a/src/commands/env/env-set.ts b/src/commands/env/env-set.ts index d1cf426cdea..77197aa1989 100644 --- a/src/commands/env/env-set.ts +++ b/src/commands/env/env-set.ts @@ -48,7 +48,7 @@ const setInEnvelope = async ({ api, context, force, key, scope, secret, siteInfo // @ts-expect-error TS(7006) FIXME: Parameter 'envVar' implicitly has an 'any' type. const existing = envelopeVariables.find((envVar) => envVar.key === key) - + console.log('force w/n set', force) // Checks if --force is passed and if it is an existing variaible, then we need to prompt the user if (Boolean(force) === false && existing) { await promptOverwriteEnvVariable(key) @@ -115,9 +115,6 @@ const setInEnvelope = async ({ api, context, force, key, scope, secret, siteInfo export const envSet = async (key: string, value: string, options: OptionValues, command: BaseCommand) => { // Prevents prompts from ci and non ineractive shells - if (command.scriptedCommand) { - options.force = true - } const { context, force, scope, secret } = options const { api, cachedConfig, site } = command.netlify diff --git a/src/commands/env/env-unset.ts b/src/commands/env/env-unset.ts index 3a7c756bdf7..cf916bf21c2 100644 --- a/src/commands/env/env-unset.ts +++ b/src/commands/env/env-unset.ts @@ -10,6 +10,7 @@ import BaseCommand from '../base-command.js' */ // @ts-expect-error TS(7031) FIXME: Binding element 'api' implicitly has an 'any' type... Remove this comment to see the full error message const unsetInEnvelope = async ({ api, context, force, key, siteInfo }) => { + console.log('force w/n unset', force) const accountId = siteInfo.account_slug const siteId = siteInfo.id // fetch envelope env vars @@ -69,9 +70,6 @@ const unsetInEnvelope = async ({ api, context, force, key, siteInfo }) => { export const envUnset = async (key: string, options: OptionValues, command: BaseCommand) => { // Prevents prompts from blocking scripted commands - if (command.scriptedCommand) { - options.force = true - } const { context, force } = options const { api, cachedConfig, site } = command.netlify diff --git a/src/commands/env/env.ts b/src/commands/env/env.ts index af14d6ca122..9016b4b3df2 100644 --- a/src/commands/env/env.ts +++ b/src/commands/env/env.ts @@ -103,7 +103,6 @@ export const createEnvCommand = (program: BaseCommand) => { 'runtime', ]), ) - .option('-f, --force', 'Force the operation to proceed without confirmation or warnings') .option('--secret', 'Indicate whether the environment variable value can be read again.') .description('Set value of environment variable') .addExamples([ @@ -131,7 +130,6 @@ export const createEnvCommand = (program: BaseCommand) => { // @ts-expect-error TS(7006) FIXME: Parameter 'context' implicitly has an 'any' type. (context, previous = []) => [...previous, normalizeContext(context)], ) - .option('-f, --force', 'Force the operation to proceed without confirmation or warnings') .addExamples([ 'netlify env:unset VAR_NAME # unset in all contexts', 'netlify env:unset VAR_NAME --context production', @@ -147,7 +145,6 @@ export const createEnvCommand = (program: BaseCommand) => { .command('env:clone') .alias('env:migrate') .option('-f, --from ', 'Site ID (From)') - .option('--force', 'Force the operation to proceed without confirmation or warnings') .requiredOption('-t, --to ', 'Site ID (To)') .description(`Clone environment variables from one site to another`) .addExamples(['netlify env:clone --to ', 'netlify env:clone --to --from ']) diff --git a/src/commands/init/index.ts b/src/commands/init/index.ts index 3c22c422548..a45ea4b0e90 100644 --- a/src/commands/init/index.ts +++ b/src/commands/init/index.ts @@ -9,7 +9,7 @@ export const createInitCommand = (program: BaseCommand) => 'Configure continuous deployment for a new or existing site. To create a new site without continuous deployment, use `netlify sites:create`', ) .option('-m, --manual', 'Manually configure a git remote for CI') - .option('--force', 'Reinitialize CI hooks if the linked site is already configured to use CI') + // .option('--force', 'Reinitialize CI hooks if the linked site is already configured to use CI') .addOption( new Option( '--gitRemoteName ', diff --git a/src/commands/lm/lm.ts b/src/commands/lm/lm.ts index a11c08db22e..245618e4c04 100644 --- a/src/commands/lm/lm.ts +++ b/src/commands/lm/lm.ts @@ -23,7 +23,7 @@ export const createLmCommand = (program: BaseCommand) => { It installs the required credentials helper for Git, and configures your Git environment with the right credentials.`, ) - .option('-f, --force', 'Force the credentials helper installation') + // .option('-f, --force', 'Force the credentials helper installation') .action(async (options: OptionValues) => { const { lmInstall } = await import('./lm-install.js') await lmInstall(options) @@ -33,7 +33,7 @@ and configures your Git environment with the right credentials.`, .command('lm:setup', { hidden: true }) .description('Configures your site to use Netlify Large Media') .option('-s, --skip-install', 'Skip the credentials helper installation check') - .option('-f, --force-install', 'Force the credentials helper installation') + // .option('-f, --force-install', 'Force the credentials helper installation') .addHelpText('after', 'It runs the install command if you have not installed the dependencies yet.') .action(async (options: OptionValues, command: BaseCommand) => { const { lmSetup } = await import('./lm-setup.js') diff --git a/src/commands/sites/sites.ts b/src/commands/sites/sites.ts index 908c8940fc3..b555b8acaa5 100644 --- a/src/commands/sites/sites.ts +++ b/src/commands/sites/sites.ts @@ -81,7 +81,7 @@ export const createSitesCommand = (program: BaseCommand) => { .command('sites:delete') .description('Delete a site\nThis command will permanently delete the site on Netlify. Use with caution.') .argument('', 'Site ID to delete.') - .option('-f, --force', 'delete without prompting (useful for CI)') + // .option('-f, --force', 'delete without prompting (useful for CI)') .addExamples(['netlify sites:delete 1234-3262-1211']) .action(async (siteId: string, options: OptionValues, command: BaseCommand) => { const { sitesDelete } = await import('./sites-delete.js') diff --git a/tests/integration/commands/addons/addons.test.js b/tests/integration/commands/addons/addons.test.js index ff9f17b31e0..ad8c7ee2f80 100644 --- a/tests/integration/commands/addons/addons.test.js +++ b/tests/integration/commands/addons/addons.test.js @@ -150,7 +150,7 @@ describe.concurrent('command-addons', () => { ] await withMockApi(deleteRoutes, async ({ apiUrl }) => { - const cliResponse = await callCli(['addons:delete', 'demo', '-f'], getCLIOptions({ builder, apiUrl })) + const cliResponse = await callCli(['addons:delete', 'demo', '--force'], getCLIOptions({ builder, apiUrl })) t.expect(cliResponse.includes('Addon "demo" deleted')).toBe(true) }) }) diff --git a/tests/integration/commands/env/env-set.test.ts b/tests/integration/commands/env/env-set.test.ts index e01f399ed0e..7face708a79 100644 --- a/tests/integration/commands/env/env-set.test.ts +++ b/tests/integration/commands/env/env-set.test.ts @@ -335,7 +335,7 @@ describe('env:set command', () => { }) }) - test('should skip warnings and prompts if -f flag is passed', async () => { + test('should skip warnings and prompts if --force flag is passed', async () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) @@ -344,7 +344,7 @@ describe('env:set command', () => { const promptSpy = vi.spyOn(inquirer, 'prompt') - await program.parseAsync(['', '', 'env:set', existingVar, newEnvValue, '-f']) + await program.parseAsync(['', '', 'env:set', existingVar, newEnvValue, '--force']) expect(promptSpy).not.toHaveBeenCalled() @@ -410,7 +410,7 @@ describe('env:set command', () => { }) test('should not show prompt in a ci/cd enviroment', async () => { - setCI(true) + setCI('true') await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) @@ -419,7 +419,6 @@ describe('env:set command', () => { createEnvCommand(program) const promptSpy = vi.spyOn(inquirer, 'prompt') - await program.parseAsync(['', '', 'env:set', existingVar, newEnvValue]) expect(promptSpy).not.toHaveBeenCalled() diff --git a/tests/integration/commands/env/env-unset.test.ts b/tests/integration/commands/env/env-unset.test.ts index 00b8c11a329..bae3e353cc1 100644 --- a/tests/integration/commands/env/env-unset.test.ts +++ b/tests/integration/commands/env/env-unset.test.ts @@ -116,7 +116,7 @@ describe('env:unset command', () => { }) }) - test('should skip warnings and prompts if -f flag is passed', async () => { + test('should skip warnings and prompts if --force flag is passed', async () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) @@ -125,7 +125,7 @@ describe('env:unset command', () => { const promptSpy = vi.spyOn(inquirer, 'prompt') - await program.parseAsync(['', '', 'env:unset', existingVar, '-f']) + await program.parseAsync(['', '', 'env:unset', existingVar, '--force']) expect(promptSpy).not.toHaveBeenCalled() diff --git a/tests/integration/commands/help/__snapshots__/help.test.ts.snap b/tests/integration/commands/help/__snapshots__/help.test.ts.snap index 57c14e623ca..d095108bbfa 100644 --- a/tests/integration/commands/help/__snapshots__/help.test.ts.snap +++ b/tests/integration/commands/help/__snapshots__/help.test.ts.snap @@ -46,6 +46,7 @@ USAGE OPTIONS -h, --help display help for command --debug Print debugging information + --force Force command to run. Bypasses prompts for certain destructive commands. DESCRIPTION Run this command to see instructions for your shell. From 4e4260914274d0aff1899b422bbc213043a7b91b Mon Sep 17 00:00:00 2001 From: t Date: Mon, 28 Oct 2024 12:18:06 -0400 Subject: [PATCH 27/39] fix: started updating tests to work with higher level --force flag for scritped commands Co-authored-by: Will --- bin/run.js | 24 +-------------- src/commands/env/env-set.ts | 1 - src/commands/env/env-unset.ts | 1 - src/utils/scriptedCommands.ts | 29 +++++++++++++++++++ .../commands/env/env-unset.test.ts | 9 ++++-- 5 files changed, 37 insertions(+), 27 deletions(-) create mode 100644 src/utils/scriptedCommands.ts diff --git a/bin/run.js b/bin/run.js index 235fb84a969..7d2cb098062 100755 --- a/bin/run.js +++ b/bin/run.js @@ -25,30 +25,8 @@ const program = createMainCommand() try { - - // Is the command run in a non-interactive shell or CI/CD environment? - const scriptedCommand = program.scriptedCommand - - // Is not the base `netlify command w/o any flags - const notNetlifyCommand = argv.length > 2 - - // Is not the base `netlify` command w/ flags - const notNetlifyCommandWithFlags = argv[2] && !(argv[2].startsWith('-')) - - // is not the `netlify help` command - const notNetlifyHelpCommand = argv[2] && !(argv[2] === 'help') - - // Is the `--force` flag not already present? - const noForceFlag = !argv.includes('--force') - // Prevents prompts from blocking scripted commands - if ( - scriptedCommand && - notNetlifyCommand && - notNetlifyCommandWithFlags && - notNetlifyHelpCommand && - noForceFlag - ) { + if (scriptedCommand(argv)) { argv.push("--force") } diff --git a/src/commands/env/env-set.ts b/src/commands/env/env-set.ts index 77197aa1989..9aad3ba338f 100644 --- a/src/commands/env/env-set.ts +++ b/src/commands/env/env-set.ts @@ -48,7 +48,6 @@ const setInEnvelope = async ({ api, context, force, key, scope, secret, siteInfo // @ts-expect-error TS(7006) FIXME: Parameter 'envVar' implicitly has an 'any' type. const existing = envelopeVariables.find((envVar) => envVar.key === key) - console.log('force w/n set', force) // Checks if --force is passed and if it is an existing variaible, then we need to prompt the user if (Boolean(force) === false && existing) { await promptOverwriteEnvVariable(key) diff --git a/src/commands/env/env-unset.ts b/src/commands/env/env-unset.ts index cf916bf21c2..a1695ecf751 100644 --- a/src/commands/env/env-unset.ts +++ b/src/commands/env/env-unset.ts @@ -10,7 +10,6 @@ import BaseCommand from '../base-command.js' */ // @ts-expect-error TS(7031) FIXME: Binding element 'api' implicitly has an 'any' type... Remove this comment to see the full error message const unsetInEnvelope = async ({ api, context, force, key, siteInfo }) => { - console.log('force w/n unset', force) const accountId = siteInfo.account_slug const siteId = siteInfo.id // fetch envelope env vars diff --git a/src/utils/scriptedCommands.ts b/src/utils/scriptedCommands.ts new file mode 100644 index 00000000000..6853746bf60 --- /dev/null +++ b/src/utils/scriptedCommands.ts @@ -0,0 +1,29 @@ +import process from 'process' +import { isCI } from 'ci-info' + +export const scriptedCommand = (argv: string[]): boolean => { + // Is the command run in a non-interactive shell or CI/CD environment? + const scriptedCommand = Boolean(!process.stdin.isTTY || isCI || process.env.CI) + + // Is not the base `netlify command w/o any flags + const notNetlifyCommand = argv.length > 2 + + // Is not the base `netlify` command w/ flags + const notNetlifyCommandWithFlags = argv[2] && !(argv[2].startsWith('-')) + + // is not the `netlify help` command + const notNetlifyHelpCommand = argv[2] && !(argv[2] === 'help') + + // Is the `--force` flag not already present? + const noForceFlag = !argv.includes('--force') + + // Prevents prompts from blocking scripted commands + return Boolean( + scriptedCommand && + notNetlifyCommand && + notNetlifyCommandWithFlags && + notNetlifyHelpCommand && + noForceFlag + ) + +} diff --git a/tests/integration/commands/env/env-unset.test.ts b/tests/integration/commands/env/env-unset.test.ts index bae3e353cc1..d350a6b82a6 100644 --- a/tests/integration/commands/env/env-unset.test.ts +++ b/tests/integration/commands/env/env-unset.test.ts @@ -10,6 +10,7 @@ import { log } from '../../../../src/utils/command-helpers.js' import { destructiveCommandMessages } from '../../../../src/utils/prompts/prompt-messages.js' import { FixtureTestContext, setupFixtureTests } from '../../utils/fixture.js' import { getEnvironmentVariables, withMockApi, setTTYMode, setCI } from '../../utils/mock-api.js' +import { scriptedCommand } from '../../../../src/utils/scriptedCommands.js' import { routes } from './api-routes.js' @@ -190,7 +191,6 @@ describe('env:unset command', () => { }) test('prompts should not show in an non-interactive shell', async () => { - setTTYMode(false) await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) @@ -198,9 +198,14 @@ describe('env:unset command', () => { const program = new BaseCommand('netlify') createEnvCommand(program) + const mockArgv = ['', '', 'env:unset', existingVar] + if (scriptedCommand(mockArgv)) { + mockArgv.push('--force') + } + const promptSpy = vi.spyOn(inquirer, 'prompt') - await program.parseAsync(['', '', 'env:unset', existingVar]) + await program.parseAsync(mockArgv) expect(promptSpy).not.toHaveBeenCalled() From 486d457965685d5c32c02b14c13d3a8d152f218c Mon Sep 17 00:00:00 2001 From: Will Date: Mon, 28 Oct 2024 14:55:01 -0500 Subject: [PATCH 28/39] feat: refactored tests to use mockProgram Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> --- bin/run.js | 10 +++-- docs/commands/api.md | 1 + docs/commands/blobs.md | 7 +++- docs/commands/build.md | 3 +- docs/commands/completion.md | 2 + docs/commands/deploy.md | 5 ++- docs/commands/dev.md | 4 +- docs/commands/env.md | 14 ++++--- docs/commands/functions.md | 8 +++- docs/commands/init.md | 2 +- docs/commands/integration.md | 4 +- docs/commands/link.md | 1 + docs/commands/login.md | 1 + docs/commands/logs.md | 3 ++ docs/commands/open.md | 3 ++ docs/commands/recipes.md | 2 + docs/commands/serve.md | 1 + docs/commands/sites.md | 10 +++-- docs/commands/status.md | 2 + docs/commands/switch.md | 1 + docs/commands/unlink.md | 1 + docs/commands/watch.md | 1 + src/utils/prompts/blob-set-prompt.ts | 4 +- ...riptedCommands.ts => scripted-commands.ts} | 17 ++++---- .../commands/blobs/blobs-delete.test.ts | 34 ++++----------- .../commands/blobs/blobs-set.test.ts | 39 ++++------------- .../commands/env/env-clone.test.ts | 33 ++++----------- .../integration/commands/env/env-set.test.ts | 35 ++++------------ .../commands/env/env-unset.test.ts | 42 ++++--------------- tests/integration/utils/mock-program.ts | 15 +++++++ tests/integration/utils/snapshots.js | 4 ++ 31 files changed, 132 insertions(+), 177 deletions(-) rename src/utils/{scriptedCommands.ts => scripted-commands.ts} (58%) create mode 100644 tests/integration/utils/mock-program.ts diff --git a/bin/run.js b/bin/run.js index 7d2cb098062..d4e8bde0e22 100755 --- a/bin/run.js +++ b/bin/run.js @@ -6,6 +6,7 @@ import updateNotifier from 'update-notifier' import { createMainCommand } from '../dist/commands/index.js' import { error } from '../dist/utils/command-helpers.js' import getPackageJson from '../dist/utils/get-package-json.js' +import { injectForceFlagIfScripted } from '../dist/utils/scripted-commands.js' // 12 hours const UPDATE_CHECK_INTERVAL = 432e5 @@ -23,12 +24,13 @@ try { const program = createMainCommand() - try { - // Prevents prompts from blocking scripted commands - if (scriptedCommand(argv)) { - argv.push("--force") + const isValidCommand = program.commands.some((cmd) => cmd.name() === argv[2]) + + if (isValidCommand) { + injectForceFlagIfScripted(argv) } + // inject the force flag if the command is a non-interactive shell or Ci enviroment await program.parseAsync(argv) diff --git a/docs/commands/api.md b/docs/commands/api.md index aff109929bb..e87f5de6e82 100644 --- a/docs/commands/api.md +++ b/docs/commands/api.md @@ -25,6 +25,7 @@ netlify api - `data` (*string*) - Data to use - `list` (*boolean*) - List out available API methods - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. **Examples** diff --git a/docs/commands/blobs.md b/docs/commands/blobs.md index c7359cb8ca0..681dedbcb4b 100644 --- a/docs/commands/blobs.md +++ b/docs/commands/blobs.md @@ -18,6 +18,7 @@ netlify blobs - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. | Subcommand | description | |:--------------------------- |:-----| @@ -57,8 +58,8 @@ netlify blobs:delete **Flags** - `filter` (*string*) - For monorepos, specify the name of the application to run the command in -- `force` (*boolean*) - Force the operation to proceed without confirmation or warnings - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. --- ## `blobs:get` @@ -81,6 +82,7 @@ netlify blobs:get - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `output` (*string*) - Defines the filesystem path where the blob data should be persisted - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. --- ## `blobs:list` @@ -104,6 +106,7 @@ netlify blobs:list - `json` (*boolean*) - Output list contents as JSON - `prefix` (*string*) - A string for filtering down the entries; when specified, only the entries whose key starts with that prefix are returned - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. --- ## `blobs:set` @@ -125,9 +128,9 @@ netlify blobs:set **Flags** - `filter` (*string*) - For monorepos, specify the name of the application to run the command in -- `force` (*boolean*) - Force the operation to proceed without confirmation or warnings - `input` (*string*) - Defines the filesystem path where the blob data should be read from - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. --- diff --git a/docs/commands/build.md b/docs/commands/build.md index 525e50693cf..ba15ab8b074 100644 --- a/docs/commands/build.md +++ b/docs/commands/build.md @@ -18,8 +18,9 @@ netlify build - `context` (*string*) - Specify a build context or branch (contexts: "production", "deploy-preview", "branch-deploy", "dev") - `dry` (*boolean*) - Dry run: show instructions without running them - `filter` (*string*) - For monorepos, specify the name of the application to run the command in -- `offline` (*boolean*) - disables any features that require network access - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. +- `offline` (*boolean*) - disables any features that require network access **Examples** diff --git a/docs/commands/completion.md b/docs/commands/completion.md index 37be1a6dedf..010ef5f4264 100644 --- a/docs/commands/completion.md +++ b/docs/commands/completion.md @@ -18,6 +18,7 @@ netlify completion **Flags** - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. | Subcommand | description | |:--------------------------- |:-----| @@ -45,6 +46,7 @@ netlify completion:install - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. --- diff --git a/docs/commands/deploy.md b/docs/commands/deploy.md index 85a23d2c327..deb8134ab49 100644 --- a/docs/commands/deploy.md +++ b/docs/commands/deploy.md @@ -92,14 +92,15 @@ netlify deploy - `functions` (*string*) - Specify a functions folder to deploy - `json` (*boolean*) - Output deployment data as JSON - `message` (*string*) - A short message to include in the deploy log +- `prod-if-unlocked` (*boolean*) - Deploy to production if unlocked, create a draft otherwise +- `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. - `open` (*boolean*) - Open site after deploy - `prod` (*boolean*) - Deploy to production -- `prod-if-unlocked` (*boolean*) - Deploy to production if unlocked, create a draft otherwise - `site` (*string*) - A site name or ID to deploy to - `skip-functions-cache` (*boolean*) - Ignore any functions created as part of a previous `build` or `deploy` commands, forcing them to be bundled again as part of the deployment - `timeout` (*string*) - Timeout to wait for deployment to finish - `trigger` (*boolean*) - Trigger a new build of your site on Netlify without uploading local files -- `debug` (*boolean*) - Print debugging information **Examples** diff --git a/docs/commands/dev.md b/docs/commands/dev.md index 8bc9e5d5aa8..72f4222803d 100644 --- a/docs/commands/dev.md +++ b/docs/commands/dev.md @@ -30,10 +30,11 @@ netlify dev - `geo` (*cache | mock | update*) - force geolocation data to be updated, use cached data from the last 24h if found, or use a mock location - `live` (*string*) - start a public live session; optionally, supply a subdomain to generate a custom URL - `no-open` (*boolean*) - disables the automatic opening of a browser window +- `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. - `offline` (*boolean*) - disables any features that require network access - `port` (*string*) - port of netlify dev - `target-port` (*string*) - port of target app server -- `debug` (*boolean*) - Print debugging information | Subcommand | description | |:--------------------------- |:-----| @@ -74,6 +75,7 @@ netlify dev:exec - `context` (*string*) - Specify a deploy context or branch for environment variables (contexts: "production", "deploy-preview", "branch-deploy", "dev") - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. **Examples** diff --git a/docs/commands/env.md b/docs/commands/env.md index 1911d923714..84aecbce1a3 100644 --- a/docs/commands/env.md +++ b/docs/commands/env.md @@ -18,6 +18,7 @@ netlify env - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. | Subcommand | description | |:--------------------------- |:-----| @@ -54,10 +55,10 @@ netlify env:clone **Flags** - `filter` (*string*) - For monorepos, specify the name of the application to run the command in -- `force` (*boolean*) - Force the operation to proceed without confirmation or warnings - `from` (*string*) - Site ID (From) - `to` (*string*) - Site ID (To) - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. **Examples** @@ -87,6 +88,7 @@ netlify env:get - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `scope` (*builds | functions | post-processing | runtime | any*) - Specify a scope - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. **Examples** @@ -117,6 +119,7 @@ netlify env:import - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `replace-existing` (*boolean*) - Replace all existing variables instead of merging them with the current ones - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. --- ## `env:list` @@ -134,9 +137,10 @@ netlify env:list - `context` (*string*) - Specify a deploy context or branch (contexts: "production", "deploy-preview", "branch-deploy", "dev") - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `json` (*boolean*) - Output environment variables as JSON -- `plain` (*boolean*) - Output environment variables as plaintext - `scope` (*builds | functions | post-processing | runtime | any*) - Specify a scope - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. +- `plain` (*boolean*) - Output environment variables as plaintext **Examples** @@ -168,10 +172,10 @@ netlify env:set - `context` (*string*) - Specify a deploy context or branch (contexts: "production", "deploy-preview", "branch-deploy", "dev") (default: all contexts) - `filter` (*string*) - For monorepos, specify the name of the application to run the command in -- `force` (*boolean*) - Force the operation to proceed without confirmation or warnings - `scope` (*builds | functions | post-processing | runtime*) - Specify a scope (default: all scopes) -- `secret` (*boolean*) - Indicate whether the environment variable value can be read again. - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. +- `secret` (*boolean*) - Indicate whether the environment variable value can be read again. **Examples** @@ -204,8 +208,8 @@ netlify env:unset - `context` (*string*) - Specify a deploy context or branch (contexts: "production", "deploy-preview", "branch-deploy", "dev") (default: all contexts) - `filter` (*string*) - For monorepos, specify the name of the application to run the command in -- `force` (*boolean*) - Force the operation to proceed without confirmation or warnings - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. **Examples** diff --git a/docs/commands/functions.md b/docs/commands/functions.md index 49559defa45..1efddb1b5af 100644 --- a/docs/commands/functions.md +++ b/docs/commands/functions.md @@ -19,6 +19,7 @@ netlify functions - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. | Subcommand | description | |:--------------------------- |:-----| @@ -53,6 +54,7 @@ netlify functions:build - `functions` (*string*) - Specify a functions directory to build to - `src` (*string*) - Specify the source directory for the functions - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. --- ## `functions:create` @@ -76,6 +78,7 @@ netlify functions:create - `name` (*string*) - function name - `url` (*string*) - pull template from URL - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. **Examples** @@ -108,9 +111,10 @@ netlify functions:invoke - `name` (*string*) - function name to invoke - `no-identity` (*boolean*) - simulate Netlify Identity authentication JWT. pass --no-identity to affirm unauthenticated request - `payload` (*string*) - Supply POST payload in stringified json, or a path to a json file +- `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. - `port` (*string*) - Port where netlify dev is accessible. e.g. 8888 - `querystring` (*string*) - Querystring to add to your function invocation -- `debug` (*boolean*) - Print debugging information **Examples** @@ -145,6 +149,7 @@ netlify functions:list - `functions` (*string*) - Specify a functions directory to list - `json` (*boolean*) - Output function data as JSON - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. --- ## `functions:serve` @@ -164,6 +169,7 @@ netlify functions:serve - `offline` (*boolean*) - disables any features that require network access - `port` (*string*) - Specify a port for the functions server - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. --- diff --git a/docs/commands/init.md b/docs/commands/init.md index 0f5bbf104c1..cab5c90deeb 100644 --- a/docs/commands/init.md +++ b/docs/commands/init.md @@ -17,10 +17,10 @@ netlify init **Flags** - `filter` (*string*) - For monorepos, specify the name of the application to run the command in -- `force` (*boolean*) - Reinitialize CI hooks if the linked site is already configured to use CI - `git-remote-name` (*string*) - Name of Git remote to use. e.g. "origin" - `manual` (*boolean*) - Manually configure a git remote for CI - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. diff --git a/docs/commands/integration.md b/docs/commands/integration.md index b0aa1c586c8..4dbb11af41f 100644 --- a/docs/commands/integration.md +++ b/docs/commands/integration.md @@ -18,6 +18,7 @@ netlify integration - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. | Subcommand | description | |:--------------------------- |:-----| @@ -41,8 +42,9 @@ netlify integration:deploy - `build` (*boolean*) - Build the integration - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `prod` (*boolean*) - Deploy to production -- `site` (*string*) - A site name or ID to deploy to - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. +- `site` (*string*) - A site name or ID to deploy to --- diff --git a/docs/commands/link.md b/docs/commands/link.md index 4a9cface348..4181dcab91c 100644 --- a/docs/commands/link.md +++ b/docs/commands/link.md @@ -21,6 +21,7 @@ netlify link - `id` (*string*) - ID of site to link to - `name` (*string*) - Name of site to link to - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. **Examples** diff --git a/docs/commands/login.md b/docs/commands/login.md index d2fb223536f..e6ea084dd8b 100644 --- a/docs/commands/login.md +++ b/docs/commands/login.md @@ -19,6 +19,7 @@ netlify login - `new` (*boolean*) - Login to new Netlify account - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. diff --git a/docs/commands/logs.md b/docs/commands/logs.md index 64303d35909..1ed111de521 100644 --- a/docs/commands/logs.md +++ b/docs/commands/logs.md @@ -17,6 +17,7 @@ netlify logs - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. | Subcommand | description | |:--------------------------- |:-----| @@ -47,6 +48,7 @@ netlify logs:deploy - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. --- ## `logs:function` @@ -68,6 +70,7 @@ netlify logs:function - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `level` (*string*) - Log levels to stream. Choices are: trace, debug, info, warn, error, fatal - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. **Examples** diff --git a/docs/commands/open.md b/docs/commands/open.md index ea2dbc5b66c..a3e02053ec4 100644 --- a/docs/commands/open.md +++ b/docs/commands/open.md @@ -19,6 +19,7 @@ netlify open - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `site` (*boolean*) - Open site - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. | Subcommand | description | |:--------------------------- |:-----| @@ -50,6 +51,7 @@ netlify open:admin - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. **Examples** @@ -72,6 +74,7 @@ netlify open:site - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. **Examples** diff --git a/docs/commands/recipes.md b/docs/commands/recipes.md index 7a7b6999b35..dae88b6adac 100644 --- a/docs/commands/recipes.md +++ b/docs/commands/recipes.md @@ -21,6 +21,7 @@ netlify recipes - `name` (*string*) - recipe name to use - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. | Subcommand | description | |:--------------------------- |:-----| @@ -49,6 +50,7 @@ netlify recipes:list - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. **Examples** diff --git a/docs/commands/serve.md b/docs/commands/serve.md index f9f35919ad7..50dd554a588 100644 --- a/docs/commands/serve.md +++ b/docs/commands/serve.md @@ -26,6 +26,7 @@ netlify serve - `offline` (*boolean*) - disables any features that require network access - `port` (*string*) - port of netlify dev - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. **Examples** diff --git a/docs/commands/sites.md b/docs/commands/sites.md index a19222ef834..55a88d31671 100644 --- a/docs/commands/sites.md +++ b/docs/commands/sites.md @@ -19,6 +19,7 @@ netlify sites - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. | Subcommand | description | |:--------------------------- |:-----| @@ -54,8 +55,9 @@ netlify sites:create - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `manual` (*boolean*) - force manual CI setup. Used --with-ci flag - `name` (*string*) - name of site -- `with-ci` (*boolean*) - initialize CI hooks during site creation - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. +- `with-ci` (*boolean*) - initialize CI hooks during site creation --- ## `sites:create-template` @@ -79,8 +81,9 @@ netlify sites:create-template - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `name` (*string*) - name of site - `url` (*string*) - template url -- `with-ci` (*boolean*) - initialize CI hooks during site creation - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. +- `with-ci` (*boolean*) - initialize CI hooks during site creation **Examples** @@ -109,8 +112,8 @@ netlify sites:delete **Flags** - `filter` (*string*) - For monorepos, specify the name of the application to run the command in -- `force` (*boolean*) - delete without prompting (useful for CI) - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. **Examples** @@ -134,6 +137,7 @@ netlify sites:list - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `json` (*boolean*) - Output site data as JSON - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. --- diff --git a/docs/commands/status.md b/docs/commands/status.md index 627e40af1da..697242a4ef5 100644 --- a/docs/commands/status.md +++ b/docs/commands/status.md @@ -18,6 +18,7 @@ netlify status - `verbose` (*boolean*) - Output system info - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. | Subcommand | description | |:--------------------------- |:-----| @@ -39,6 +40,7 @@ netlify status:hooks - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. --- diff --git a/docs/commands/switch.md b/docs/commands/switch.md index 38166dbd6a1..1f61c1dd9b2 100644 --- a/docs/commands/switch.md +++ b/docs/commands/switch.md @@ -17,6 +17,7 @@ netlify switch **Flags** - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. diff --git a/docs/commands/unlink.md b/docs/commands/unlink.md index c0f48f04539..44d81b730a2 100644 --- a/docs/commands/unlink.md +++ b/docs/commands/unlink.md @@ -18,6 +18,7 @@ netlify unlink - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. diff --git a/docs/commands/watch.md b/docs/commands/watch.md index 2d08bac6676..f3610f6e2fc 100644 --- a/docs/commands/watch.md +++ b/docs/commands/watch.md @@ -18,6 +18,7 @@ netlify watch - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information +- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. **Examples** diff --git a/src/utils/prompts/blob-set-prompt.ts b/src/utils/prompts/blob-set-prompt.ts index df84022db60..59ad163dfbb 100644 --- a/src/utils/prompts/blob-set-prompt.ts +++ b/src/utils/prompts/blob-set-prompt.ts @@ -4,11 +4,11 @@ import { confirmPrompt } from './confirm-prompt.js' import { destructiveCommandMessages } from './prompt-messages.js' export const promptBlobSetOverwrite = async (key: string, storeName: string): Promise => { - const warningMessage = destructiveCommandMessages.blobDelete.generateWarning(key, storeName) + const warningMessage = destructiveCommandMessages.blobSet.generateWarning(key, storeName) log() log(warningMessage) log() log(destructiveCommandMessages.overwriteNotice) - await confirmPrompt(destructiveCommandMessages.blobDelete.overwriteConfirmation) + await confirmPrompt(destructiveCommandMessages.blobSet.overwriteConfirmation) } diff --git a/src/utils/scriptedCommands.ts b/src/utils/scripted-commands.ts similarity index 58% rename from src/utils/scriptedCommands.ts rename to src/utils/scripted-commands.ts index 6853746bf60..17844ce6f65 100644 --- a/src/utils/scriptedCommands.ts +++ b/src/utils/scripted-commands.ts @@ -1,7 +1,7 @@ -import process from 'process' +import process, { argv } from 'process' import { isCI } from 'ci-info' -export const scriptedCommand = (argv: string[]): boolean => { +export const shouldForceFlagBeInjected = (argv: string[]): boolean => { // Is the command run in a non-interactive shell or CI/CD environment? const scriptedCommand = Boolean(!process.stdin.isTTY || isCI || process.env.CI) @@ -9,7 +9,7 @@ export const scriptedCommand = (argv: string[]): boolean => { const notNetlifyCommand = argv.length > 2 // Is not the base `netlify` command w/ flags - const notNetlifyCommandWithFlags = argv[2] && !(argv[2].startsWith('-')) + const notNetlifyCommandWithFlags = argv[2] && !argv[2].startsWith('-') // is not the `netlify help` command const notNetlifyHelpCommand = argv[2] && !(argv[2] === 'help') @@ -19,11 +19,12 @@ export const scriptedCommand = (argv: string[]): boolean => { // Prevents prompts from blocking scripted commands return Boolean( - scriptedCommand && - notNetlifyCommand && - notNetlifyCommandWithFlags && - notNetlifyHelpCommand && - noForceFlag + scriptedCommand && notNetlifyCommand && notNetlifyCommandWithFlags && notNetlifyHelpCommand && noForceFlag, ) +} +export const injectForceFlagIfScripted = (argv: string[]) => { + if (shouldForceFlagBeInjected(argv)) { + argv.push('--force') + } } diff --git a/tests/integration/commands/blobs/blobs-delete.test.ts b/tests/integration/commands/blobs/blobs-delete.test.ts index d4652830161..eaa85efafef 100644 --- a/tests/integration/commands/blobs/blobs-delete.test.ts +++ b/tests/integration/commands/blobs/blobs-delete.test.ts @@ -5,13 +5,12 @@ import chalk from 'chalk' import inquirer from 'inquirer' import { describe, expect, test, vi, beforeEach, afterEach, beforeAll } from 'vitest' -import BaseCommand from '../../../../src/commands/base-command.js' -import { createBlobsCommand } from '../../../../src/commands/blobs/blobs.js' import { log } from '../../../../src/utils/command-helpers.js' import { destructiveCommandMessages } from '../../../../src/utils/prompts/prompt-messages.js' import { reportError } from '../../../../src/utils/telemetry/report-error.js' import { Route } from '../../utils/mock-api-vitest.js' import { getEnvironmentVariables, withMockApi, setTTYMode, setCI } from '../../utils/mock-api.js' +import { runMockProgram } from '../../utils/mock-program.js' const siteInfo = { account_slug: 'test-account', @@ -79,12 +78,9 @@ describe('blob:delete command', () => { delete: mockDelete, }) - const program = new BaseCommand('netlify') - createBlobsCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: true }) - await program.parseAsync(['', '', 'blob:delete', storeName, key]) + await runMockProgram(['', '', 'blob:delete', storeName, key]) expect(promptSpy).toHaveBeenCalledWith({ type: 'confirm', @@ -109,13 +105,10 @@ describe('blob:delete command', () => { delete: mockDelete, }) - const program = new BaseCommand('netlify') - createBlobsCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: false }) try { - await program.parseAsync(['', '', 'blob:delete', storeName, key]) + await runMockProgram(['', '', 'blob:delete', storeName, key]) } catch (error) { // We expect the process to exit, so this is fine expect(error.message).toContain('process.exit unexpectedly called') @@ -144,12 +137,9 @@ describe('blob:delete command', () => { delete: mockDelete, }) - const program = new BaseCommand('netlify') - createBlobsCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt') - await program.parseAsync(['', '', 'blob:delete', storeName, key, '--force']) + await runMockProgram(['', '', 'blob:delete', storeName, key, '--force']) expect(promptSpy).not.toHaveBeenCalled() @@ -172,13 +162,10 @@ describe('blob:delete command', () => { delete: mockDelete, }) - const program = new BaseCommand('netlify') - createBlobsCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt') try { - await program.parseAsync(['', '', 'blob:delete', storeName, key, '--force']) + await runMockProgram(['', '', 'blob:delete', storeName, key, '--force']) } catch (error) { expect(error.message).toContain( `Could not delete blob ${chalk.yellow(key)} from store ${chalk.yellow(storeName)}`, @@ -219,13 +206,9 @@ describe('blob:delete command', () => { delete: mockDelete, }) - const program = new BaseCommand('netlify') - createBlobsCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt') - await program.parseAsync(['', '', 'blob:delete', storeName, key]) - + await runMockProgram(['', '', 'blob:delete', storeName, key]) expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) @@ -245,12 +228,9 @@ describe('blob:delete command', () => { delete: mockDelete, }) - const program = new BaseCommand('netlify') - createBlobsCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt') - await program.parseAsync(['', '', 'blob:delete', storeName, key]) + await runMockProgram(['', '', 'blob:delete', storeName, key]) expect(promptSpy).not.toHaveBeenCalled() diff --git a/tests/integration/commands/blobs/blobs-set.test.ts b/tests/integration/commands/blobs/blobs-set.test.ts index 4e043a843fc..364f110002d 100644 --- a/tests/integration/commands/blobs/blobs-set.test.ts +++ b/tests/integration/commands/blobs/blobs-set.test.ts @@ -5,13 +5,12 @@ import chalk from 'chalk' import inquirer from 'inquirer' import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest' -import BaseCommand from '../../../../src/commands/base-command.js' -import { createBlobsCommand } from '../../../../src/commands/blobs/blobs.js' import { log } from '../../../../src/utils/command-helpers.js' import { destructiveCommandMessages } from '../../../../src/utils/prompts/prompt-messages.js' import { reportError } from '../../../../src/utils/telemetry/report-error.js' import { Route } from '../../utils/mock-api-vitest.js' import { getEnvironmentVariables, withMockApi, setTTYMode, setCI } from '../../utils/mock-api.js' +import { runMockProgram } from '../../utils/mock-program.js' const siteInfo = { account_slug: 'test-account', @@ -79,12 +78,9 @@ describe('blob:set command', () => { set: mockSet, }) - const program = new BaseCommand('netlify') - createBlobsCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: true }) - await program.parseAsync(['', '', 'blob:set', storeName, key, value]) + await runMockProgram(['', '', 'blob:set', storeName, key, value]) expect(promptSpy).not.toHaveBeenCalled() expect(log).toHaveBeenCalledWith(successMessage) @@ -106,12 +102,9 @@ describe('blob:set command', () => { set: mockSet, }) - const program = new BaseCommand('netlify') - createBlobsCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: true }) - await program.parseAsync(['', '', 'blob:set', storeName, key, newValue]) + await runMockProgram(['', '', 'blob:set', storeName, key, newValue]) expect(promptSpy).toHaveBeenCalledWith({ type: 'confirm', @@ -139,13 +132,10 @@ describe('blob:set command', () => { set: mockSet, }) - const program = new BaseCommand('netlify') - createBlobsCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: false }) try { - await program.parseAsync(['', '', 'blob:set', storeName, key, newValue]) + await runMockProgram(['', '', 'blob:set', storeName, key, newValue]) } catch (error) { // We expect the process to exit, so this is fine expect(error.message).toContain('process.exit unexpectedly called with "0"') @@ -177,12 +167,9 @@ describe('blob:set command', () => { set: mockSet, }) - const program = new BaseCommand('netlify') - createBlobsCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt') - await program.parseAsync(['', '', 'blob:set', storeName, key, newValue, '--force']) + await runMockProgram(['', '', 'blob:set', storeName, key, newValue, '--force']) expect(promptSpy).not.toHaveBeenCalled() @@ -202,13 +189,10 @@ describe('blob:set command', () => { set: mockSet, }) - const program = new BaseCommand('netlify') - createBlobsCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt') try { - await program.parseAsync(['', '', 'blob:set', storeName, key, newValue, '--force']) + await runMockProgram(['', '', 'blob:set', storeName, key, newValue, '--force']) } catch (error) { expect(error.message).toContain(`Could not set blob ${chalk.yellow(key)} in store ${chalk.yellow(storeName)}`) } @@ -246,12 +230,9 @@ describe('blob:set command', () => { set: mockSet, }) - const program = new BaseCommand('netlify') - createBlobsCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt') - await program.parseAsync(['', '', 'blob:set', storeName, key, newValue, '--force']) + await runMockProgram(['', '', 'blob:set', storeName, key, newValue, '--force']) expect(promptSpy).not.toHaveBeenCalled() @@ -275,13 +256,9 @@ describe('blob:set command', () => { set: mockSet, }) - const program = new BaseCommand('netlify') - createBlobsCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt') - await program.parseAsync(['', '', 'blob:set', storeName, key, newValue, '--force']) - + await runMockProgram(['', '', 'blob:set', storeName, key, newValue, '--force']) expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) diff --git a/tests/integration/commands/env/env-clone.test.ts b/tests/integration/commands/env/env-clone.test.ts index 17fe47f6632..5281ecb2bb9 100644 --- a/tests/integration/commands/env/env-clone.test.ts +++ b/tests/integration/commands/env/env-clone.test.ts @@ -4,14 +4,13 @@ import chalk from 'chalk' import inquirer from 'inquirer' import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest' -import BaseCommand from '../../../../src/commands/base-command.js' -import { createEnvCommand } from '../../../../src/commands/env/index.js' import { log } from '../../../../src/utils/command-helpers.js' import { generateEnvVarsList } from '../../../../src/utils/prompts/env-clone-prompt.js' import { destructiveCommandMessages } from '../../../../src/utils/prompts/prompt-messages.js' import { getEnvironmentVariables, withMockApi, setTTYMode, setCI } from '../../utils/mock-api.js' import { existingVar, routes, secondSiteInfo } from './api-routes.js' +import { runMockProgram } from '../../utils/mock-program.js' vi.mock('../../../../src/utils/command-helpers.js', async () => ({ ...(await vi.importActual('../../../../src/utils/command-helpers.js')), @@ -42,12 +41,9 @@ describe('env:clone command', () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const program = new BaseCommand('netlify') - createEnvCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: true }) - await program.parseAsync(['', '', 'env:clone', '-t', siteIdTwo]) + await runMockProgram(['', '', 'env:clone', '-t', siteIdTwo]) expect(promptSpy).toHaveBeenCalledWith({ type: 'confirm', @@ -70,12 +66,9 @@ describe('env:clone command', () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const program = new BaseCommand('netlify') - createEnvCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt') - await program.parseAsync(['', '', 'env:clone', '--force', '-t', siteIdTwo]) + await runMockProgram(['', '', 'env:clone', '--force', '-t', siteIdTwo]) expect(promptSpy).not.toHaveBeenCalled() @@ -93,13 +86,10 @@ describe('env:clone command', () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const program = new BaseCommand('netlify') - createEnvCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: false }) try { - await program.parseAsync(['', '', 'env:clone', '-t', siteIdTwo]) + await runMockProgram(['', '', 'env:clone', '-t', siteIdTwo]) } catch (error) { // We expect the process to exit, so this is fine expect(error.message).toContain('process.exit unexpectedly called') @@ -124,12 +114,9 @@ describe('env:clone command', () => { 'site-name', )} to ${chalk.green('site-name-3')}` - const program = new BaseCommand('netlify') - createEnvCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt') - await program.parseAsync(['', '', 'env:clone', '-t', 'site_id_3']) + await runMockProgram(['', '', 'env:clone', '-t', 'site_id_3']) expect(promptSpy).not.toHaveBeenCalled() @@ -160,12 +147,9 @@ describe('env:clone command', () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const program = new BaseCommand('netlify') - createEnvCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt') - await program.parseAsync(['', '', 'env:clone', '-t', siteIdTwo]) + await runMockProgram(['', '', 'env:clone', '-t', siteIdTwo]) expect(promptSpy).not.toHaveBeenCalled() @@ -181,12 +165,9 @@ describe('env:clone command', () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const program = new BaseCommand('netlify') - createEnvCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt') - await program.parseAsync(['', '', 'env:clone', '-t', siteIdTwo]) + await runMockProgram(['', '', 'env:clone', '-t', siteIdTwo]) expect(promptSpy).not.toHaveBeenCalled() diff --git a/tests/integration/commands/env/env-set.test.ts b/tests/integration/commands/env/env-set.test.ts index 7face708a79..5931f1492a8 100644 --- a/tests/integration/commands/env/env-set.test.ts +++ b/tests/integration/commands/env/env-set.test.ts @@ -4,13 +4,11 @@ import chalk from 'chalk' import inquirer from 'inquirer' import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest' -import BaseCommand from '../../../../src/commands/base-command.js' -import { createEnvCommand } from '../../../../src/commands/env/env.js' import { log } from '../../../../src/utils/command-helpers.js' import { destructiveCommandMessages } from '../../../../src/utils/prompts/prompt-messages.js' import { FixtureTestContext, setupFixtureTests } from '../../utils/fixture.js' import { getEnvironmentVariables, withMockApi, setTTYMode, setCI } from '../../utils/mock-api.js' - +import { runMockProgram } from '../../utils/mock-program.js' import { routes } from './api-routes.js' vi.mock('../../../../src/utils/command-helpers.js', async () => ({ @@ -291,13 +289,9 @@ describe('env:set command', () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const program = new BaseCommand('netlify') - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: true }) - createEnvCommand(program) - - await program.parseAsync(['', '', 'env:set', existingVar, newEnvValue]) + await runMockProgram(['', '', 'env:set', existingVar, newEnvValue]) expect(promptSpy).toHaveBeenCalledWith({ type: 'confirm', @@ -316,12 +310,9 @@ describe('env:set command', () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const program = new BaseCommand('netlify') - createEnvCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt') - await program.parseAsync(['', '', 'env:set', 'NEW_ENV_VAR', 'NEW_VALUE']) + await runMockProgram(['', '', 'env:set', 'NEW_ENV_VAR', 'NEW_VALUE']) expect(promptSpy).not.toHaveBeenCalled() @@ -339,12 +330,9 @@ describe('env:set command', () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const program = new BaseCommand('netlify') - createEnvCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt') - await program.parseAsync(['', '', 'env:set', existingVar, newEnvValue, '--force']) + await runMockProgram(['', '', 'env:set', existingVar, newEnvValue, '--force']) expect(promptSpy).not.toHaveBeenCalled() @@ -358,13 +346,10 @@ describe('env:set command', () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const program = new BaseCommand('netlify') - createEnvCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: false }) try { - await program.parseAsync(['', '', 'env:set', existingVar, newEnvValue]) + await runMockProgram(['', '', 'env:set', existingVar, newEnvValue]) } catch (error) { // We expect the process to exit, so this is fine expect(error.message).toContain('process.exit unexpectedly called') @@ -394,12 +379,9 @@ describe('env:set command', () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const program = new BaseCommand('netlify') - createEnvCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt') - await program.parseAsync(['', '', 'env:set', existingVar, newEnvValue]) + await runMockProgram(['', '', 'env:set', existingVar, newEnvValue]) expect(promptSpy).not.toHaveBeenCalled() @@ -415,11 +397,8 @@ describe('env:set command', () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const program = new BaseCommand('netlify') - createEnvCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt') - await program.parseAsync(['', '', 'env:set', existingVar, newEnvValue]) + await runMockProgram(['', '', 'env:set', existingVar, newEnvValue]) expect(promptSpy).not.toHaveBeenCalled() diff --git a/tests/integration/commands/env/env-unset.test.ts b/tests/integration/commands/env/env-unset.test.ts index d350a6b82a6..f82203455f4 100644 --- a/tests/integration/commands/env/env-unset.test.ts +++ b/tests/integration/commands/env/env-unset.test.ts @@ -4,15 +4,13 @@ import chalk from 'chalk' import inquirer from 'inquirer' import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest' -import BaseCommand from '../../../../src/commands/base-command.js' -import { createEnvCommand } from '../../../../src/commands/env/env.js' import { log } from '../../../../src/utils/command-helpers.js' import { destructiveCommandMessages } from '../../../../src/utils/prompts/prompt-messages.js' import { FixtureTestContext, setupFixtureTests } from '../../utils/fixture.js' import { getEnvironmentVariables, withMockApi, setTTYMode, setCI } from '../../utils/mock-api.js' -import { scriptedCommand } from '../../../../src/utils/scriptedCommands.js' import { routes } from './api-routes.js' +import { runMockProgram } from '../../utils/mock-program.js' vi.mock('../../../../src/utils/command-helpers.js', async () => ({ ...(await vi.importActual('../../../../src/utils/command-helpers.js')), @@ -97,12 +95,9 @@ describe('env:unset command', () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const program = new BaseCommand('netlify') - createEnvCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: true }) - await program.parseAsync(['', '', 'env:unset', existingVar]) + await runMockProgram(['', '', 'env:unset', existingVar]) expect(promptSpy).toHaveBeenCalledWith({ type: 'confirm', @@ -121,12 +116,9 @@ describe('env:unset command', () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const program = new BaseCommand('netlify') - createEnvCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt') - await program.parseAsync(['', '', 'env:unset', existingVar, '--force']) + await runMockProgram(['', '', 'env:unset', existingVar, '--force']) expect(promptSpy).not.toHaveBeenCalled() @@ -140,13 +132,10 @@ describe('env:unset command', () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const program = new BaseCommand('netlify') - createEnvCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: false }) try { - await program.parseAsync(['', '', 'env:unset', existingVar]) + await runMockProgram(['', '', 'env:unset', existingVar]) } catch (error) { // We expect the process to exit, so this is fine expect(error.message).toContain('process.exit unexpectedly called') @@ -164,12 +153,9 @@ describe('env:unset command', () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const program = new BaseCommand('netlify') - createEnvCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt') - await program.parseAsync(['', '', 'env:unset', 'NEW_ENV_VAR']) + await runMockProgram(['', '', 'env:unset', 'NEW_ENV_VAR']) expect(promptSpy).not.toHaveBeenCalled() @@ -191,22 +177,14 @@ describe('env:unset command', () => { }) test('prompts should not show in an non-interactive shell', async () => { + setTTYMode(false) await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const program = new BaseCommand('netlify') - createEnvCommand(program) - - const mockArgv = ['', '', 'env:unset', existingVar] - if (scriptedCommand(mockArgv)) { - mockArgv.push('--force') - } - const promptSpy = vi.spyOn(inquirer, 'prompt') - await program.parseAsync(mockArgv) - + await runMockProgram(['', '', 'env:unset', existingVar]) expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) @@ -221,13 +199,9 @@ describe('env:unset command', () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const program = new BaseCommand('netlify') - createEnvCommand(program) - const promptSpy = vi.spyOn(inquirer, 'prompt') - await program.parseAsync(['', '', 'env:unset', existingVar]) - + await runMockProgram(['', '', 'env:unset', existingVar]) expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) diff --git a/tests/integration/utils/mock-program.ts b/tests/integration/utils/mock-program.ts new file mode 100644 index 00000000000..95e2d2dab0b --- /dev/null +++ b/tests/integration/utils/mock-program.ts @@ -0,0 +1,15 @@ +import { createMainCommand } from '../../../src/commands/index.js' +import { injectForceFlagIfScripted } from '../../../src/utils/scripted-commands.js' + +export const runMockProgram = async (argv) => { + // inject the force flag if the command is a non-interactive shell or Ci enviroment + const program = createMainCommand() + + const isValidCommand = program.commands.some((cmd) => cmd.name() === argv[2]) + + if (isValidCommand) { + injectForceFlagIfScripted(argv) + } + + await program.parseAsync(argv) +} diff --git a/tests/integration/utils/snapshots.js b/tests/integration/utils/snapshots.js index f8eecb5c556..4d968f32eb9 100644 --- a/tests/integration/utils/snapshots.js +++ b/tests/integration/utils/snapshots.js @@ -14,6 +14,10 @@ const baseNormalizers = [ // this is specific to npm v6 { pattern: /@ (\w+).+\/.+netlify-cli-tests-v[\d{2}].+/, value: '$1' }, { pattern: /It should be one of.+/gm, value: 'It should be one of: *' }, + { + pattern: /--force\s+Force command to run\. Bypasses prompts for certain\s*\n\s*destructive commands\./g, + value: '--force Force command to run. Bypasses prompts for certain destructive commands.', + }, ] const optionalNormalizers = { From 621c640cec481433deadb4b17363a94105120ba9 Mon Sep 17 00:00:00 2001 From: Will Date: Tue, 29 Oct 2024 10:17:06 -0500 Subject: [PATCH 29/39] feat: refactor of run.js into components to add force flag Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> --- bin/run.js | 13 +++---------- docs/commands/api.md | 1 - docs/commands/blobs.md | 7 ++----- docs/commands/build.md | 3 +-- docs/commands/completion.md | 2 -- docs/commands/deploy.md | 5 ++--- docs/commands/dev.md | 4 +--- docs/commands/env.md | 14 +++++--------- docs/commands/functions.md | 8 +------- docs/commands/init.md | 2 +- docs/commands/integration.md | 4 +--- docs/commands/link.md | 1 - docs/commands/login.md | 1 - docs/commands/logs.md | 3 --- docs/commands/open.md | 3 --- docs/commands/recipes.md | 2 -- docs/commands/serve.md | 1 - docs/commands/sites.md | 10 +++------- docs/commands/status.md | 2 -- docs/commands/switch.md | 1 - docs/commands/unlink.md | 1 - docs/commands/watch.md | 1 - src/commands/base-command.ts | 2 +- src/commands/main.ts | 18 ++++++++++++++++++ src/utils/run-program.ts | 15 +++++++++++++++ src/utils/scripted-commands.ts | 15 ++------------- .../help/__snapshots__/help.test.ts.snap | 1 - tests/integration/utils/mock-program.ts | 10 ++-------- tests/integration/utils/snapshots.js | 4 ---- 29 files changed, 58 insertions(+), 96 deletions(-) create mode 100644 src/utils/run-program.ts diff --git a/bin/run.js b/bin/run.js index d4e8bde0e22..2e34d199ca8 100755 --- a/bin/run.js +++ b/bin/run.js @@ -3,10 +3,10 @@ import { argv } from 'process' import updateNotifier from 'update-notifier' -import { createMainCommand } from '../dist/commands/index.js' +import { runProgram } from '../dist/utils/run-program.js' import { error } from '../dist/utils/command-helpers.js' import getPackageJson from '../dist/utils/get-package-json.js' -import { injectForceFlagIfScripted } from '../dist/utils/scripted-commands.js' +import { createMainCommand } from '../dist/commands/main.js' // 12 hours const UPDATE_CHECK_INTERVAL = 432e5 @@ -25,14 +25,7 @@ try { const program = createMainCommand() try { - const isValidCommand = program.commands.some((cmd) => cmd.name() === argv[2]) - - if (isValidCommand) { - injectForceFlagIfScripted(argv) - } - // inject the force flag if the command is a non-interactive shell or Ci enviroment - - await program.parseAsync(argv) + await runProgram(program, argv) program.onEnd() } catch (error_) { diff --git a/docs/commands/api.md b/docs/commands/api.md index e87f5de6e82..aff109929bb 100644 --- a/docs/commands/api.md +++ b/docs/commands/api.md @@ -25,7 +25,6 @@ netlify api - `data` (*string*) - Data to use - `list` (*boolean*) - List out available API methods - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. **Examples** diff --git a/docs/commands/blobs.md b/docs/commands/blobs.md index 681dedbcb4b..d4553f13d20 100644 --- a/docs/commands/blobs.md +++ b/docs/commands/blobs.md @@ -18,7 +18,6 @@ netlify blobs - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. | Subcommand | description | |:--------------------------- |:-----| @@ -58,8 +57,8 @@ netlify blobs:delete **Flags** - `filter` (*string*) - For monorepos, specify the name of the application to run the command in +- `force` (*boolean*) - Bypasses prompts & Force the command to run. - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. --- ## `blobs:get` @@ -82,7 +81,6 @@ netlify blobs:get - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `output` (*string*) - Defines the filesystem path where the blob data should be persisted - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. --- ## `blobs:list` @@ -106,7 +104,6 @@ netlify blobs:list - `json` (*boolean*) - Output list contents as JSON - `prefix` (*string*) - A string for filtering down the entries; when specified, only the entries whose key starts with that prefix are returned - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. --- ## `blobs:set` @@ -128,9 +125,9 @@ netlify blobs:set **Flags** - `filter` (*string*) - For monorepos, specify the name of the application to run the command in +- `force` (*boolean*) - Bypasses prompts & Force the command to run. - `input` (*string*) - Defines the filesystem path where the blob data should be read from - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. --- diff --git a/docs/commands/build.md b/docs/commands/build.md index ba15ab8b074..525e50693cf 100644 --- a/docs/commands/build.md +++ b/docs/commands/build.md @@ -18,9 +18,8 @@ netlify build - `context` (*string*) - Specify a build context or branch (contexts: "production", "deploy-preview", "branch-deploy", "dev") - `dry` (*boolean*) - Dry run: show instructions without running them - `filter` (*string*) - For monorepos, specify the name of the application to run the command in -- `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. - `offline` (*boolean*) - disables any features that require network access +- `debug` (*boolean*) - Print debugging information **Examples** diff --git a/docs/commands/completion.md b/docs/commands/completion.md index 010ef5f4264..37be1a6dedf 100644 --- a/docs/commands/completion.md +++ b/docs/commands/completion.md @@ -18,7 +18,6 @@ netlify completion **Flags** - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. | Subcommand | description | |:--------------------------- |:-----| @@ -46,7 +45,6 @@ netlify completion:install - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. --- diff --git a/docs/commands/deploy.md b/docs/commands/deploy.md index deb8134ab49..85a23d2c327 100644 --- a/docs/commands/deploy.md +++ b/docs/commands/deploy.md @@ -92,15 +92,14 @@ netlify deploy - `functions` (*string*) - Specify a functions folder to deploy - `json` (*boolean*) - Output deployment data as JSON - `message` (*string*) - A short message to include in the deploy log -- `prod-if-unlocked` (*boolean*) - Deploy to production if unlocked, create a draft otherwise -- `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. - `open` (*boolean*) - Open site after deploy - `prod` (*boolean*) - Deploy to production +- `prod-if-unlocked` (*boolean*) - Deploy to production if unlocked, create a draft otherwise - `site` (*string*) - A site name or ID to deploy to - `skip-functions-cache` (*boolean*) - Ignore any functions created as part of a previous `build` or `deploy` commands, forcing them to be bundled again as part of the deployment - `timeout` (*string*) - Timeout to wait for deployment to finish - `trigger` (*boolean*) - Trigger a new build of your site on Netlify without uploading local files +- `debug` (*boolean*) - Print debugging information **Examples** diff --git a/docs/commands/dev.md b/docs/commands/dev.md index 72f4222803d..8bc9e5d5aa8 100644 --- a/docs/commands/dev.md +++ b/docs/commands/dev.md @@ -30,11 +30,10 @@ netlify dev - `geo` (*cache | mock | update*) - force geolocation data to be updated, use cached data from the last 24h if found, or use a mock location - `live` (*string*) - start a public live session; optionally, supply a subdomain to generate a custom URL - `no-open` (*boolean*) - disables the automatic opening of a browser window -- `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. - `offline` (*boolean*) - disables any features that require network access - `port` (*string*) - port of netlify dev - `target-port` (*string*) - port of target app server +- `debug` (*boolean*) - Print debugging information | Subcommand | description | |:--------------------------- |:-----| @@ -75,7 +74,6 @@ netlify dev:exec - `context` (*string*) - Specify a deploy context or branch for environment variables (contexts: "production", "deploy-preview", "branch-deploy", "dev") - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. **Examples** diff --git a/docs/commands/env.md b/docs/commands/env.md index 84aecbce1a3..517102bfa81 100644 --- a/docs/commands/env.md +++ b/docs/commands/env.md @@ -18,7 +18,6 @@ netlify env - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. | Subcommand | description | |:--------------------------- |:-----| @@ -55,10 +54,10 @@ netlify env:clone **Flags** - `filter` (*string*) - For monorepos, specify the name of the application to run the command in +- `force` (*boolean*) - Bypasses prompts & Force the command to run. - `from` (*string*) - Site ID (From) - `to` (*string*) - Site ID (To) - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. **Examples** @@ -88,7 +87,6 @@ netlify env:get - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `scope` (*builds | functions | post-processing | runtime | any*) - Specify a scope - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. **Examples** @@ -119,7 +117,6 @@ netlify env:import - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `replace-existing` (*boolean*) - Replace all existing variables instead of merging them with the current ones - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. --- ## `env:list` @@ -137,10 +134,9 @@ netlify env:list - `context` (*string*) - Specify a deploy context or branch (contexts: "production", "deploy-preview", "branch-deploy", "dev") - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `json` (*boolean*) - Output environment variables as JSON +- `plain` (*boolean*) - Output environment variables as plaintext - `scope` (*builds | functions | post-processing | runtime | any*) - Specify a scope - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. -- `plain` (*boolean*) - Output environment variables as plaintext **Examples** @@ -172,10 +168,10 @@ netlify env:set - `context` (*string*) - Specify a deploy context or branch (contexts: "production", "deploy-preview", "branch-deploy", "dev") (default: all contexts) - `filter` (*string*) - For monorepos, specify the name of the application to run the command in +- `force` (*boolean*) - Bypasses prompts & Force the command to run. - `scope` (*builds | functions | post-processing | runtime*) - Specify a scope (default: all scopes) -- `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. - `secret` (*boolean*) - Indicate whether the environment variable value can be read again. +- `debug` (*boolean*) - Print debugging information **Examples** @@ -208,8 +204,8 @@ netlify env:unset - `context` (*string*) - Specify a deploy context or branch (contexts: "production", "deploy-preview", "branch-deploy", "dev") (default: all contexts) - `filter` (*string*) - For monorepos, specify the name of the application to run the command in +- `force` (*boolean*) - Bypasses prompts & Force the command to run. - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. **Examples** diff --git a/docs/commands/functions.md b/docs/commands/functions.md index 1efddb1b5af..49559defa45 100644 --- a/docs/commands/functions.md +++ b/docs/commands/functions.md @@ -19,7 +19,6 @@ netlify functions - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. | Subcommand | description | |:--------------------------- |:-----| @@ -54,7 +53,6 @@ netlify functions:build - `functions` (*string*) - Specify a functions directory to build to - `src` (*string*) - Specify the source directory for the functions - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. --- ## `functions:create` @@ -78,7 +76,6 @@ netlify functions:create - `name` (*string*) - function name - `url` (*string*) - pull template from URL - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. **Examples** @@ -111,10 +108,9 @@ netlify functions:invoke - `name` (*string*) - function name to invoke - `no-identity` (*boolean*) - simulate Netlify Identity authentication JWT. pass --no-identity to affirm unauthenticated request - `payload` (*string*) - Supply POST payload in stringified json, or a path to a json file -- `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. - `port` (*string*) - Port where netlify dev is accessible. e.g. 8888 - `querystring` (*string*) - Querystring to add to your function invocation +- `debug` (*boolean*) - Print debugging information **Examples** @@ -149,7 +145,6 @@ netlify functions:list - `functions` (*string*) - Specify a functions directory to list - `json` (*boolean*) - Output function data as JSON - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. --- ## `functions:serve` @@ -169,7 +164,6 @@ netlify functions:serve - `offline` (*boolean*) - disables any features that require network access - `port` (*string*) - Specify a port for the functions server - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. --- diff --git a/docs/commands/init.md b/docs/commands/init.md index cab5c90deeb..4bb49541cb2 100644 --- a/docs/commands/init.md +++ b/docs/commands/init.md @@ -17,10 +17,10 @@ netlify init **Flags** - `filter` (*string*) - For monorepos, specify the name of the application to run the command in +- `force` (*boolean*) - Bypasses prompts & Force the command to run. - `git-remote-name` (*string*) - Name of Git remote to use. e.g. "origin" - `manual` (*boolean*) - Manually configure a git remote for CI - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. diff --git a/docs/commands/integration.md b/docs/commands/integration.md index 4dbb11af41f..b0aa1c586c8 100644 --- a/docs/commands/integration.md +++ b/docs/commands/integration.md @@ -18,7 +18,6 @@ netlify integration - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. | Subcommand | description | |:--------------------------- |:-----| @@ -42,9 +41,8 @@ netlify integration:deploy - `build` (*boolean*) - Build the integration - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `prod` (*boolean*) - Deploy to production -- `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. - `site` (*string*) - A site name or ID to deploy to +- `debug` (*boolean*) - Print debugging information --- diff --git a/docs/commands/link.md b/docs/commands/link.md index 4181dcab91c..4a9cface348 100644 --- a/docs/commands/link.md +++ b/docs/commands/link.md @@ -21,7 +21,6 @@ netlify link - `id` (*string*) - ID of site to link to - `name` (*string*) - Name of site to link to - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. **Examples** diff --git a/docs/commands/login.md b/docs/commands/login.md index e6ea084dd8b..d2fb223536f 100644 --- a/docs/commands/login.md +++ b/docs/commands/login.md @@ -19,7 +19,6 @@ netlify login - `new` (*boolean*) - Login to new Netlify account - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. diff --git a/docs/commands/logs.md b/docs/commands/logs.md index 1ed111de521..64303d35909 100644 --- a/docs/commands/logs.md +++ b/docs/commands/logs.md @@ -17,7 +17,6 @@ netlify logs - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. | Subcommand | description | |:--------------------------- |:-----| @@ -48,7 +47,6 @@ netlify logs:deploy - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. --- ## `logs:function` @@ -70,7 +68,6 @@ netlify logs:function - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `level` (*string*) - Log levels to stream. Choices are: trace, debug, info, warn, error, fatal - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. **Examples** diff --git a/docs/commands/open.md b/docs/commands/open.md index a3e02053ec4..ea2dbc5b66c 100644 --- a/docs/commands/open.md +++ b/docs/commands/open.md @@ -19,7 +19,6 @@ netlify open - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `site` (*boolean*) - Open site - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. | Subcommand | description | |:--------------------------- |:-----| @@ -51,7 +50,6 @@ netlify open:admin - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. **Examples** @@ -74,7 +72,6 @@ netlify open:site - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. **Examples** diff --git a/docs/commands/recipes.md b/docs/commands/recipes.md index dae88b6adac..7a7b6999b35 100644 --- a/docs/commands/recipes.md +++ b/docs/commands/recipes.md @@ -21,7 +21,6 @@ netlify recipes - `name` (*string*) - recipe name to use - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. | Subcommand | description | |:--------------------------- |:-----| @@ -50,7 +49,6 @@ netlify recipes:list - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. **Examples** diff --git a/docs/commands/serve.md b/docs/commands/serve.md index 50dd554a588..f9f35919ad7 100644 --- a/docs/commands/serve.md +++ b/docs/commands/serve.md @@ -26,7 +26,6 @@ netlify serve - `offline` (*boolean*) - disables any features that require network access - `port` (*string*) - port of netlify dev - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. **Examples** diff --git a/docs/commands/sites.md b/docs/commands/sites.md index 55a88d31671..d5e3f515f7c 100644 --- a/docs/commands/sites.md +++ b/docs/commands/sites.md @@ -19,7 +19,6 @@ netlify sites - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. | Subcommand | description | |:--------------------------- |:-----| @@ -55,9 +54,8 @@ netlify sites:create - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `manual` (*boolean*) - force manual CI setup. Used --with-ci flag - `name` (*string*) - name of site -- `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. - `with-ci` (*boolean*) - initialize CI hooks during site creation +- `debug` (*boolean*) - Print debugging information --- ## `sites:create-template` @@ -81,9 +79,8 @@ netlify sites:create-template - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `name` (*string*) - name of site - `url` (*string*) - template url -- `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. - `with-ci` (*boolean*) - initialize CI hooks during site creation +- `debug` (*boolean*) - Print debugging information **Examples** @@ -112,8 +109,8 @@ netlify sites:delete **Flags** - `filter` (*string*) - For monorepos, specify the name of the application to run the command in +- `force` (*boolean*) - Bypasses prompts & Force the command to run. - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. **Examples** @@ -137,7 +134,6 @@ netlify sites:list - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `json` (*boolean*) - Output site data as JSON - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. --- diff --git a/docs/commands/status.md b/docs/commands/status.md index 697242a4ef5..627e40af1da 100644 --- a/docs/commands/status.md +++ b/docs/commands/status.md @@ -18,7 +18,6 @@ netlify status - `verbose` (*boolean*) - Output system info - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. | Subcommand | description | |:--------------------------- |:-----| @@ -40,7 +39,6 @@ netlify status:hooks - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. --- diff --git a/docs/commands/switch.md b/docs/commands/switch.md index 1f61c1dd9b2..38166dbd6a1 100644 --- a/docs/commands/switch.md +++ b/docs/commands/switch.md @@ -17,7 +17,6 @@ netlify switch **Flags** - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. diff --git a/docs/commands/unlink.md b/docs/commands/unlink.md index 44d81b730a2..c0f48f04539 100644 --- a/docs/commands/unlink.md +++ b/docs/commands/unlink.md @@ -18,7 +18,6 @@ netlify unlink - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. diff --git a/docs/commands/watch.md b/docs/commands/watch.md index f3610f6e2fc..2d08bac6676 100644 --- a/docs/commands/watch.md +++ b/docs/commands/watch.md @@ -18,7 +18,6 @@ netlify watch - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information -- `force` (*boolean*) - Force command to run. Bypasses prompts for certain destructive commands. **Examples** diff --git a/src/commands/base-command.ts b/src/commands/base-command.ts index fbdba97f741..9b14685014f 100644 --- a/src/commands/base-command.ts +++ b/src/commands/base-command.ts @@ -193,7 +193,7 @@ export default class BaseCommand extends Command { createCommand(name: string): BaseCommand { const base = new BaseCommand(name) // If --silent or --json flag passed disable logger - .addOption(new Option('--force', 'Force command to run. Bypasses prompts for certain destructive commands.')) + // .addOption(new Option('--force', 'Force command to run. Bypasses prompts for certain destructive commands.')) .addOption(new Option('--json', 'Output return values as JSON').hideHelp(true)) .addOption(new Option('--silent', 'Silence CLI output').hideHelp(true)) .addOption(new Option('--cwd ').hideHelp(true)) diff --git a/src/commands/main.ts b/src/commands/main.ts index 451dd0307b3..53ecfa378d9 100644 --- a/src/commands/main.ts +++ b/src/commands/main.ts @@ -39,6 +39,18 @@ import { createUnlinkCommand } from './unlink/index.js' import { createWatchCommand } from './watch/index.js' const SUGGESTION_TIMEOUT = 1e4 +export const COMMANDS_WITH_FORCE = new Set([ + 'env:set', + 'env:unset', + 'env:clone', + 'blobs:set', + 'blobs:delete', + 'addons:delete', + 'init', + 'lm:install', + 'lm:setup', + 'sites:delete', +]) process.on('uncaughtException', async (err) => { console.log('') @@ -230,5 +242,11 @@ export const createMainCommand = () => { }) .action(mainCommand) + program.commands.forEach((cmd) => { + if (COMMANDS_WITH_FORCE.has(cmd.name())) { + cmd.option('--force', 'Bypasses prompts & Force the command to run.') + } + }) + return program } diff --git a/src/utils/run-program.ts b/src/utils/run-program.ts new file mode 100644 index 00000000000..954abfd3327 --- /dev/null +++ b/src/utils/run-program.ts @@ -0,0 +1,15 @@ +import { injectForceFlagIfScripted } from './scripted-commands.js' +import { BaseCommand } from '../commands/index.js' +import { COMMANDS_WITH_FORCE } from '../commands/main.js' + +// This function is used to run the program with the correct flags +export const runProgram = async (program: BaseCommand, argv: string[]) => { + //if the command is not a valid command, + const isValidForceCommand = COMMANDS_WITH_FORCE.has(argv[2]) + + if (isValidForceCommand) { + injectForceFlagIfScripted(argv) + } + + await program.parseAsync(argv) +} diff --git a/src/utils/scripted-commands.ts b/src/utils/scripted-commands.ts index 17844ce6f65..c6cf938e01e 100644 --- a/src/utils/scripted-commands.ts +++ b/src/utils/scripted-commands.ts @@ -1,26 +1,15 @@ -import process, { argv } from 'process' +import process from 'process' import { isCI } from 'ci-info' export const shouldForceFlagBeInjected = (argv: string[]): boolean => { // Is the command run in a non-interactive shell or CI/CD environment? const scriptedCommand = Boolean(!process.stdin.isTTY || isCI || process.env.CI) - // Is not the base `netlify command w/o any flags - const notNetlifyCommand = argv.length > 2 - - // Is not the base `netlify` command w/ flags - const notNetlifyCommandWithFlags = argv[2] && !argv[2].startsWith('-') - - // is not the `netlify help` command - const notNetlifyHelpCommand = argv[2] && !(argv[2] === 'help') - // Is the `--force` flag not already present? const noForceFlag = !argv.includes('--force') // Prevents prompts from blocking scripted commands - return Boolean( - scriptedCommand && notNetlifyCommand && notNetlifyCommandWithFlags && notNetlifyHelpCommand && noForceFlag, - ) + return Boolean(scriptedCommand && noForceFlag) } export const injectForceFlagIfScripted = (argv: string[]) => { diff --git a/tests/integration/commands/help/__snapshots__/help.test.ts.snap b/tests/integration/commands/help/__snapshots__/help.test.ts.snap index d095108bbfa..57c14e623ca 100644 --- a/tests/integration/commands/help/__snapshots__/help.test.ts.snap +++ b/tests/integration/commands/help/__snapshots__/help.test.ts.snap @@ -46,7 +46,6 @@ USAGE OPTIONS -h, --help display help for command --debug Print debugging information - --force Force command to run. Bypasses prompts for certain destructive commands. DESCRIPTION Run this command to see instructions for your shell. diff --git a/tests/integration/utils/mock-program.ts b/tests/integration/utils/mock-program.ts index 95e2d2dab0b..0456e6b454a 100644 --- a/tests/integration/utils/mock-program.ts +++ b/tests/integration/utils/mock-program.ts @@ -1,15 +1,9 @@ +import { runProgram } from '../../../src/utils/run-program.js' import { createMainCommand } from '../../../src/commands/index.js' -import { injectForceFlagIfScripted } from '../../../src/utils/scripted-commands.js' export const runMockProgram = async (argv) => { // inject the force flag if the command is a non-interactive shell or Ci enviroment const program = createMainCommand() - const isValidCommand = program.commands.some((cmd) => cmd.name() === argv[2]) - - if (isValidCommand) { - injectForceFlagIfScripted(argv) - } - - await program.parseAsync(argv) + await runProgram(program, argv) } diff --git a/tests/integration/utils/snapshots.js b/tests/integration/utils/snapshots.js index 4d968f32eb9..f8eecb5c556 100644 --- a/tests/integration/utils/snapshots.js +++ b/tests/integration/utils/snapshots.js @@ -14,10 +14,6 @@ const baseNormalizers = [ // this is specific to npm v6 { pattern: /@ (\w+).+\/.+netlify-cli-tests-v[\d{2}].+/, value: '$1' }, { pattern: /It should be one of.+/gm, value: 'It should be one of: *' }, - { - pattern: /--force\s+Force command to run\. Bypasses prompts for certain\s*\n\s*destructive commands\./g, - value: '--force Force command to run. Bypasses prompts for certain destructive commands.', - }, ] const optionalNormalizers = { From 26d70c83e3274addb546ef2aedbc4c0d766297de Mon Sep 17 00:00:00 2001 From: Will Date: Tue, 29 Oct 2024 10:51:36 -0500 Subject: [PATCH 30/39] fix: types.ts merge } deletion Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> --- src/utils/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/types.ts b/src/utils/types.ts index f19d3e8c51e..ea85aaf6cdc 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -161,6 +161,7 @@ export type EnvVar = { type EnvVarValue = { id: string context: string +} export interface Account { id: string From f45f6d48891e6f0152007b352f06c4e377390091 Mon Sep 17 00:00:00 2001 From: Will Date: Tue, 29 Oct 2024 11:01:59 -0500 Subject: [PATCH 31/39] fix: fix default lint issue and typescript issue fixed lint issue that was casuing test in ci enviroment to fail Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> --- tests/integration/commands/dev/dev.exec.test.js | 2 +- tests/integration/commands/env/api-routes.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/commands/dev/dev.exec.test.js b/tests/integration/commands/dev/dev.exec.test.js index 068c480d079..42a8c0834eb 100644 --- a/tests/integration/commands/dev/dev.exec.test.js +++ b/tests/integration/commands/dev/dev.exec.test.js @@ -5,7 +5,7 @@ import { test } from 'vitest' import { callCli } from '../../utils/call-cli.js' import { getCLIOptions, withMockApi } from '../../utils/mock-api.js' import { withSiteBuilder } from '../../utils/site-builder.ts' -import routes from '../env/api-routes.ts' +import { routes } from '../env/api-routes.ts' test('should pass .env variables to exec command', async (t) => { await withSiteBuilder(t, async (builder) => { diff --git a/tests/integration/commands/env/api-routes.ts b/tests/integration/commands/env/api-routes.ts index d5913deb974..717a5f5df97 100644 --- a/tests/integration/commands/env/api-routes.ts +++ b/tests/integration/commands/env/api-routes.ts @@ -28,7 +28,7 @@ const thirdSiteInfo = { name: 'site-name-3', } -export const existingVar: EnvVar = { +export const existingVar = { key: 'EXISTING_VAR', scopes: ['builds', 'functions'], values: [ From 637df207938246e46b214fe38f0b9b4b32361a10 Mon Sep 17 00:00:00 2001 From: Will Date: Tue, 29 Oct 2024 11:21:49 -0500 Subject: [PATCH 32/39] fix: update blob to blobs Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> --- .../commands/blobs/blobs-delete.test.ts | 14 +++++++------- .../integration/commands/blobs/blobs-set.test.ts | 16 ++++++++-------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/integration/commands/blobs/blobs-delete.test.ts b/tests/integration/commands/blobs/blobs-delete.test.ts index eaa85efafef..341d71b9d4e 100644 --- a/tests/integration/commands/blobs/blobs-delete.test.ts +++ b/tests/integration/commands/blobs/blobs-delete.test.ts @@ -46,7 +46,7 @@ const routes: Route[] = [ }, ] -describe('blob:delete command', () => { +describe('blobs:delete command', () => { const storeName = 'my-store' const key = 'my-key' @@ -80,7 +80,7 @@ describe('blob:delete command', () => { const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: true }) - await runMockProgram(['', '', 'blob:delete', storeName, key]) + await runMockProgram(['', '', 'blobs:delete', storeName, key]) expect(promptSpy).toHaveBeenCalledWith({ type: 'confirm', @@ -108,7 +108,7 @@ describe('blob:delete command', () => { const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: false }) try { - await runMockProgram(['', '', 'blob:delete', storeName, key]) + await runMockProgram(['', '', 'blobs:delete', storeName, key]) } catch (error) { // We expect the process to exit, so this is fine expect(error.message).toContain('process.exit unexpectedly called') @@ -139,7 +139,7 @@ describe('blob:delete command', () => { const promptSpy = vi.spyOn(inquirer, 'prompt') - await runMockProgram(['', '', 'blob:delete', storeName, key, '--force']) + await runMockProgram(['', '', 'blobs:delete', storeName, key, '--force']) expect(promptSpy).not.toHaveBeenCalled() @@ -165,7 +165,7 @@ describe('blob:delete command', () => { const promptSpy = vi.spyOn(inquirer, 'prompt') try { - await runMockProgram(['', '', 'blob:delete', storeName, key, '--force']) + await runMockProgram(['', '', 'blobs:delete', storeName, key, '--force']) } catch (error) { expect(error.message).toContain( `Could not delete blob ${chalk.yellow(key)} from store ${chalk.yellow(storeName)}`, @@ -208,7 +208,7 @@ describe('blob:delete command', () => { const promptSpy = vi.spyOn(inquirer, 'prompt') - await runMockProgram(['', '', 'blob:delete', storeName, key]) + await runMockProgram(['', '', 'blobs:delete', storeName, key]) expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) @@ -230,7 +230,7 @@ describe('blob:delete command', () => { const promptSpy = vi.spyOn(inquirer, 'prompt') - await runMockProgram(['', '', 'blob:delete', storeName, key]) + await runMockProgram(['', '', 'blobs:delete', storeName, key]) expect(promptSpy).not.toHaveBeenCalled() diff --git a/tests/integration/commands/blobs/blobs-set.test.ts b/tests/integration/commands/blobs/blobs-set.test.ts index 364f110002d..08c20272a4a 100644 --- a/tests/integration/commands/blobs/blobs-set.test.ts +++ b/tests/integration/commands/blobs/blobs-set.test.ts @@ -45,7 +45,7 @@ const routes: Route[] = [ }, ] -describe('blob:set command', () => { +describe('blobs:set command', () => { const storeName = 'my-store' const key = 'my-key' const value = 'my-value' @@ -80,7 +80,7 @@ describe('blob:set command', () => { const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: true }) - await runMockProgram(['', '', 'blob:set', storeName, key, value]) + await runMockProgram(['', '', 'blobs:set', storeName, key, value]) expect(promptSpy).not.toHaveBeenCalled() expect(log).toHaveBeenCalledWith(successMessage) @@ -104,7 +104,7 @@ describe('blob:set command', () => { const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: true }) - await runMockProgram(['', '', 'blob:set', storeName, key, newValue]) + await runMockProgram(['', '', 'blobs:set', storeName, key, newValue]) expect(promptSpy).toHaveBeenCalledWith({ type: 'confirm', @@ -135,7 +135,7 @@ describe('blob:set command', () => { const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: false }) try { - await runMockProgram(['', '', 'blob:set', storeName, key, newValue]) + await runMockProgram(['', '', 'blobs:set', storeName, key, newValue]) } catch (error) { // We expect the process to exit, so this is fine expect(error.message).toContain('process.exit unexpectedly called with "0"') @@ -169,7 +169,7 @@ describe('blob:set command', () => { const promptSpy = vi.spyOn(inquirer, 'prompt') - await runMockProgram(['', '', 'blob:set', storeName, key, newValue, '--force']) + await runMockProgram(['', '', 'blobs:set', storeName, key, newValue, '--force']) expect(promptSpy).not.toHaveBeenCalled() @@ -192,7 +192,7 @@ describe('blob:set command', () => { const promptSpy = vi.spyOn(inquirer, 'prompt') try { - await runMockProgram(['', '', 'blob:set', storeName, key, newValue, '--force']) + await runMockProgram(['', '', 'blobs:set', storeName, key, newValue, '--force']) } catch (error) { expect(error.message).toContain(`Could not set blob ${chalk.yellow(key)} in store ${chalk.yellow(storeName)}`) } @@ -232,7 +232,7 @@ describe('blob:set command', () => { const promptSpy = vi.spyOn(inquirer, 'prompt') - await runMockProgram(['', '', 'blob:set', storeName, key, newValue, '--force']) + await runMockProgram(['', '', 'blobs:set', storeName, key, newValue, '--force']) expect(promptSpy).not.toHaveBeenCalled() @@ -258,7 +258,7 @@ describe('blob:set command', () => { const promptSpy = vi.spyOn(inquirer, 'prompt') - await runMockProgram(['', '', 'blob:set', storeName, key, newValue, '--force']) + await runMockProgram(['', '', 'blobs:set', storeName, key, newValue, '--force']) expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) From cbc3b7c28fe3b0094e7a24d12bce936438ccdbf5 Mon Sep 17 00:00:00 2001 From: Will Date: Tue, 29 Oct 2024 11:39:11 -0500 Subject: [PATCH 33/39] fix: updated prompt tests for ci/cd enviroment Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> --- tests/integration/commands/blobs/blobs-delete.test.ts | 4 +++- tests/integration/commands/blobs/blobs-set.test.ts | 3 ++- tests/integration/commands/env/env-clone.test.ts | 1 + tests/integration/commands/env/env-set.test.ts | 1 + tests/integration/commands/env/env-unset.test.ts | 1 + 5 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/integration/commands/blobs/blobs-delete.test.ts b/tests/integration/commands/blobs/blobs-delete.test.ts index 341d71b9d4e..4a3c83dc3c7 100644 --- a/tests/integration/commands/blobs/blobs-delete.test.ts +++ b/tests/integration/commands/blobs/blobs-delete.test.ts @@ -60,8 +60,10 @@ describe('blobs:delete command', () => { )}` describe('user is prompted to confirm when deleting a blob key', () => { - beforeAll(() => { + beforeEach(() => { setTTYMode(true) + setCI('') + vi.resetAllMocks() }) beforeEach(() => { diff --git a/tests/integration/commands/blobs/blobs-set.test.ts b/tests/integration/commands/blobs/blobs-set.test.ts index 08c20272a4a..36a5dd2e322 100644 --- a/tests/integration/commands/blobs/blobs-set.test.ts +++ b/tests/integration/commands/blobs/blobs-set.test.ts @@ -62,8 +62,9 @@ describe('blobs:set command', () => { describe('user is prompted to confirm when setting a a blob key that already exists', () => { beforeEach(() => { - vi.resetAllMocks() setTTYMode(true) + setCI('') + vi.resetAllMocks() }) test('should not log warnings and prompt if blob key does not exist', async () => { diff --git a/tests/integration/commands/env/env-clone.test.ts b/tests/integration/commands/env/env-clone.test.ts index 5281ecb2bb9..fc4765d4c22 100644 --- a/tests/integration/commands/env/env-clone.test.ts +++ b/tests/integration/commands/env/env-clone.test.ts @@ -34,6 +34,7 @@ describe('env:clone command', () => { describe('user is prompted to confirm when setting an env var that already exists', () => { beforeEach(() => { setTTYMode(true) + setCI('') vi.resetAllMocks() }) diff --git a/tests/integration/commands/env/env-set.test.ts b/tests/integration/commands/env/env-set.test.ts index 5931f1492a8..72d6d4106cc 100644 --- a/tests/integration/commands/env/env-set.test.ts +++ b/tests/integration/commands/env/env-set.test.ts @@ -282,6 +282,7 @@ describe('env:set command', () => { describe('user is prompted to confirmOverwrite when setting an env var that already exists', () => { beforeEach(() => { setTTYMode(true) + setCI('') vi.resetAllMocks() }) diff --git a/tests/integration/commands/env/env-unset.test.ts b/tests/integration/commands/env/env-unset.test.ts index f82203455f4..aad03cf307b 100644 --- a/tests/integration/commands/env/env-unset.test.ts +++ b/tests/integration/commands/env/env-unset.test.ts @@ -88,6 +88,7 @@ describe('env:unset command', () => { describe('user is prompted to confirm when unsetting an env var that already exists', () => { beforeEach(() => { setTTYMode(true) + setCI('') vi.resetAllMocks() }) From c9592a0399775a704543bf007f0d3ec7f8ab00f9 Mon Sep 17 00:00:00 2001 From: Will Date: Tue, 29 Oct 2024 12:09:02 -0500 Subject: [PATCH 34/39] fix: updated prompt tests to work correctly in ci/cd enviroments Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> --- src/utils/scripted-commands.ts | 4 +++- .../commands/blobs/blobs-delete.test.ts | 14 ++++++++------ .../commands/blobs/blobs-set.test.ts | 18 ++++++++++-------- .../integration/commands/env/env-clone.test.ts | 14 ++++++++++---- tests/integration/commands/env/env-set.test.ts | 15 +++++++++++---- .../integration/commands/env/env-unset.test.ts | 14 ++++++++++---- tests/integration/utils/mock-api.js | 4 ++++ 7 files changed, 56 insertions(+), 27 deletions(-) diff --git a/src/utils/scripted-commands.ts b/src/utils/scripted-commands.ts index c6cf938e01e..9227dc8fcfe 100644 --- a/src/utils/scripted-commands.ts +++ b/src/utils/scripted-commands.ts @@ -4,7 +4,6 @@ import { isCI } from 'ci-info' export const shouldForceFlagBeInjected = (argv: string[]): boolean => { // Is the command run in a non-interactive shell or CI/CD environment? const scriptedCommand = Boolean(!process.stdin.isTTY || isCI || process.env.CI) - // Is the `--force` flag not already present? const noForceFlag = !argv.includes('--force') @@ -13,6 +12,9 @@ export const shouldForceFlagBeInjected = (argv: string[]): boolean => { } export const injectForceFlagIfScripted = (argv: string[]) => { + // ENV Variable used to tests prompts in CI/CD enviroment + if (process.env.TESTING_PROMPTS === 'true') return + if (shouldForceFlagBeInjected(argv)) { argv.push('--force') } diff --git a/tests/integration/commands/blobs/blobs-delete.test.ts b/tests/integration/commands/blobs/blobs-delete.test.ts index 4a3c83dc3c7..07254bfc3fc 100644 --- a/tests/integration/commands/blobs/blobs-delete.test.ts +++ b/tests/integration/commands/blobs/blobs-delete.test.ts @@ -3,13 +3,13 @@ import process from 'process' import { getStore } from '@netlify/blobs' import chalk from 'chalk' import inquirer from 'inquirer' -import { describe, expect, test, vi, beforeEach, afterEach, beforeAll } from 'vitest' +import { describe, expect, test, vi, beforeEach, afterEach, beforeAll, afterAll } from 'vitest' import { log } from '../../../../src/utils/command-helpers.js' import { destructiveCommandMessages } from '../../../../src/utils/prompts/prompt-messages.js' import { reportError } from '../../../../src/utils/telemetry/report-error.js' import { Route } from '../../utils/mock-api-vitest.js' -import { getEnvironmentVariables, withMockApi, setTTYMode, setCI } from '../../utils/mock-api.js' +import { getEnvironmentVariables, withMockApi, setTTYMode, setCI, setTestingPrompts } from '../../utils/mock-api.js' import { runMockProgram } from '../../utils/mock-program.js' const siteInfo = { @@ -60,16 +60,18 @@ describe('blobs:delete command', () => { )}` describe('user is prompted to confirm when deleting a blob key', () => { - beforeEach(() => { - setTTYMode(true) - setCI('') - vi.resetAllMocks() + beforeAll(() => { + setTestingPrompts('true') }) beforeEach(() => { vi.resetAllMocks() }) + afterAll(() => { + setTestingPrompts('false') + }) + test('should log warning message and prompt for confirmation', async () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) diff --git a/tests/integration/commands/blobs/blobs-set.test.ts b/tests/integration/commands/blobs/blobs-set.test.ts index 36a5dd2e322..a720673cf37 100644 --- a/tests/integration/commands/blobs/blobs-set.test.ts +++ b/tests/integration/commands/blobs/blobs-set.test.ts @@ -3,13 +3,13 @@ import process from 'process' import { getStore } from '@netlify/blobs' import chalk from 'chalk' import inquirer from 'inquirer' -import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest' +import { describe, expect, test, vi, beforeEach, afterEach, beforeAll, afterAll } from 'vitest' import { log } from '../../../../src/utils/command-helpers.js' import { destructiveCommandMessages } from '../../../../src/utils/prompts/prompt-messages.js' import { reportError } from '../../../../src/utils/telemetry/report-error.js' import { Route } from '../../utils/mock-api-vitest.js' -import { getEnvironmentVariables, withMockApi, setTTYMode, setCI } from '../../utils/mock-api.js' +import { getEnvironmentVariables, withMockApi, setTTYMode, setCI, setTestingPrompts } from '../../utils/mock-api.js' import { runMockProgram } from '../../utils/mock-program.js' const siteInfo = { @@ -61,12 +61,18 @@ describe('blobs:set command', () => { )}` describe('user is prompted to confirm when setting a a blob key that already exists', () => { + beforeAll(() => { + setTestingPrompts('true') + }) + beforeEach(() => { - setTTYMode(true) - setCI('') vi.resetAllMocks() }) + afterAll(() => { + setTestingPrompts('false') + }) + test('should not log warnings and prompt if blob key does not exist', async () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) @@ -207,10 +213,6 @@ describe('blobs:set command', () => { }) }) - beforeEach(() => { - vi.resetAllMocks() - }) - describe('prompts should not show in a non-interactive shell or in a ci/cd enviroment', () => { afterEach(() => { setTTYMode(true) diff --git a/tests/integration/commands/env/env-clone.test.ts b/tests/integration/commands/env/env-clone.test.ts index fc4765d4c22..02b3200c5ad 100644 --- a/tests/integration/commands/env/env-clone.test.ts +++ b/tests/integration/commands/env/env-clone.test.ts @@ -2,12 +2,12 @@ import process from 'process' import chalk from 'chalk' import inquirer from 'inquirer' -import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest' +import { describe, expect, test, vi, beforeEach, afterEach, beforeAll, afterAll } from 'vitest' import { log } from '../../../../src/utils/command-helpers.js' import { generateEnvVarsList } from '../../../../src/utils/prompts/env-clone-prompt.js' import { destructiveCommandMessages } from '../../../../src/utils/prompts/prompt-messages.js' -import { getEnvironmentVariables, withMockApi, setTTYMode, setCI } from '../../utils/mock-api.js' +import { getEnvironmentVariables, withMockApi, setTTYMode, setCI, setTestingPrompts } from '../../utils/mock-api.js' import { existingVar, routes, secondSiteInfo } from './api-routes.js' import { runMockProgram } from '../../utils/mock-program.js' @@ -32,12 +32,18 @@ describe('env:clone command', () => { )}` describe('user is prompted to confirm when setting an env var that already exists', () => { + beforeAll(() => { + setTestingPrompts('true') + }) + beforeEach(() => { - setTTYMode(true) - setCI('') vi.resetAllMocks() }) + afterAll(() => { + setTestingPrompts('false') + }) + test('should log warnings and prompts if enviroment variable already exists', async () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) diff --git a/tests/integration/commands/env/env-set.test.ts b/tests/integration/commands/env/env-set.test.ts index 72d6d4106cc..948ba7d1377 100644 --- a/tests/integration/commands/env/env-set.test.ts +++ b/tests/integration/commands/env/env-set.test.ts @@ -2,12 +2,12 @@ import process from 'process' import chalk from 'chalk' import inquirer from 'inquirer' -import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest' +import { describe, expect, test, vi, beforeEach, afterEach, beforeAll, afterAll } from 'vitest' import { log } from '../../../../src/utils/command-helpers.js' import { destructiveCommandMessages } from '../../../../src/utils/prompts/prompt-messages.js' import { FixtureTestContext, setupFixtureTests } from '../../utils/fixture.js' -import { getEnvironmentVariables, withMockApi, setTTYMode, setCI } from '../../utils/mock-api.js' +import { getEnvironmentVariables, withMockApi, setTTYMode, setCI, setTestingPrompts } from '../../utils/mock-api.js' import { runMockProgram } from '../../utils/mock-program.js' import { routes } from './api-routes.js' @@ -280,12 +280,18 @@ describe('env:set command', () => { }) describe('user is prompted to confirmOverwrite when setting an env var that already exists', () => { + beforeAll(() => { + setTestingPrompts('true') + }) + beforeEach(() => { - setTTYMode(true) - setCI('') vi.resetAllMocks() }) + afterAll(() => { + setTestingPrompts('false') + }) + test('should log warnings and prompts if enviroment variable already exists', async () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) @@ -369,6 +375,7 @@ describe('env:set command', () => { beforeEach(() => { vi.resetAllMocks() }) + afterEach(() => { setTTYMode(true) setCI('') diff --git a/tests/integration/commands/env/env-unset.test.ts b/tests/integration/commands/env/env-unset.test.ts index aad03cf307b..c2ea9dee8b4 100644 --- a/tests/integration/commands/env/env-unset.test.ts +++ b/tests/integration/commands/env/env-unset.test.ts @@ -2,12 +2,12 @@ import process from 'process' import chalk from 'chalk' import inquirer from 'inquirer' -import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest' +import { describe, expect, test, vi, beforeEach, afterEach, beforeAll, afterAll } from 'vitest' import { log } from '../../../../src/utils/command-helpers.js' import { destructiveCommandMessages } from '../../../../src/utils/prompts/prompt-messages.js' import { FixtureTestContext, setupFixtureTests } from '../../utils/fixture.js' -import { getEnvironmentVariables, withMockApi, setTTYMode, setCI } from '../../utils/mock-api.js' +import { getEnvironmentVariables, withMockApi, setTTYMode, setCI, setTestingPrompts } from '../../utils/mock-api.js' import { routes } from './api-routes.js' import { runMockProgram } from '../../utils/mock-program.js' @@ -86,12 +86,18 @@ describe('env:unset command', () => { }) describe('user is prompted to confirm when unsetting an env var that already exists', () => { + beforeAll(() => { + setTestingPrompts('true') + }) + beforeEach(() => { - setTTYMode(true) - setCI('') vi.resetAllMocks() }) + afterAll(() => { + setTestingPrompts('false') + }) + test('should log warnings and prompts if enviroment variable already exists', async () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) diff --git a/tests/integration/utils/mock-api.js b/tests/integration/utils/mock-api.js index eaf13487cfc..624a9303ba4 100644 --- a/tests/integration/utils/mock-api.js +++ b/tests/integration/utils/mock-api.js @@ -98,6 +98,10 @@ export const setTTYMode = (bool) => { process.stdin.isTTY = bool } +export const setTestingPrompts = (value) => { + process.env.TESTING_PROMPTS = value +} + /** * Simulates a Continuous Integration environment by toggling the `CI` * environment variable. Truthy value is From b2b200d3a2606ad1e024f06fad902ad68dbc47aa Mon Sep 17 00:00:00 2001 From: Will Date: Tue, 29 Oct 2024 17:01:30 -0500 Subject: [PATCH 35/39] fix: updated types and env variables not being restored after tests Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> --- package-lock.json | 16 +++--- src/commands/env/env-clone.ts | 10 +++- .../commands/blobs/blobs-delete.test.ts | 50 ++++++++--------- .../commands/blobs/blobs-set.test.ts | 45 +++++++++------- .../commands/env/env-clone.test.ts | 48 ++++++++--------- .../integration/commands/env/env-set.test.ts | 53 ++++++++++--------- .../commands/env/env-unset.test.ts | 53 ++++++++++--------- tests/integration/commands/env/env.test.js | 1 + .../commands/integration/deploy.test.ts | 19 ++++++- .../integration/utils/inquirer-mock-prompt.ts | 23 ++++++++ tests/integration/utils/mock-api.js | 8 +++ 11 files changed, 197 insertions(+), 129 deletions(-) create mode 100644 tests/integration/utils/inquirer-mock-prompt.ts diff --git a/package-lock.json b/package-lock.json index 55882ef2137..70a116cb335 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5654,15 +5654,17 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" } }, "node_modules/@types/inquirer/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==", + "dev": true, + "license": "0BSD" }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", @@ -27470,9 +27472,9 @@ } }, "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==", "dev": true } } diff --git a/src/commands/env/env-clone.ts b/src/commands/env/env-clone.ts index aed02756b1f..7901cee341c 100644 --- a/src/commands/env/env-clone.ts +++ b/src/commands/env/env-clone.ts @@ -68,8 +68,16 @@ export const envClone = async (options: OptionValues, command: BaseCommand) => { return false } + const sourceId = options.from || site.id + + if (!sourceId) { + log( + 'Please include the source site Id as the `--from` option, or run `netlify link` to link this folder to a Netlify site', + ) + } + const siteId = { - from: options.from || site.id, + from: sourceId, to: options.to, } diff --git a/tests/integration/commands/blobs/blobs-delete.test.ts b/tests/integration/commands/blobs/blobs-delete.test.ts index 07254bfc3fc..2bc35224863 100644 --- a/tests/integration/commands/blobs/blobs-delete.test.ts +++ b/tests/integration/commands/blobs/blobs-delete.test.ts @@ -3,7 +3,7 @@ import process from 'process' import { getStore } from '@netlify/blobs' import chalk from 'chalk' import inquirer from 'inquirer' -import { describe, expect, test, vi, beforeEach, afterEach, beforeAll, afterAll } from 'vitest' +import { describe, expect, test, vi, beforeEach, afterAll } from 'vitest' import { log } from '../../../../src/utils/command-helpers.js' import { destructiveCommandMessages } from '../../../../src/utils/prompts/prompt-messages.js' @@ -11,6 +11,7 @@ import { reportError } from '../../../../src/utils/telemetry/report-error.js' import { Route } from '../../utils/mock-api-vitest.js' import { getEnvironmentVariables, withMockApi, setTTYMode, setCI, setTestingPrompts } from '../../utils/mock-api.js' import { runMockProgram } from '../../utils/mock-program.js' +import { mockPrompt, spyOnMockPrompt } from '../../utils/inquirer-mock-prompt.js' const siteInfo = { account_slug: 'test-account', @@ -46,6 +47,8 @@ const routes: Route[] = [ }, ] +const OLD_ENV = process.env + describe('blobs:delete command', () => { const storeName = 'my-store' const key = 'my-key' @@ -59,17 +62,25 @@ describe('blobs:delete command', () => { storeName, )}` - describe('user is prompted to confirm when deleting a blob key', () => { - beforeAll(() => { - setTestingPrompts('true') - }) + beforeEach(() => { + vi.resetModules() + vi.clearAllMocks() - beforeEach(() => { - vi.resetAllMocks() + Object.defineProperty(process, 'env', { value: {} }) + }) + + afterAll(() => { + vi.resetModules() + vi.restoreAllMocks() + + Object.defineProperty(process, 'env', { + value: OLD_ENV, }) + }) - afterAll(() => { - setTestingPrompts('false') + describe('user is prompted to confirm when deleting a blob key', () => { + beforeEach(() => { + setTestingPrompts('true') }) test('should log warning message and prompt for confirmation', async () => { @@ -82,7 +93,7 @@ describe('blobs:delete command', () => { delete: mockDelete, }) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: true }) + const promptSpy = mockPrompt({ confirm: true }) await runMockProgram(['', '', 'blobs:delete', storeName, key]) @@ -109,7 +120,7 @@ describe('blobs:delete command', () => { delete: mockDelete, }) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: false }) + const promptSpy = mockPrompt({ confirm: false }) try { await runMockProgram(['', '', 'blobs:delete', storeName, key]) @@ -141,7 +152,7 @@ describe('blobs:delete command', () => { delete: mockDelete, }) - const promptSpy = vi.spyOn(inquirer, 'prompt') + const promptSpy = spyOnMockPrompt() await runMockProgram(['', '', 'blobs:delete', storeName, key, '--force']) @@ -166,7 +177,7 @@ describe('blobs:delete command', () => { delete: mockDelete, }) - const promptSpy = vi.spyOn(inquirer, 'prompt') + const promptSpy = spyOnMockPrompt() try { await runMockProgram(['', '', 'blobs:delete', storeName, key, '--force']) @@ -189,15 +200,6 @@ describe('blobs:delete command', () => { }) describe('should not show prompts if in non-interactive shell or CI/CD', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - - afterEach(() => { - setTTYMode(true) - setCI('') - }) - test('should not show prompt for non-interactive shell', async () => { setTTYMode(false) @@ -210,7 +212,7 @@ describe('blobs:delete command', () => { delete: mockDelete, }) - const promptSpy = vi.spyOn(inquirer, 'prompt') + const promptSpy = spyOnMockPrompt() await runMockProgram(['', '', 'blobs:delete', storeName, key]) expect(promptSpy).not.toHaveBeenCalled() @@ -232,7 +234,7 @@ describe('blobs:delete command', () => { delete: mockDelete, }) - const promptSpy = vi.spyOn(inquirer, 'prompt') + const promptSpy = spyOnMockPrompt() await runMockProgram(['', '', 'blobs:delete', storeName, key]) diff --git a/tests/integration/commands/blobs/blobs-set.test.ts b/tests/integration/commands/blobs/blobs-set.test.ts index a720673cf37..2e0436da83d 100644 --- a/tests/integration/commands/blobs/blobs-set.test.ts +++ b/tests/integration/commands/blobs/blobs-set.test.ts @@ -3,7 +3,7 @@ import process from 'process' import { getStore } from '@netlify/blobs' import chalk from 'chalk' import inquirer from 'inquirer' -import { describe, expect, test, vi, beforeEach, afterEach, beforeAll, afterAll } from 'vitest' +import { describe, expect, test, vi, beforeEach, afterAll } from 'vitest' import { log } from '../../../../src/utils/command-helpers.js' import { destructiveCommandMessages } from '../../../../src/utils/prompts/prompt-messages.js' @@ -11,6 +11,7 @@ import { reportError } from '../../../../src/utils/telemetry/report-error.js' import { Route } from '../../utils/mock-api-vitest.js' import { getEnvironmentVariables, withMockApi, setTTYMode, setCI, setTestingPrompts } from '../../utils/mock-api.js' import { runMockProgram } from '../../utils/mock-program.js' +import { mockPrompt, spyOnMockPrompt } from '../../utils/inquirer-mock-prompt.js' const siteInfo = { account_slug: 'test-account', @@ -45,6 +46,8 @@ const routes: Route[] = [ }, ] +const OLD_ENV = process.env + describe('blobs:set command', () => { const storeName = 'my-store' const key = 'my-key' @@ -60,17 +63,25 @@ describe('blobs:set command', () => { storeName, )}` - describe('user is prompted to confirm when setting a a blob key that already exists', () => { - beforeAll(() => { - setTestingPrompts('true') - }) + beforeEach(() => { + vi.resetModules() + vi.clearAllMocks() - beforeEach(() => { - vi.resetAllMocks() + Object.defineProperty(process, 'env', { value: {} }) + }) + + afterAll(() => { + vi.resetModules() + vi.restoreAllMocks() + + Object.defineProperty(process, 'env', { + value: OLD_ENV, }) + }) - afterAll(() => { - setTestingPrompts('false') + describe('user is prompted to confirm when setting a a blob key that already exists', () => { + beforeEach(() => { + setTestingPrompts('true') }) test('should not log warnings and prompt if blob key does not exist', async () => { @@ -109,7 +120,7 @@ describe('blobs:set command', () => { set: mockSet, }) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: true }) + const promptSpy = mockPrompt({ confirm: true }) await runMockProgram(['', '', 'blobs:set', storeName, key, newValue]) @@ -139,7 +150,7 @@ describe('blobs:set command', () => { set: mockSet, }) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: false }) + const promptSpy = mockPrompt({ confirm: false }) try { await runMockProgram(['', '', 'blobs:set', storeName, key, newValue]) @@ -174,7 +185,7 @@ describe('blobs:set command', () => { set: mockSet, }) - const promptSpy = vi.spyOn(inquirer, 'prompt') + const promptSpy = spyOnMockPrompt() await runMockProgram(['', '', 'blobs:set', storeName, key, newValue, '--force']) @@ -196,7 +207,7 @@ describe('blobs:set command', () => { set: mockSet, }) - const promptSpy = vi.spyOn(inquirer, 'prompt') + const promptSpy = spyOnMockPrompt() try { await runMockProgram(['', '', 'blobs:set', storeName, key, newValue, '--force']) @@ -214,10 +225,6 @@ describe('blobs:set command', () => { }) describe('prompts should not show in a non-interactive shell or in a ci/cd enviroment', () => { - afterEach(() => { - setTTYMode(true) - setCI('') - }) test('should not show prompt in an non-interactive shell', async () => { setTTYMode(false) @@ -233,7 +240,7 @@ describe('blobs:set command', () => { set: mockSet, }) - const promptSpy = vi.spyOn(inquirer, 'prompt') + const promptSpy = spyOnMockPrompt() await runMockProgram(['', '', 'blobs:set', storeName, key, newValue, '--force']) @@ -259,7 +266,7 @@ describe('blobs:set command', () => { set: mockSet, }) - const promptSpy = vi.spyOn(inquirer, 'prompt') + const promptSpy = spyOnMockPrompt() await runMockProgram(['', '', 'blobs:set', storeName, key, newValue, '--force']) expect(promptSpy).not.toHaveBeenCalled() diff --git a/tests/integration/commands/env/env-clone.test.ts b/tests/integration/commands/env/env-clone.test.ts index 02b3200c5ad..d4aec9ebf6c 100644 --- a/tests/integration/commands/env/env-clone.test.ts +++ b/tests/integration/commands/env/env-clone.test.ts @@ -1,7 +1,6 @@ import process from 'process' import chalk from 'chalk' -import inquirer from 'inquirer' import { describe, expect, test, vi, beforeEach, afterEach, beforeAll, afterAll } from 'vitest' import { log } from '../../../../src/utils/command-helpers.js' @@ -11,11 +10,13 @@ import { getEnvironmentVariables, withMockApi, setTTYMode, setCI, setTestingProm import { existingVar, routes, secondSiteInfo } from './api-routes.js' import { runMockProgram } from '../../utils/mock-program.js' +import { mockPrompt, spyOnMockPrompt } from '../../utils/inquirer-mock-prompt.js' vi.mock('../../../../src/utils/command-helpers.js', async () => ({ ...(await vi.importActual('../../../../src/utils/command-helpers.js')), log: vi.fn(), })) +const OLD_ENV = process.env describe('env:clone command', () => { const sharedEnvVars = [existingVar, existingVar] @@ -31,24 +32,32 @@ describe('env:clone command', () => { 'site-name-2', )}` - describe('user is prompted to confirm when setting an env var that already exists', () => { - beforeAll(() => { - setTestingPrompts('true') - }) + beforeEach(() => { + vi.resetModules() + vi.clearAllMocks() - beforeEach(() => { - vi.resetAllMocks() + Object.defineProperty(process, 'env', { value: {} }) + }) + + afterAll(() => { + vi.resetModules() + vi.restoreAllMocks() + + Object.defineProperty(process, 'env', { + value: OLD_ENV, }) + }) - afterAll(() => { - setTestingPrompts('false') + describe('user is prompted to confirm when setting an env var that already exists', () => { + beforeEach(() => { + setTestingPrompts('true') }) test('should log warnings and prompts if enviroment variable already exists', async () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: true }) + const promptSpy = mockPrompt({ confirm: true }) await runMockProgram(['', '', 'env:clone', '-t', siteIdTwo]) @@ -73,7 +82,7 @@ describe('env:clone command', () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const promptSpy = vi.spyOn(inquirer, 'prompt') + const promptSpy = spyOnMockPrompt() await runMockProgram(['', '', 'env:clone', '--force', '-t', siteIdTwo]) @@ -93,7 +102,7 @@ describe('env:clone command', () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: false }) + const promptSpy = mockPrompt({ confirm: false }) try { await runMockProgram(['', '', 'env:clone', '-t', siteIdTwo]) @@ -121,7 +130,7 @@ describe('env:clone command', () => { 'site-name', )} to ${chalk.green('site-name-3')}` - const promptSpy = vi.spyOn(inquirer, 'prompt') + const promptSpy = spyOnMockPrompt() await runMockProgram(['', '', 'env:clone', '-t', 'site_id_3']) @@ -139,22 +148,13 @@ describe('env:clone command', () => { }) describe('should not run prompts if in non-interactive shell or CI/CD environment', async () => { - beforeEach(() => { - vi.resetAllMocks() - }) - - afterEach(() => { - setTTYMode(true) - setCI('') - }) - test('should not show prompt in an non-interactive shell', async () => { setTTYMode(false) await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const promptSpy = vi.spyOn(inquirer, 'prompt') + const promptSpy = spyOnMockPrompt() await runMockProgram(['', '', 'env:clone', '-t', siteIdTwo]) @@ -172,7 +172,7 @@ describe('env:clone command', () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const promptSpy = vi.spyOn(inquirer, 'prompt') + const promptSpy = spyOnMockPrompt() await runMockProgram(['', '', 'env:clone', '-t', siteIdTwo]) diff --git a/tests/integration/commands/env/env-set.test.ts b/tests/integration/commands/env/env-set.test.ts index 948ba7d1377..3df884489b7 100644 --- a/tests/integration/commands/env/env-set.test.ts +++ b/tests/integration/commands/env/env-set.test.ts @@ -1,8 +1,7 @@ import process from 'process' import chalk from 'chalk' -import inquirer from 'inquirer' -import { describe, expect, test, vi, beforeEach, afterEach, beforeAll, afterAll } from 'vitest' +import { describe, expect, test, vi, beforeEach, afterAll } from 'vitest' import { log } from '../../../../src/utils/command-helpers.js' import { destructiveCommandMessages } from '../../../../src/utils/prompts/prompt-messages.js' @@ -10,12 +9,15 @@ import { FixtureTestContext, setupFixtureTests } from '../../utils/fixture.js' import { getEnvironmentVariables, withMockApi, setTTYMode, setCI, setTestingPrompts } from '../../utils/mock-api.js' import { runMockProgram } from '../../utils/mock-program.js' import { routes } from './api-routes.js' +import { mockPrompt, spyOnMockPrompt } from '../../utils/inquirer-mock-prompt.js' vi.mock('../../../../src/utils/command-helpers.js', async () => ({ ...(await vi.importActual('../../../../src/utils/command-helpers.js')), log: vi.fn(), })) +const OLD_ENV = process.env + describe('env:set command', () => { // already exists as value in withMockApi const existingVar = 'EXISTING_VAR' @@ -29,6 +31,22 @@ describe('env:set command', () => { `${existingVar}=${newEnvValue}`, )} in the ${chalk.magenta('all')} context` + beforeEach(() => { + vi.resetModules() + vi.clearAllMocks() + + Object.defineProperty(process, 'env', { value: {} }) + }) + + afterAll(() => { + vi.resetModules() + vi.restoreAllMocks() + + Object.defineProperty(process, 'env', { + value: OLD_ENV, + }) + }) + setupFixtureTests('empty-project', { mockApi: { routes } }, () => { test('should create and return new var in the dev context', async ({ fixture, mockApi }) => { const cliResponse = await fixture.callCli( @@ -280,23 +298,15 @@ describe('env:set command', () => { }) describe('user is prompted to confirmOverwrite when setting an env var that already exists', () => { - beforeAll(() => { - setTestingPrompts('true') - }) - beforeEach(() => { - vi.resetAllMocks() - }) - - afterAll(() => { - setTestingPrompts('false') + setTestingPrompts('true') }) test('should log warnings and prompts if enviroment variable already exists', async () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: true }) + const promptSpy = mockPrompt({ confirm: true }) await runMockProgram(['', '', 'env:set', existingVar, newEnvValue]) @@ -317,7 +327,7 @@ describe('env:set command', () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const promptSpy = vi.spyOn(inquirer, 'prompt') + const promptSpy = spyOnMockPrompt() await runMockProgram(['', '', 'env:set', 'NEW_ENV_VAR', 'NEW_VALUE']) @@ -337,7 +347,7 @@ describe('env:set command', () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const promptSpy = vi.spyOn(inquirer, 'prompt') + const promptSpy = spyOnMockPrompt() await runMockProgram(['', '', 'env:set', existingVar, newEnvValue, '--force']) @@ -353,7 +363,7 @@ describe('env:set command', () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: false }) + const promptSpy = mockPrompt({ confirm: false }) try { await runMockProgram(['', '', 'env:set', existingVar, newEnvValue]) @@ -372,22 +382,13 @@ describe('env:set command', () => { }) describe('prompts should not show in an non-interactive shell or in a ci/cd enviroment', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - - afterEach(() => { - setTTYMode(true) - setCI('') - }) - test('should not show prompt in an non-interactive shell', async () => { setTTYMode(false) await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const promptSpy = vi.spyOn(inquirer, 'prompt') + const promptSpy = spyOnMockPrompt() await runMockProgram(['', '', 'env:set', existingVar, newEnvValue]) @@ -405,7 +406,7 @@ describe('env:set command', () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const promptSpy = vi.spyOn(inquirer, 'prompt') + const promptSpy = spyOnMockPrompt() await runMockProgram(['', '', 'env:set', existingVar, newEnvValue]) expect(promptSpy).not.toHaveBeenCalled() diff --git a/tests/integration/commands/env/env-unset.test.ts b/tests/integration/commands/env/env-unset.test.ts index c2ea9dee8b4..d5c4ea2253b 100644 --- a/tests/integration/commands/env/env-unset.test.ts +++ b/tests/integration/commands/env/env-unset.test.ts @@ -1,8 +1,7 @@ import process from 'process' import chalk from 'chalk' -import inquirer from 'inquirer' -import { describe, expect, test, vi, beforeEach, afterEach, beforeAll, afterAll } from 'vitest' +import { describe, expect, test, vi, beforeEach, afterAll } from 'vitest' import { log } from '../../../../src/utils/command-helpers.js' import { destructiveCommandMessages } from '../../../../src/utils/prompts/prompt-messages.js' @@ -11,12 +10,15 @@ import { getEnvironmentVariables, withMockApi, setTTYMode, setCI, setTestingProm import { routes } from './api-routes.js' import { runMockProgram } from '../../utils/mock-program.js' +import { mockPrompt, spyOnMockPrompt } from '../../utils/inquirer-mock-prompt.js' vi.mock('../../../../src/utils/command-helpers.js', async () => ({ ...(await vi.importActual('../../../../src/utils/command-helpers.js')), log: vi.fn(), })) +const OLD_ENV = process.env + describe('env:unset command', () => { const { overwriteNotice } = destructiveCommandMessages const { generateWarning, overwriteConfirmation } = destructiveCommandMessages.envUnset @@ -28,6 +30,22 @@ describe('env:unset command', () => { 'all', )} context` + beforeEach(() => { + vi.resetModules() + vi.clearAllMocks() + + Object.defineProperty(process, 'env', { value: {} }) + }) + + afterAll(() => { + vi.resetModules() + vi.restoreAllMocks() + + Object.defineProperty(process, 'env', { + value: OLD_ENV, + }) + }) + setupFixtureTests('empty-project', { mockApi: { routes } }, () => { test('should remove existing variable', async ({ fixture, mockApi }) => { const cliResponse = await fixture.callCli(['env:unset', '--json', 'EXISTING_VAR', '--force'], { @@ -86,23 +104,15 @@ describe('env:unset command', () => { }) describe('user is prompted to confirm when unsetting an env var that already exists', () => { - beforeAll(() => { - setTestingPrompts('true') - }) - beforeEach(() => { - vi.resetAllMocks() - }) - - afterAll(() => { - setTestingPrompts('false') + setTestingPrompts('true') }) test('should log warnings and prompts if enviroment variable already exists', async () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: true }) + const promptSpy = mockPrompt({ confirm: true }) await runMockProgram(['', '', 'env:unset', existingVar]) @@ -123,7 +133,7 @@ describe('env:unset command', () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const promptSpy = vi.spyOn(inquirer, 'prompt') + const promptSpy = spyOnMockPrompt() await runMockProgram(['', '', 'env:unset', existingVar, '--force']) @@ -139,7 +149,7 @@ describe('env:unset command', () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ confirm: false }) + const promptSpy = mockPrompt({ confirm: false }) try { await runMockProgram(['', '', 'env:unset', existingVar]) @@ -160,7 +170,7 @@ describe('env:unset command', () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const promptSpy = vi.spyOn(inquirer, 'prompt') + const promptSpy = spyOnMockPrompt() await runMockProgram(['', '', 'env:unset', 'NEW_ENV_VAR']) @@ -174,22 +184,13 @@ describe('env:unset command', () => { }) describe('prompts should not show in an non-interactive shell or in a ci/cd enviroment', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - - afterEach(() => { - setTTYMode(true) - setCI('') - }) - test('prompts should not show in an non-interactive shell', async () => { setTTYMode(false) await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const promptSpy = vi.spyOn(inquirer, 'prompt') + const promptSpy = spyOnMockPrompt() await runMockProgram(['', '', 'env:unset', existingVar]) expect(promptSpy).not.toHaveBeenCalled() @@ -206,7 +207,7 @@ describe('env:unset command', () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const promptSpy = vi.spyOn(inquirer, 'prompt') + const promptSpy = spyOnMockPrompt() await runMockProgram(['', '', 'env:unset', existingVar]) expect(promptSpy).not.toHaveBeenCalled() diff --git a/tests/integration/commands/env/env.test.js b/tests/integration/commands/env/env.test.js index ad2f71486d5..84353d0f837 100644 --- a/tests/integration/commands/env/env.test.js +++ b/tests/integration/commands/env/env.test.js @@ -446,6 +446,7 @@ describe('commands/env', () => { extendEnv: false, PATH: process.env.PATH, }) + t.expect(normalize(cliResponse)).toMatchSnapshot() }) }) diff --git a/tests/integration/commands/integration/deploy.test.ts b/tests/integration/commands/integration/deploy.test.ts index ee0972b4f08..133a956c180 100644 --- a/tests/integration/commands/integration/deploy.test.ts +++ b/tests/integration/commands/integration/deploy.test.ts @@ -1,6 +1,6 @@ import process from 'process' -import { beforeEach, describe, expect, test, vi } from 'vitest' +import { afterAll, beforeEach, describe, expect, test, vi } from 'vitest' import BaseCommand from '../../../../src/commands/base-command.js' import { deploy as siteDeploy } from '../../../../src/commands/deploy/deploy.js' @@ -24,10 +24,25 @@ describe('integration:deploy areScopesEqual', () => { }) }) +const OLD_ENV = process.env + describe(`integration:deploy`, () => { beforeEach(() => { - vi.resetAllMocks() + vi.resetModules() + vi.clearAllMocks() + + Object.defineProperty(process, 'env', { value: {} }) }) + + afterAll(() => { + vi.resetModules() + vi.restoreAllMocks() + + Object.defineProperty(process, 'env', { + value: OLD_ENV, + }) + }) + test('deploys an integration', async (t) => { vi.mock(`../../../../src/commands/deploy/deploy.js`, () => ({ deploy: vi.fn(() => console.log(`yay it was mocked!`)), diff --git a/tests/integration/utils/inquirer-mock-prompt.ts b/tests/integration/utils/inquirer-mock-prompt.ts new file mode 100644 index 00000000000..e4121f886fc --- /dev/null +++ b/tests/integration/utils/inquirer-mock-prompt.ts @@ -0,0 +1,23 @@ +// tests/utils/inquirer-mock.ts +import inquirer from 'inquirer' +import { vi } from 'vitest' + +export const mockPrompt = (response = { confirm: true }) => { + // Create the mock function + const mockFn = vi.fn().mockResolvedValue(response) + + // Preserve the original properties of inquirer.prompt + Object.assign(mockFn, inquirer.prompt) + + // Create the spy with our prepared mock + const spy = vi.spyOn(inquirer, 'prompt').mockImplementation(mockFn) + + inquirer.registerPrompt = vi.fn() + inquirer.prompt.registerPrompt = vi.fn() + + return spy +} + +export const spyOnMockPrompt = () => { + return vi.spyOn(inquirer, 'prompt') +} diff --git a/tests/integration/utils/mock-api.js b/tests/integration/utils/mock-api.js index 624a9303ba4..ae3b27710a2 100644 --- a/tests/integration/utils/mock-api.js +++ b/tests/integration/utils/mock-api.js @@ -98,6 +98,14 @@ export const setTTYMode = (bool) => { process.stdin.isTTY = bool } +/** + * Sets the `TESTING_PROMPTS` environment variable to the specified value. + * This is used to make sure prompts are shown in the needed test sin ci/cd enviroments + * If this is set to 'true', then prompts will be shown in for destructive commands even in non-interactive shells + * or CI/CD enviroment + * + * @param {string} value - The value to set for the `TESTING_PROMPTS` environment variable. + */ export const setTestingPrompts = (value) => { process.env.TESTING_PROMPTS = value } From c929fccfdf99a5f0c99b51e87558deca15a554ea Mon Sep 17 00:00:00 2001 From: Will Date: Tue, 29 Oct 2024 19:47:17 -0500 Subject: [PATCH 36/39] fix: fixed tests Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> --- src/utils/scripted-commands.ts | 9 +- .../commands/blobs/blobs-delete.test.ts | 269 +++++++------- .../commands/blobs/blobs-set.test.ts | 340 +++++++++--------- tests/integration/commands/dev/v2-api.test.ts | 2 +- .../commands/env/env-clone.test.ts | 231 ++++++------ .../integration/commands/env/env-set.test.ts | 214 +++++------ .../commands/env/env-unset.test.ts | 204 +++++------ .../commands/integration/deploy.test.ts | 3 +- 8 files changed, 641 insertions(+), 631 deletions(-) diff --git a/src/utils/scripted-commands.ts b/src/utils/scripted-commands.ts index 9227dc8fcfe..d9bbf29d6da 100644 --- a/src/utils/scripted-commands.ts +++ b/src/utils/scripted-commands.ts @@ -4,17 +4,18 @@ import { isCI } from 'ci-info' export const shouldForceFlagBeInjected = (argv: string[]): boolean => { // Is the command run in a non-interactive shell or CI/CD environment? const scriptedCommand = Boolean(!process.stdin.isTTY || isCI || process.env.CI) + // Is the `--force` flag not already present? const noForceFlag = !argv.includes('--force') + // ENV Variable used to tests prompts in CI/CD enviroment + const testingPrompts = process.env.TESTING_PROMPTS !== 'true' + // Prevents prompts from blocking scripted commands - return Boolean(scriptedCommand && noForceFlag) + return Boolean(scriptedCommand && testingPrompts && noForceFlag) } export const injectForceFlagIfScripted = (argv: string[]) => { - // ENV Variable used to tests prompts in CI/CD enviroment - if (process.env.TESTING_PROMPTS === 'true') return - if (shouldForceFlagBeInjected(argv)) { argv.push('--force') } diff --git a/tests/integration/commands/blobs/blobs-delete.test.ts b/tests/integration/commands/blobs/blobs-delete.test.ts index 2bc35224863..a728e4da707 100644 --- a/tests/integration/commands/blobs/blobs-delete.test.ts +++ b/tests/integration/commands/blobs/blobs-delete.test.ts @@ -2,11 +2,10 @@ import process from 'process' import { getStore } from '@netlify/blobs' import chalk from 'chalk' -import inquirer from 'inquirer' import { describe, expect, test, vi, beforeEach, afterAll } from 'vitest' import { log } from '../../../../src/utils/command-helpers.js' -import { destructiveCommandMessages } from '../../../../src/utils/prompts/prompt-messages.js' +import { destructiveCommandMessages } from '../.././../../src/utils/prompts/prompt-messages.js' import { reportError } from '../../../../src/utils/telemetry/report-error.js' import { Route } from '../../utils/mock-api-vitest.js' import { getEnvironmentVariables, withMockApi, setTTYMode, setCI, setTestingPrompts } from '../../utils/mock-api.js' @@ -50,128 +49,104 @@ const routes: Route[] = [ const OLD_ENV = process.env describe('blobs:delete command', () => { - const storeName = 'my-store' - const key = 'my-key' + describe('prompt messages for blobs:delete command', () => { + const storeName = 'my-store' + const key = 'my-key' - const { overwriteNotice } = destructiveCommandMessages - const { generateWarning, overwriteConfirmation } = destructiveCommandMessages.blobDelete + const { overwriteNotice } = destructiveCommandMessages + const { generateWarning, overwriteConfirmation } = destructiveCommandMessages.blobDelete - const warningMessage = generateWarning(key, storeName) + const warningMessage = generateWarning(key, storeName) - const successMessage = `${chalk.greenBright('Success')}: Blob ${chalk.yellow(key)} deleted from store ${chalk.yellow( - storeName, - )}` + const successMessage = `${chalk.greenBright('Success')}: Blob ${chalk.yellow( + key, + )} deleted from store ${chalk.yellow(storeName)}` - beforeEach(() => { - vi.resetModules() - vi.clearAllMocks() - - Object.defineProperty(process, 'env', { value: {} }) - }) - - afterAll(() => { - vi.resetModules() - vi.restoreAllMocks() - - Object.defineProperty(process, 'env', { - value: OLD_ENV, - }) - }) - - describe('user is prompted to confirm when deleting a blob key', () => { beforeEach(() => { - setTestingPrompts('true') - }) - - test('should log warning message and prompt for confirmation', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - - const mockDelete = vi.fn().mockResolvedValue('true') - - ;(getStore as any).mockReturnValue({ - delete: mockDelete, - }) - - const promptSpy = mockPrompt({ confirm: true }) + vi.resetModules() + vi.clearAllMocks() - await runMockProgram(['', '', 'blobs:delete', storeName, key]) + Object.defineProperty(process, 'env', { value: {} }) + }) - expect(promptSpy).toHaveBeenCalledWith({ - type: 'confirm', - name: 'confirm', - message: expect.stringContaining(overwriteConfirmation), - default: false, - }) + afterAll(() => { + vi.resetModules() + vi.restoreAllMocks() - expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(overwriteNotice) - expect(log).toHaveBeenCalledWith(successMessage) + Object.defineProperty(process, 'env', { + value: OLD_ENV, }) }) - test('should exit if user responds with no to confirmation prompt', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + describe('user is prompted to confirm when deleting a blob key', () => { + beforeEach(() => { + setTestingPrompts('true') + }) - const mockDelete = vi.fn().mockResolvedValue('true') + test('should log warning message and prompt for confirmation', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - ;(getStore as any).mockReturnValue({ - delete: mockDelete, - }) + const mockDelete = vi.fn().mockResolvedValue('true') - const promptSpy = mockPrompt({ confirm: false }) + ;(getStore as any).mockReturnValue({ + delete: mockDelete, + }) + + const promptSpy = mockPrompt({ confirm: true }) - try { await runMockProgram(['', '', 'blobs:delete', storeName, key]) - } catch (error) { - // We expect the process to exit, so this is fine - expect(error.message).toContain('process.exit unexpectedly called') - } - expect(promptSpy).toHaveBeenCalledWith({ - type: 'confirm', - name: 'confirm', - message: expect.stringContaining(overwriteConfirmation), - default: false, - }) + expect(promptSpy).toHaveBeenCalledWith({ + type: 'confirm', + name: 'confirm', + message: expect.stringContaining(overwriteConfirmation), + default: false, + }) - expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(overwriteNotice) - expect(log).not.toHaveBeenCalledWith(successMessage) + expect(log).toHaveBeenCalledWith(warningMessage) + expect(log).toHaveBeenCalledWith(overwriteNotice) + expect(log).toHaveBeenCalledWith(successMessage) + }) }) - }) - test('should not log warning message and prompt for confirmation if --force flag is passed', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + test('should exit if user responds with no to confirmation prompt', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const mockDelete = vi.fn().mockResolvedValue('true') + const mockDelete = vi.fn().mockResolvedValue('true') - ;(getStore as any).mockReturnValue({ - delete: mockDelete, - }) + ;(getStore as any).mockReturnValue({ + delete: mockDelete, + }) - const promptSpy = spyOnMockPrompt() + const promptSpy = mockPrompt({ confirm: false }) - await runMockProgram(['', '', 'blobs:delete', storeName, key, '--force']) + try { + await runMockProgram(['', '', 'blobs:delete', storeName, key]) + } catch (error) { + // We expect the process to exit, so this is fine + expect(error.message).toContain('process.exit unexpectedly called') + } - expect(promptSpy).not.toHaveBeenCalled() + expect(promptSpy).toHaveBeenCalledWith({ + type: 'confirm', + name: 'confirm', + message: expect.stringContaining(overwriteConfirmation), + default: false, + }) - expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNotice) - expect(log).toHaveBeenCalledWith(successMessage) + expect(log).toHaveBeenCalledWith(warningMessage) + expect(log).toHaveBeenCalledWith(overwriteNotice) + expect(log).not.toHaveBeenCalledWith(successMessage) + }) }) - }) - test('should log error message if delete fails', async () => { - try { + test('should not log warning message and prompt for confirmation if --force flag is passed', async () => { await withMockApi(routes, async ({ apiUrl }) => { Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - vi.mocked(reportError).mockResolvedValue() - - const mockDelete = vi.fn().mockRejectedValue(new Error('Could not delete blob')) + const mockDelete = vi.fn().mockResolvedValue('true') ;(getStore as any).mockReturnValue({ delete: mockDelete, @@ -179,70 +154,96 @@ describe('blobs:delete command', () => { const promptSpy = spyOnMockPrompt() - try { - await runMockProgram(['', '', 'blobs:delete', storeName, key, '--force']) - } catch (error) { - expect(error.message).toContain( - `Could not delete blob ${chalk.yellow(key)} from store ${chalk.yellow(storeName)}`, - ) - } + await runMockProgram(['', '', 'blobs:delete', storeName, key, '--force']) expect(promptSpy).not.toHaveBeenCalled() expect(log).not.toHaveBeenCalledWith(warningMessage) expect(log).not.toHaveBeenCalledWith(overwriteNotice) - expect(log).not.toHaveBeenCalledWith(successMessage) + expect(log).toHaveBeenCalledWith(successMessage) }) - } catch (error) { - console.error(error) - } - }) - }) + }) - describe('should not show prompts if in non-interactive shell or CI/CD', () => { - test('should not show prompt for non-interactive shell', async () => { - setTTYMode(false) + test('should log error message if delete fails', async () => { + try { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + vi.mocked(reportError).mockResolvedValue() - const mockDelete = vi.fn().mockResolvedValue('true') + const mockDelete = vi.fn().mockRejectedValue(new Error('Could not delete blob')) - ;(getStore as any).mockReturnValue({ - delete: mockDelete, - }) + ;(getStore as any).mockReturnValue({ + delete: mockDelete, + }) + + const promptSpy = spyOnMockPrompt() - const promptSpy = spyOnMockPrompt() + try { + await runMockProgram(['', '', 'blobs:delete', storeName, key, '--force']) + } catch (error) { + expect(error.message).toContain( + `Could not delete blob ${chalk.yellow(key)} from store ${chalk.yellow(storeName)}`, + ) + } - await runMockProgram(['', '', 'blobs:delete', storeName, key]) - expect(promptSpy).not.toHaveBeenCalled() + expect(promptSpy).not.toHaveBeenCalled() - expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNotice) - expect(log).toHaveBeenCalledWith(successMessage) + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) + expect(log).not.toHaveBeenCalledWith(successMessage) + }) + } catch (error) { + console.error(error) + } }) }) - test('should not show prompt for CI/CD', async () => { - setCI(true) - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + describe('should not show prompts if in non-interactive shell or CI/CD', () => { + test('should not show prompt for non-interactive shell', async () => { + setTTYMode(false) + + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const mockDelete = vi.fn().mockResolvedValue('true') + const mockDelete = vi.fn().mockResolvedValue('true') - ;(getStore as any).mockReturnValue({ - delete: mockDelete, + ;(getStore as any).mockReturnValue({ + delete: mockDelete, + }) + + const promptSpy = spyOnMockPrompt() + + await runMockProgram(['', '', 'blobs:delete', storeName, key]) + expect(promptSpy).not.toHaveBeenCalled() + + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) + expect(log).toHaveBeenCalledWith(successMessage) }) + }) - const promptSpy = spyOnMockPrompt() + test('should not show prompt for CI/CD', async () => { + setCI(true) + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - await runMockProgram(['', '', 'blobs:delete', storeName, key]) + const mockDelete = vi.fn().mockResolvedValue('true') - expect(promptSpy).not.toHaveBeenCalled() + ;(getStore as any).mockReturnValue({ + delete: mockDelete, + }) + + const promptSpy = spyOnMockPrompt() + + await runMockProgram(['', '', 'blobs:delete', storeName, key]) - expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNotice) - expect(log).toHaveBeenCalledWith(successMessage) + expect(promptSpy).not.toHaveBeenCalled() + + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) + expect(log).toHaveBeenCalledWith(successMessage) + }) }) }) }) diff --git a/tests/integration/commands/blobs/blobs-set.test.ts b/tests/integration/commands/blobs/blobs-set.test.ts index 2e0436da83d..305c9d6ea18 100644 --- a/tests/integration/commands/blobs/blobs-set.test.ts +++ b/tests/integration/commands/blobs/blobs-set.test.ts @@ -6,7 +6,7 @@ import inquirer from 'inquirer' import { describe, expect, test, vi, beforeEach, afterAll } from 'vitest' import { log } from '../../../../src/utils/command-helpers.js' -import { destructiveCommandMessages } from '../../../../src/utils/prompts/prompt-messages.js' +import { destructiveCommandMessages } from '../.././../../src/utils/prompts/prompt-messages.js' import { reportError } from '../../../../src/utils/telemetry/report-error.js' import { Route } from '../../utils/mock-api-vitest.js' import { getEnvironmentVariables, withMockApi, setTTYMode, setCI, setTestingPrompts } from '../../utils/mock-api.js' @@ -49,231 +49,235 @@ const routes: Route[] = [ const OLD_ENV = process.env describe('blobs:set command', () => { - const storeName = 'my-store' - const key = 'my-key' - const value = 'my-value' - const newValue = 'my-new-value' + describe('prompt messages for blobs:set command', () => { + const storeName = 'my-store' + const key = 'my-key' + const value = 'my-value' + const newValue = 'my-new-value' - const { overwriteNotice } = destructiveCommandMessages - const { generateWarning, overwriteConfirmation } = destructiveCommandMessages.blobSet + const { overwriteNotice } = destructiveCommandMessages + const { generateWarning, overwriteConfirmation } = destructiveCommandMessages.blobSet - const warningMessage = generateWarning(key, storeName) + const warningMessage = generateWarning(key, storeName) - const successMessage = `${chalk.greenBright('Success')}: Blob ${chalk.yellow(key)} set in store ${chalk.yellow( - storeName, - )}` + const successMessage = `${chalk.greenBright('Success')}: Blob ${chalk.yellow(key)} set in store ${chalk.yellow( + storeName, + )}` - beforeEach(() => { - vi.resetModules() - vi.clearAllMocks() - - Object.defineProperty(process, 'env', { value: {} }) - }) - - afterAll(() => { - vi.resetModules() - vi.restoreAllMocks() - - Object.defineProperty(process, 'env', { - value: OLD_ENV, - }) - }) - - describe('user is prompted to confirm when setting a a blob key that already exists', () => { beforeEach(() => { - setTestingPrompts('true') - }) - - test('should not log warnings and prompt if blob key does not exist', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - - const mockGet = vi.fn().mockResolvedValue('') - const mockSet = vi.fn().mockResolvedValue('true') + vi.resetModules() + vi.clearAllMocks() - ;(getStore as any).mockReturnValue({ - get: mockGet, - set: mockSet, - }) - - const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: true }) + Object.defineProperty(process, 'env', { value: {} }) + }) - await runMockProgram(['', '', 'blobs:set', storeName, key, value]) + afterAll(() => { + vi.resetModules() + vi.restoreAllMocks() - expect(promptSpy).not.toHaveBeenCalled() - expect(log).toHaveBeenCalledWith(successMessage) - expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNotice) + Object.defineProperty(process, 'env', { + value: OLD_ENV, }) }) - test('should log warnings and prompt if blob key already exists', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + describe('user is prompted to confirm when setting a a blob key that already exists', () => { + beforeEach(() => { + setTestingPrompts('true') + }) - // Mocking the store.get method to return a value (simulating that the key already exists) - const mockGet = vi.fn().mockResolvedValue(value) - const mockSet = vi.fn().mockResolvedValue('true') + test('should not log warnings and prompt if blob key does not exist', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - ;(getStore as any).mockReturnValue({ - get: mockGet, - set: mockSet, - }) + const mockGet = vi.fn().mockResolvedValue('') + const mockSet = vi.fn().mockResolvedValue('true') - const promptSpy = mockPrompt({ confirm: true }) + ;(getStore as any).mockReturnValue({ + get: mockGet, + set: mockSet, + }) - await runMockProgram(['', '', 'blobs:set', storeName, key, newValue]) + const promptSpy = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ wantsToSet: true }) - expect(promptSpy).toHaveBeenCalledWith({ - type: 'confirm', - name: 'confirm', - message: expect.stringContaining(overwriteConfirmation), - default: false, - }) + await runMockProgram(['', '', 'blobs:set', storeName, key, value]) - expect(log).toHaveBeenCalledWith(successMessage) - expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(overwriteNotice) + expect(promptSpy).not.toHaveBeenCalled() + expect(log).toHaveBeenCalledWith(successMessage) + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) + }) }) - }) - test('should exit if user responds with no to confirmation prompt', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + test('should log warnings and prompt if blob key already exists', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - // Mocking the store.get method to return a value (simulating that the key already exists) - const mockGet = vi.fn().mockResolvedValue('my-value') - const mockSet = vi.fn().mockResolvedValue('true') + // Mocking the store.get method to return a value (simulating that the key already exists) + const mockGet = vi.fn().mockResolvedValue(value) + const mockSet = vi.fn().mockResolvedValue('true') - ;(getStore as any).mockReturnValue({ - get: mockGet, - set: mockSet, - }) + ;(getStore as any).mockReturnValue({ + get: mockGet, + set: mockSet, + }) - const promptSpy = mockPrompt({ confirm: false }) + const promptSpy = mockPrompt({ confirm: true }) - try { await runMockProgram(['', '', 'blobs:set', storeName, key, newValue]) - } catch (error) { - // We expect the process to exit, so this is fine - expect(error.message).toContain('process.exit unexpectedly called with "0"') - } - - expect(promptSpy).toHaveBeenCalledWith({ - type: 'confirm', - name: 'confirm', - message: expect.stringContaining(overwriteConfirmation), - default: false, + + expect(promptSpy).toHaveBeenCalledWith({ + type: 'confirm', + name: 'confirm', + message: expect.stringContaining(overwriteConfirmation), + default: false, + }) + + expect(log).toHaveBeenCalledWith(successMessage) + expect(log).toHaveBeenCalledWith(warningMessage) + expect(log).toHaveBeenCalledWith(overwriteNotice) }) + }) - expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(overwriteNotice) - expect(log).not.toHaveBeenCalledWith(successMessage) + test('should exit if user responds with no to confirmation prompt', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + // Mocking the store.get method to return a value (simulating that the key already exists) + const mockGet = vi.fn().mockResolvedValue('my-value') + const mockSet = vi.fn().mockResolvedValue('true') + + ;(getStore as any).mockReturnValue({ + get: mockGet, + set: mockSet, + }) + + const promptSpy = mockPrompt({ confirm: false }) + + try { + await runMockProgram(['', '', 'blobs:set', storeName, key, newValue]) + } catch (error) { + // We expect the process to exit, so this is fine + expect(error.message).toContain('process.exit unexpectedly called with "0"') + } + + expect(promptSpy).toHaveBeenCalledWith({ + type: 'confirm', + name: 'confirm', + message: expect.stringContaining(overwriteConfirmation), + default: false, + }) + + expect(log).toHaveBeenCalledWith(warningMessage) + expect(log).toHaveBeenCalledWith(overwriteNotice) + expect(log).not.toHaveBeenCalledWith(successMessage) + }) }) - }) - test('should not log warnings and prompt if blob key already exists and --force flag is passed', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + test('should not log warnings and prompt if blob key already exists and --force flag is passed', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - // Mocking the store.get method to return a value (simulating that the key already exists) - const mockGet = vi.fn().mockResolvedValue('my-value') - const mockSet = vi.fn().mockResolvedValue('true') + // Mocking the store.get method to return a value (simulating that the key already exists) + const mockGet = vi.fn().mockResolvedValue('my-value') + const mockSet = vi.fn().mockResolvedValue('true') - ;(getStore as any).mockReturnValue({ - get: mockGet, - set: mockSet, - }) + ;(getStore as any).mockReturnValue({ + get: mockGet, + set: mockSet, + }) - const promptSpy = spyOnMockPrompt() + const promptSpy = spyOnMockPrompt() - await runMockProgram(['', '', 'blobs:set', storeName, key, newValue, '--force']) + await runMockProgram(['', '', 'blobs:set', storeName, key, newValue, '--force']) - expect(promptSpy).not.toHaveBeenCalled() + expect(promptSpy).not.toHaveBeenCalled() - expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNotice) - expect(log).toHaveBeenCalledWith(successMessage) + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) + expect(log).toHaveBeenCalledWith(successMessage) + }) }) - }) - test('should log error message if adding a key fails', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + test('should log error message if adding a key fails', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const mockSet = vi.fn().mockRejectedValue('') - vi.mocked(reportError).mockResolvedValue() - ;(getStore as any).mockReturnValue({ - set: mockSet, - }) + const mockSet = vi.fn().mockRejectedValue('') + vi.mocked(reportError).mockResolvedValue() + ;(getStore as any).mockReturnValue({ + set: mockSet, + }) - const promptSpy = spyOnMockPrompt() + const promptSpy = spyOnMockPrompt() - try { - await runMockProgram(['', '', 'blobs:set', storeName, key, newValue, '--force']) - } catch (error) { - expect(error.message).toContain(`Could not set blob ${chalk.yellow(key)} in store ${chalk.yellow(storeName)}`) - } + try { + await runMockProgram(['', '', 'blobs:set', storeName, key, newValue, '--force']) + } catch (error) { + expect(error.message).toContain( + `Could not set blob ${chalk.yellow(key)} in store ${chalk.yellow(storeName)}`, + ) + } - expect(promptSpy).not.toHaveBeenCalled() + expect(promptSpy).not.toHaveBeenCalled() - expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNotice) - expect(log).not.toHaveBeenCalledWith(successMessage) + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) + expect(log).not.toHaveBeenCalledWith(successMessage) + }) }) }) - }) - describe('prompts should not show in a non-interactive shell or in a ci/cd enviroment', () => { - test('should not show prompt in an non-interactive shell', async () => { - setTTYMode(false) + describe('prompts should not show in a non-interactive shell or in a ci/cd enviroment', () => { + test('should not show prompt in an non-interactive shell', async () => { + setTTYMode(false) - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - // Mocking the store.get method to return a value (simulating that the key already exists) - const mockGet = vi.fn().mockResolvedValue('my-value') - const mockSet = vi.fn().mockResolvedValue('true') + // Mocking the store.get method to return a value (simulating that the key already exists) + const mockGet = vi.fn().mockResolvedValue('my-value') + const mockSet = vi.fn().mockResolvedValue('true') - ;(getStore as any).mockReturnValue({ - get: mockGet, - set: mockSet, - }) + ;(getStore as any).mockReturnValue({ + get: mockGet, + set: mockSet, + }) - const promptSpy = spyOnMockPrompt() + const promptSpy = spyOnMockPrompt() - await runMockProgram(['', '', 'blobs:set', storeName, key, newValue, '--force']) + await runMockProgram(['', '', 'blobs:set', storeName, key, newValue, '--force']) - expect(promptSpy).not.toHaveBeenCalled() + expect(promptSpy).not.toHaveBeenCalled() - expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNotice) - expect(log).toHaveBeenCalledWith(successMessage) + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) + expect(log).toHaveBeenCalledWith(successMessage) + }) }) - }) - test('should not show prompt in a ci/cd environment', async () => { - setCI(true) - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + test('should not show prompt in a ci/cd environment', async () => { + setCI(true) + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - // Mocking the store.get method to return a value (simulating that the key already exists) - const mockGet = vi.fn().mockResolvedValue('my-value') - const mockSet = vi.fn().mockResolvedValue('true') + // Mocking the store.get method to return a value (simulating that the key already exists) + const mockGet = vi.fn().mockResolvedValue('my-value') + const mockSet = vi.fn().mockResolvedValue('true') - ;(getStore as any).mockReturnValue({ - get: mockGet, - set: mockSet, - }) + ;(getStore as any).mockReturnValue({ + get: mockGet, + set: mockSet, + }) - const promptSpy = spyOnMockPrompt() + const promptSpy = spyOnMockPrompt() - await runMockProgram(['', '', 'blobs:set', storeName, key, newValue, '--force']) - expect(promptSpy).not.toHaveBeenCalled() + await runMockProgram(['', '', 'blobs:set', storeName, key, newValue, '--force']) + expect(promptSpy).not.toHaveBeenCalled() - expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNotice) - expect(log).toHaveBeenCalledWith(successMessage) + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) + expect(log).toHaveBeenCalledWith(successMessage) + }) }) }) }) diff --git a/tests/integration/commands/dev/v2-api.test.ts b/tests/integration/commands/dev/v2-api.test.ts index 931acf10975..33267d1434b 100644 --- a/tests/integration/commands/dev/v2-api.test.ts +++ b/tests/integration/commands/dev/v2-api.test.ts @@ -66,7 +66,7 @@ describe.runIf(gte(version, '18.13.0')).concurrent('v2 api', () => { expect(context.requestId).toEqual(response.headers.get('x-nf-request-id')) expect(context.site.url).toEqual(`http://localhost:${devServer.port}`) expect(context.server.region).toEqual('dev') - expect(context.ip).toEqual('::1') + expect(['::1', '127.0.0.1'].includes(context.ip)).toBe(true) expect(context.geo.city).toEqual('Mock City') expect(context.cookies).toEqual({ foo: 'bar' }) expect(context.account.id).toEqual('mock-account-id') diff --git a/tests/integration/commands/env/env-clone.test.ts b/tests/integration/commands/env/env-clone.test.ts index d4aec9ebf6c..42c1d33cafb 100644 --- a/tests/integration/commands/env/env-clone.test.ts +++ b/tests/integration/commands/env/env-clone.test.ts @@ -4,8 +4,8 @@ import chalk from 'chalk' import { describe, expect, test, vi, beforeEach, afterEach, beforeAll, afterAll } from 'vitest' import { log } from '../../../../src/utils/command-helpers.js' -import { generateEnvVarsList } from '../../../../src/utils/prompts/env-clone-prompt.js' -import { destructiveCommandMessages } from '../../../../src/utils/prompts/prompt-messages.js' +import { generateEnvVarsList } from '../.././../../src/utils/prompts/env-clone-prompt.js' +import { destructiveCommandMessages } from '../.././../../src/utils/prompts/prompt-messages.js' import { getEnvironmentVariables, withMockApi, setTTYMode, setCI, setTestingPrompts } from '../../utils/mock-api.js' import { existingVar, routes, secondSiteInfo } from './api-routes.js' @@ -16,171 +16,174 @@ vi.mock('../../../../src/utils/command-helpers.js', async () => ({ ...(await vi.importActual('../../../../src/utils/command-helpers.js')), log: vi.fn(), })) + const OLD_ENV = process.env describe('env:clone command', () => { - const sharedEnvVars = [existingVar, existingVar] - const siteIdTwo = secondSiteInfo.id - - const { overwriteNotice } = destructiveCommandMessages - const { generateWarning, noticeEnvVars, overwriteConfirmation } = destructiveCommandMessages.envClone - - const envVarsList = generateEnvVarsList(sharedEnvVars) - const warningMessage = generateWarning(siteIdTwo) + describe('prompt messages for env:clone', () => { + const sharedEnvVars = [existingVar, existingVar] + const siteIdTwo = secondSiteInfo.id - const successMessage = `Successfully cloned environment variables from ${chalk.green('site-name')} to ${chalk.green( - 'site-name-2', - )}` + const { overwriteNotice } = destructiveCommandMessages + const { generateWarning, noticeEnvVars, overwriteConfirmation } = destructiveCommandMessages.envClone - beforeEach(() => { - vi.resetModules() - vi.clearAllMocks() + const envVarsList = generateEnvVarsList(sharedEnvVars) + const warningMessage = generateWarning(siteIdTwo) - Object.defineProperty(process, 'env', { value: {} }) - }) + const successMessage = `Successfully cloned environment variables from ${chalk.green('site-name')} to ${chalk.green( + 'site-name-2', + )}` - afterAll(() => { - vi.resetModules() - vi.restoreAllMocks() + beforeEach(() => { + vi.resetModules() + vi.clearAllMocks() - Object.defineProperty(process, 'env', { - value: OLD_ENV, + Object.defineProperty(process, 'env', { value: {} }) }) - }) - describe('user is prompted to confirm when setting an env var that already exists', () => { - beforeEach(() => { - setTestingPrompts('true') + afterAll(() => { + vi.resetModules() + vi.restoreAllMocks() + + Object.defineProperty(process, 'env', { + value: OLD_ENV, + }) }) - test('should log warnings and prompts if enviroment variable already exists', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + describe('user is prompted to confirm when setting an env var that already exists', () => { + beforeEach(() => { + setTestingPrompts('true') + }) - const promptSpy = mockPrompt({ confirm: true }) + test('should log warnings and prompts if enviroment variable already exists', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - await runMockProgram(['', '', 'env:clone', '-t', siteIdTwo]) + const promptSpy = mockPrompt({ confirm: true }) - expect(promptSpy).toHaveBeenCalledWith({ - type: 'confirm', - name: 'confirm', - message: expect.stringContaining(overwriteConfirmation), - default: false, - }) + await runMockProgram(['', '', 'env:clone', '-t', siteIdTwo]) - expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(noticeEnvVars) - envVarsList.forEach((envVar) => { - expect(log).toHaveBeenCalledWith(envVar) + expect(promptSpy).toHaveBeenCalledWith({ + type: 'confirm', + name: 'confirm', + message: expect.stringContaining(overwriteConfirmation), + default: false, + }) + + expect(log).toHaveBeenCalledWith(warningMessage) + expect(log).toHaveBeenCalledWith(noticeEnvVars) + envVarsList.forEach((envVar) => { + expect(log).toHaveBeenCalledWith(envVar) + }) + expect(log).toHaveBeenCalledWith(overwriteNotice) + expect(log).toHaveBeenCalledWith(successMessage) }) - expect(log).toHaveBeenCalledWith(overwriteNotice) - expect(log).toHaveBeenCalledWith(successMessage) }) - }) - test('should skip warnings and prompts if --force flag is passed', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + test('should skip warnings and prompts if --force flag is passed', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const promptSpy = spyOnMockPrompt() + const promptSpy = spyOnMockPrompt() - await runMockProgram(['', '', 'env:clone', '--force', '-t', siteIdTwo]) + await runMockProgram(['', '', 'env:clone', '--force', '-t', siteIdTwo]) - expect(promptSpy).not.toHaveBeenCalled() + expect(promptSpy).not.toHaveBeenCalled() - expect(log).not.toHaveBeenCalledWith(warningMessage) - envVarsList.forEach((envVar) => { - expect(log).not.toHaveBeenCalledWith(envVar) + expect(log).not.toHaveBeenCalledWith(warningMessage) + envVarsList.forEach((envVar) => { + expect(log).not.toHaveBeenCalledWith(envVar) + }) + expect(log).not.toHaveBeenCalledWith(noticeEnvVars) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) + expect(log).toHaveBeenCalledWith(successMessage) }) - expect(log).not.toHaveBeenCalledWith(noticeEnvVars) - expect(log).not.toHaveBeenCalledWith(overwriteNotice) - expect(log).toHaveBeenCalledWith(successMessage) }) - }) - test('should exit user reponds is no to confirmatnion prompt', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + test('should exit user reponds is no to confirmatnion prompt', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const promptSpy = mockPrompt({ confirm: false }) + const promptSpy = mockPrompt({ confirm: false }) - try { - await runMockProgram(['', '', 'env:clone', '-t', siteIdTwo]) - } catch (error) { - // We expect the process to exit, so this is fine - expect(error.message).toContain('process.exit unexpectedly called') - } + try { + await runMockProgram(['', '', 'env:clone', '-t', siteIdTwo]) + } catch (error) { + // We expect the process to exit, so this is fine + expect(error.message).toContain('process.exit unexpectedly called') + } - expect(promptSpy).toHaveBeenCalled() + expect(promptSpy).toHaveBeenCalled() - expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(noticeEnvVars) - envVarsList.forEach((envVar) => { - expect(log).toHaveBeenCalledWith(envVar) + expect(log).toHaveBeenCalledWith(warningMessage) + expect(log).toHaveBeenCalledWith(noticeEnvVars) + envVarsList.forEach((envVar) => { + expect(log).toHaveBeenCalledWith(envVar) + }) + expect(log).toHaveBeenCalledWith(overwriteNotice) + expect(log).not.toHaveBeenCalledWith(successMessage) }) - expect(log).toHaveBeenCalledWith(overwriteNotice) - expect(log).not.toHaveBeenCalledWith(successMessage) }) - }) - test('should not run prompts if sites have no enviroment variables in common', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const successMessageSite3 = `Successfully cloned environment variables from ${chalk.green( - 'site-name', - )} to ${chalk.green('site-name-3')}` + test('should not run prompts if sites have no enviroment variables in common', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + const successMessageSite3 = `Successfully cloned environment variables from ${chalk.green( + 'site-name', + )} to ${chalk.green('site-name-3')}` - const promptSpy = spyOnMockPrompt() + const promptSpy = spyOnMockPrompt() - await runMockProgram(['', '', 'env:clone', '-t', 'site_id_3']) + await runMockProgram(['', '', 'env:clone', '-t', 'site_id_3']) - expect(promptSpy).not.toHaveBeenCalled() + expect(promptSpy).not.toHaveBeenCalled() - expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(noticeEnvVars) - envVarsList.forEach((envVar) => { - expect(log).not.toHaveBeenCalledWith(envVar) + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(noticeEnvVars) + envVarsList.forEach((envVar) => { + expect(log).not.toHaveBeenCalledWith(envVar) + }) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) + expect(log).toHaveBeenCalledWith(successMessageSite3) }) - expect(log).not.toHaveBeenCalledWith(overwriteNotice) - expect(log).toHaveBeenCalledWith(successMessageSite3) }) }) - }) - describe('should not run prompts if in non-interactive shell or CI/CD environment', async () => { - test('should not show prompt in an non-interactive shell', async () => { - setTTYMode(false) + describe('should not run prompts if in non-interactive shell or CI/CD environment', async () => { + test('should not show prompt in an non-interactive shell', async () => { + setTTYMode(false) - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const promptSpy = spyOnMockPrompt() + const promptSpy = spyOnMockPrompt() - await runMockProgram(['', '', 'env:clone', '-t', siteIdTwo]) + await runMockProgram(['', '', 'env:clone', '-t', siteIdTwo]) - expect(promptSpy).not.toHaveBeenCalled() + expect(promptSpy).not.toHaveBeenCalled() - expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNotice) - expect(log).toHaveBeenCalledWith(successMessage) + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) + expect(log).toHaveBeenCalledWith(successMessage) + }) }) - }) - test('should not show prompt in a ci/cd enviroment', async () => { - setCI(true) + test('should not show prompt in a ci/cd enviroment', async () => { + setCI(true) - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const promptSpy = spyOnMockPrompt() + const promptSpy = spyOnMockPrompt() - await runMockProgram(['', '', 'env:clone', '-t', siteIdTwo]) + await runMockProgram(['', '', 'env:clone', '-t', siteIdTwo]) - expect(promptSpy).not.toHaveBeenCalled() + expect(promptSpy).not.toHaveBeenCalled() - expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNotice) - expect(log).toHaveBeenCalledWith(successMessage) + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) + expect(log).toHaveBeenCalledWith(successMessage) + }) }) }) }) diff --git a/tests/integration/commands/env/env-set.test.ts b/tests/integration/commands/env/env-set.test.ts index 3df884489b7..933d9071287 100644 --- a/tests/integration/commands/env/env-set.test.ts +++ b/tests/integration/commands/env/env-set.test.ts @@ -4,7 +4,7 @@ import chalk from 'chalk' import { describe, expect, test, vi, beforeEach, afterAll } from 'vitest' import { log } from '../../../../src/utils/command-helpers.js' -import { destructiveCommandMessages } from '../../../../src/utils/prompts/prompt-messages.js' +import { destructiveCommandMessages } from '../.././../../src/utils/prompts/prompt-messages.js' import { FixtureTestContext, setupFixtureTests } from '../../utils/fixture.js' import { getEnvironmentVariables, withMockApi, setTTYMode, setCI, setTestingPrompts } from '../../utils/mock-api.js' import { runMockProgram } from '../../utils/mock-program.js' @@ -19,34 +19,6 @@ vi.mock('../../../../src/utils/command-helpers.js', async () => ({ const OLD_ENV = process.env describe('env:set command', () => { - // already exists as value in withMockApi - const existingVar = 'EXISTING_VAR' - const newEnvValue = 'value' - const { overwriteNotice } = destructiveCommandMessages - const { generateWarning, overwriteConfirmation } = destructiveCommandMessages.envSet - - const warningMessage = generateWarning(existingVar) - - const successMessage = `Set environment variable ${chalk.yellow( - `${existingVar}=${newEnvValue}`, - )} in the ${chalk.magenta('all')} context` - - beforeEach(() => { - vi.resetModules() - vi.clearAllMocks() - - Object.defineProperty(process, 'env', { value: {} }) - }) - - afterAll(() => { - vi.resetModules() - vi.restoreAllMocks() - - Object.defineProperty(process, 'env', { - value: OLD_ENV, - }) - }) - setupFixtureTests('empty-project', { mockApi: { routes } }, () => { test('should create and return new var in the dev context', async ({ fixture, mockApi }) => { const cliResponse = await fixture.callCli( @@ -297,123 +269,151 @@ describe('env:set command', () => { }) }) - describe('user is prompted to confirmOverwrite when setting an env var that already exists', () => { + describe('prompt messages for env:set command', () => { + const existingVar = 'EXISTING_VAR' + const newEnvValue = 'value' + const { overwriteNotice } = destructiveCommandMessages + const { generateWarning, overwriteConfirmation } = destructiveCommandMessages.envSet + + const warningMessage = generateWarning(existingVar) + + const successMessage = `Set environment variable ${chalk.yellow( + `${existingVar}=${newEnvValue}`, + )} in the ${chalk.magenta('all')} context` + beforeEach(() => { - setTestingPrompts('true') + vi.resetModules() + vi.clearAllMocks() + + Object.defineProperty(process, 'env', { value: {} }) }) - test('should log warnings and prompts if enviroment variable already exists', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + afterAll(() => { + vi.resetModules() + vi.restoreAllMocks() - const promptSpy = mockPrompt({ confirm: true }) + Object.defineProperty(process, 'env', { + value: OLD_ENV, + }) + }) + describe('user is prompted to confirmOverwrite when setting an env var that already exists', () => { + beforeEach(() => { + setTestingPrompts('true') + }) - await runMockProgram(['', '', 'env:set', existingVar, newEnvValue]) + test('should log warnings and prompts if enviroment variable already exists', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - expect(promptSpy).toHaveBeenCalledWith({ - type: 'confirm', - name: 'confirm', - message: expect.stringContaining(overwriteConfirmation), - default: false, - }) + const promptSpy = mockPrompt({ confirm: true }) - expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(overwriteNotice) - expect(log).toHaveBeenCalledWith(successMessage) + await runMockProgram(['', '', 'env:set', existingVar, newEnvValue]) + + expect(promptSpy).toHaveBeenCalledWith({ + type: 'confirm', + name: 'confirm', + message: expect.stringContaining(overwriteConfirmation), + default: false, + }) + + expect(log).toHaveBeenCalledWith(warningMessage) + expect(log).toHaveBeenCalledWith(overwriteNotice) + expect(log).toHaveBeenCalledWith(successMessage) + }) }) - }) - test('should skip warnings and prompts if enviroment variable does not exist', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + test('should skip warnings and prompts if enviroment variable does not exist', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const promptSpy = spyOnMockPrompt() + const promptSpy = spyOnMockPrompt() - await runMockProgram(['', '', 'env:set', 'NEW_ENV_VAR', 'NEW_VALUE']) + await runMockProgram(['', '', 'env:set', 'NEW_ENV_VAR', 'NEW_VALUE']) - expect(promptSpy).not.toHaveBeenCalled() + expect(promptSpy).not.toHaveBeenCalled() - expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNotice) - expect(log).toHaveBeenCalledWith( - `Set environment variable ${chalk.yellow(`${'NEW_ENV_VAR'}=${'NEW_VALUE'}`)} in the ${chalk.magenta( - 'all', - )} context`, - ) + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) + expect(log).toHaveBeenCalledWith( + `Set environment variable ${chalk.yellow(`${'NEW_ENV_VAR'}=${'NEW_VALUE'}`)} in the ${chalk.magenta( + 'all', + )} context`, + ) + }) }) - }) - test('should skip warnings and prompts if --force flag is passed', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + test('should skip warnings and prompts if --force flag is passed', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const promptSpy = spyOnMockPrompt() + const promptSpy = spyOnMockPrompt() - await runMockProgram(['', '', 'env:set', existingVar, newEnvValue, '--force']) + await runMockProgram(['', '', 'env:set', existingVar, newEnvValue, '--force']) - expect(promptSpy).not.toHaveBeenCalled() + expect(promptSpy).not.toHaveBeenCalled() - expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNotice) - expect(log).toHaveBeenCalledWith(successMessage) + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) + expect(log).toHaveBeenCalledWith(successMessage) + }) }) - }) - test('should exit user responds is no to confirmatnion prompt', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + test('should exit user responds is no to confirmatnion prompt', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const promptSpy = mockPrompt({ confirm: false }) + const promptSpy = mockPrompt({ confirm: false }) - try { - await runMockProgram(['', '', 'env:set', existingVar, newEnvValue]) - } catch (error) { - // We expect the process to exit, so this is fine - expect(error.message).toContain('process.exit unexpectedly called') - } + try { + await runMockProgram(['', '', 'env:set', existingVar, newEnvValue]) + } catch (error) { + // We expect the process to exit, so this is fine + expect(error.message).toContain('process.exit unexpectedly called') + } - expect(promptSpy).toHaveBeenCalled() + expect(promptSpy).toHaveBeenCalled() - expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(overwriteNotice) - expect(log).not.toHaveBeenCalledWith(successMessage) + expect(log).toHaveBeenCalledWith(warningMessage) + expect(log).toHaveBeenCalledWith(overwriteNotice) + expect(log).not.toHaveBeenCalledWith(successMessage) + }) }) }) - }) - describe('prompts should not show in an non-interactive shell or in a ci/cd enviroment', () => { - test('should not show prompt in an non-interactive shell', async () => { - setTTYMode(false) + describe('prompts should not show in an non-interactive shell or in a ci/cd enviroment', () => { + test('should not show prompt in an non-interactive shell', async () => { + setTTYMode(false) - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const promptSpy = spyOnMockPrompt() + const promptSpy = spyOnMockPrompt() - await runMockProgram(['', '', 'env:set', existingVar, newEnvValue]) + await runMockProgram(['', '', 'env:set', existingVar, newEnvValue]) - expect(promptSpy).not.toHaveBeenCalled() + expect(promptSpy).not.toHaveBeenCalled() - expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNotice) - expect(log).toHaveBeenCalledWith(successMessage) + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) + expect(log).toHaveBeenCalledWith(successMessage) + }) }) - }) - test('should not show prompt in a ci/cd enviroment', async () => { - setCI('true') + test('should not show prompt in a ci/cd enviroment', async () => { + setCI('true') - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const promptSpy = spyOnMockPrompt() - await runMockProgram(['', '', 'env:set', existingVar, newEnvValue]) + const promptSpy = spyOnMockPrompt() + await runMockProgram(['', '', 'env:set', existingVar, newEnvValue]) - expect(promptSpy).not.toHaveBeenCalled() + expect(promptSpy).not.toHaveBeenCalled() - expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNotice) - expect(log).toHaveBeenCalledWith(successMessage) + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) + expect(log).toHaveBeenCalledWith(successMessage) + }) }) }) }) diff --git a/tests/integration/commands/env/env-unset.test.ts b/tests/integration/commands/env/env-unset.test.ts index d5c4ea2253b..86003c4708a 100644 --- a/tests/integration/commands/env/env-unset.test.ts +++ b/tests/integration/commands/env/env-unset.test.ts @@ -4,7 +4,7 @@ import chalk from 'chalk' import { describe, expect, test, vi, beforeEach, afterAll } from 'vitest' import { log } from '../../../../src/utils/command-helpers.js' -import { destructiveCommandMessages } from '../../../../src/utils/prompts/prompt-messages.js' +import { destructiveCommandMessages } from '../.././../../src/utils/prompts/prompt-messages.js' import { FixtureTestContext, setupFixtureTests } from '../../utils/fixture.js' import { getEnvironmentVariables, withMockApi, setTTYMode, setCI, setTestingPrompts } from '../../utils/mock-api.js' @@ -20,32 +20,6 @@ vi.mock('../../../../src/utils/command-helpers.js', async () => ({ const OLD_ENV = process.env describe('env:unset command', () => { - const { overwriteNotice } = destructiveCommandMessages - const { generateWarning, overwriteConfirmation } = destructiveCommandMessages.envUnset - - // already exists as value in withMockApi - const existingVar = 'EXISTING_VAR' - const warningMessage = generateWarning(existingVar) - const expectedSuccessMessage = `Unset environment variable ${chalk.yellow(`${existingVar}`)} in the ${chalk.magenta( - 'all', - )} context` - - beforeEach(() => { - vi.resetModules() - vi.clearAllMocks() - - Object.defineProperty(process, 'env', { value: {} }) - }) - - afterAll(() => { - vi.resetModules() - vi.restoreAllMocks() - - Object.defineProperty(process, 'env', { - value: OLD_ENV, - }) - }) - setupFixtureTests('empty-project', { mockApi: { routes } }, () => { test('should remove existing variable', async ({ fixture, mockApi }) => { const cliResponse = await fixture.callCli(['env:unset', '--json', 'EXISTING_VAR', '--force'], { @@ -103,118 +77,146 @@ describe('env:unset command', () => { }) }) - describe('user is prompted to confirm when unsetting an env var that already exists', () => { + describe('prompt messages for env:unset command', () => { + const { overwriteNotice } = destructiveCommandMessages + const { generateWarning, overwriteConfirmation } = destructiveCommandMessages.envUnset + + // already exists as value in withMockApi + const existingVar = 'EXISTING_VAR' + const warningMessage = generateWarning(existingVar) + const expectedSuccessMessage = `Unset environment variable ${chalk.yellow(`${existingVar}`)} in the ${chalk.magenta( + 'all', + )} context` + beforeEach(() => { - setTestingPrompts('true') + vi.resetModules() + vi.clearAllMocks() + + Object.defineProperty(process, 'env', { value: {} }) }) - test('should log warnings and prompts if enviroment variable already exists', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + afterAll(() => { + vi.resetModules() + vi.restoreAllMocks() + + Object.defineProperty(process, 'env', { + value: OLD_ENV, + }) + }) - const promptSpy = mockPrompt({ confirm: true }) + describe('user is prompted to confirm when unsetting an env var that already exists', () => { + beforeEach(() => { + setTestingPrompts('true') + }) - await runMockProgram(['', '', 'env:unset', existingVar]) + test('should log warnings and prompts if enviroment variable already exists', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - expect(promptSpy).toHaveBeenCalledWith({ - type: 'confirm', - name: 'confirm', - message: expect.stringContaining(overwriteConfirmation), - default: false, - }) + const promptSpy = mockPrompt({ confirm: true }) - expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(overwriteNotice) - expect(log).toHaveBeenCalledWith(expectedSuccessMessage) + await runMockProgram(['', '', 'env:unset', existingVar]) + + expect(promptSpy).toHaveBeenCalledWith({ + type: 'confirm', + name: 'confirm', + message: expect.stringContaining(overwriteConfirmation), + default: false, + }) + + expect(log).toHaveBeenCalledWith(warningMessage) + expect(log).toHaveBeenCalledWith(overwriteNotice) + expect(log).toHaveBeenCalledWith(expectedSuccessMessage) + }) }) - }) - test('should skip warnings and prompts if --force flag is passed', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + test('should skip warnings and prompts if --force flag is passed', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const promptSpy = spyOnMockPrompt() + const promptSpy = spyOnMockPrompt() - await runMockProgram(['', '', 'env:unset', existingVar, '--force']) + await runMockProgram(['', '', 'env:unset', existingVar, '--force']) - expect(promptSpy).not.toHaveBeenCalled() + expect(promptSpy).not.toHaveBeenCalled() - expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNotice) - expect(log).toHaveBeenCalledWith(expectedSuccessMessage) + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) + expect(log).toHaveBeenCalledWith(expectedSuccessMessage) + }) }) - }) - test('should exit user reponds is no to confirmatnion prompt', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + test('should exit user reponds is no to confirmatnion prompt', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const promptSpy = mockPrompt({ confirm: false }) + const promptSpy = mockPrompt({ confirm: false }) - try { - await runMockProgram(['', '', 'env:unset', existingVar]) - } catch (error) { - // We expect the process to exit, so this is fine - expect(error.message).toContain('process.exit unexpectedly called') - } + try { + await runMockProgram(['', '', 'env:unset', existingVar]) + } catch (error) { + // We expect the process to exit, so this is fine + expect(error.message).toContain('process.exit unexpectedly called') + } - expect(promptSpy).toHaveBeenCalled() + expect(promptSpy).toHaveBeenCalled() - expect(log).toHaveBeenCalledWith(warningMessage) - expect(log).toHaveBeenCalledWith(overwriteNotice) - expect(log).not.toHaveBeenCalledWith(expectedSuccessMessage) + expect(log).toHaveBeenCalledWith(warningMessage) + expect(log).toHaveBeenCalledWith(overwriteNotice) + expect(log).not.toHaveBeenCalledWith(expectedSuccessMessage) + }) }) - }) - test('should not run prompts if enviroment variable does not exist', async () => { - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + test('should not run prompts if enviroment variable does not exist', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const promptSpy = spyOnMockPrompt() + const promptSpy = spyOnMockPrompt() - await runMockProgram(['', '', 'env:unset', 'NEW_ENV_VAR']) + await runMockProgram(['', '', 'env:unset', 'NEW_ENV_VAR']) - expect(promptSpy).not.toHaveBeenCalled() + expect(promptSpy).not.toHaveBeenCalled() - expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNotice) - expect(log).not.toHaveBeenCalledWith(expectedSuccessMessage) + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) + expect(log).not.toHaveBeenCalledWith(expectedSuccessMessage) + }) }) }) - }) - describe('prompts should not show in an non-interactive shell or in a ci/cd enviroment', () => { - test('prompts should not show in an non-interactive shell', async () => { - setTTYMode(false) + describe('prompts should not show in an non-interactive shell or in a ci/cd enviroment', () => { + test('prompts should not show in an non-interactive shell', async () => { + setTTYMode(false) - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const promptSpy = spyOnMockPrompt() + const promptSpy = spyOnMockPrompt() - await runMockProgram(['', '', 'env:unset', existingVar]) - expect(promptSpy).not.toHaveBeenCalled() + await runMockProgram(['', '', 'env:unset', existingVar]) + expect(promptSpy).not.toHaveBeenCalled() - expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNotice) - expect(log).toHaveBeenCalledWith(expectedSuccessMessage) + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) + expect(log).toHaveBeenCalledWith(expectedSuccessMessage) + }) }) - }) - test('prompts should not show in a ci/cd enviroment', async () => { - setCI(true) + test('prompts should not show in a ci/cd enviroment', async () => { + setCI(true) - await withMockApi(routes, async ({ apiUrl }) => { - Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) - const promptSpy = spyOnMockPrompt() + const promptSpy = spyOnMockPrompt() - await runMockProgram(['', '', 'env:unset', existingVar]) - expect(promptSpy).not.toHaveBeenCalled() + await runMockProgram(['', '', 'env:unset', existingVar]) + expect(promptSpy).not.toHaveBeenCalled() - expect(log).not.toHaveBeenCalledWith(warningMessage) - expect(log).not.toHaveBeenCalledWith(overwriteNotice) - expect(log).toHaveBeenCalledWith(expectedSuccessMessage) + expect(log).not.toHaveBeenCalledWith(warningMessage) + expect(log).not.toHaveBeenCalledWith(overwriteNotice) + expect(log).toHaveBeenCalledWith(expectedSuccessMessage) + }) }) }) }) diff --git a/tests/integration/commands/integration/deploy.test.ts b/tests/integration/commands/integration/deploy.test.ts index 133a956c180..8d72566e644 100644 --- a/tests/integration/commands/integration/deploy.test.ts +++ b/tests/integration/commands/integration/deploy.test.ts @@ -30,8 +30,7 @@ describe(`integration:deploy`, () => { beforeEach(() => { vi.resetModules() vi.clearAllMocks() - - Object.defineProperty(process, 'env', { value: {} }) + Object.defineProperty(process, 'env', { value: OLD_ENV }) }) afterAll(() => { From 6707b33bd9a4672c978bbda4bb548422d72604b2 Mon Sep 17 00:00:00 2001 From: Will Date: Wed, 30 Oct 2024 11:14:14 -0500 Subject: [PATCH 37/39] fix: fixed flakey deploy test and added env cleanup to more tests --- src/commands/integration/deploy.ts | 6 ++++-- .../commands/blobs/blobs-delete.test.ts | 1 - .../commands/integration/deploy.test.ts | 8 +++++--- tests/integration/commands/logs/build.test.ts | 14 +++++++++++--- tests/integration/commands/logs/functions.test.ts | 12 +++++++++++- 5 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/commands/integration/deploy.ts b/src/commands/integration/deploy.ts index 9a86d6ec23f..bfd6551f4aa 100644 --- a/src/commands/integration/deploy.ts +++ b/src/commands/integration/deploy.ts @@ -1,6 +1,7 @@ import fs from 'fs' +import process from 'process' import { resolve } from 'path' -import { env, exit } from 'process' +import { exit } from 'process' import { OptionValues } from 'commander' import inquirer from 'inquirer' @@ -17,7 +18,7 @@ import { checkOptions } from '../build/build.js' import { deploy as siteDeploy } from '../deploy/deploy.js' function getIntegrationAPIUrl() { - return env.INTEGRATION_URL || 'https://api.netlifysdk.com' + return process.env.INTEGRATION_URL || 'https://api.netlifysdk.com' } // @ts-expect-error TS(7006) FIXME: Parameter 'localScopes' implicitly has an 'any' ty... Remove this comment to see the full error message @@ -425,6 +426,7 @@ export const deploy = async (options: OptionValues, command: BaseCommand) => { headers, }, ).then(async (res) => { + console.log('res', res) const body = await res.json() return { body, statusCode: res.status } }) diff --git a/tests/integration/commands/blobs/blobs-delete.test.ts b/tests/integration/commands/blobs/blobs-delete.test.ts index a728e4da707..c0c347d3624 100644 --- a/tests/integration/commands/blobs/blobs-delete.test.ts +++ b/tests/integration/commands/blobs/blobs-delete.test.ts @@ -65,7 +65,6 @@ describe('blobs:delete command', () => { beforeEach(() => { vi.resetModules() vi.clearAllMocks() - Object.defineProperty(process, 'env', { value: {} }) }) diff --git a/tests/integration/commands/integration/deploy.test.ts b/tests/integration/commands/integration/deploy.test.ts index 8d72566e644..ef0e192f6ab 100644 --- a/tests/integration/commands/integration/deploy.test.ts +++ b/tests/integration/commands/integration/deploy.test.ts @@ -24,13 +24,15 @@ describe('integration:deploy areScopesEqual', () => { }) }) -const OLD_ENV = process.env +const originalEnv = process.env describe(`integration:deploy`, () => { beforeEach(() => { vi.resetModules() vi.clearAllMocks() - Object.defineProperty(process, 'env', { value: OLD_ENV }) + Object.defineProperty(process, 'env', { + value: originalEnv, + }) }) afterAll(() => { @@ -38,7 +40,7 @@ describe(`integration:deploy`, () => { vi.restoreAllMocks() Object.defineProperty(process, 'env', { - value: OLD_ENV, + value: originalEnv, }) }) diff --git a/tests/integration/commands/logs/build.test.ts b/tests/integration/commands/logs/build.test.ts index 956bab228a4..51b3d57990e 100644 --- a/tests/integration/commands/logs/build.test.ts +++ b/tests/integration/commands/logs/build.test.ts @@ -1,4 +1,4 @@ -import { Mock, afterEach, beforeEach, describe, expect, test, vi } from 'vitest' +import { Mock, afterAll, afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import BaseCommand from '../../../../src/commands/base-command.js' import { createLogsBuildCommand } from '../../../../src/commands/logs/index.js' @@ -49,21 +49,29 @@ const routes = [ }, ] +const originalEnv = { ...process.env } + describe('logs:deploy command', () => { let program: BaseCommand - const originalEnv = { ...process.env } afterEach(() => { vi.clearAllMocks() + process.env = { ...originalEnv } }) beforeEach(() => { - process.env = { ...originalEnv } program = new BaseCommand('netlify') createLogsBuildCommand(program) }) + afterAll(() => { + vi.restoreAllMocks() + vi.resetModules() + + process.env = { ...originalEnv } + }) + test('should setup the deploy stream correctly', async ({}) => { const { apiUrl } = await startMockApi({ routes }) const spyWebsocket = getWebSocket as unknown as Mock diff --git a/tests/integration/commands/logs/functions.test.ts b/tests/integration/commands/logs/functions.test.ts index 278dba7423f..f7267d9eda7 100644 --- a/tests/integration/commands/logs/functions.test.ts +++ b/tests/integration/commands/logs/functions.test.ts @@ -1,4 +1,4 @@ -import { Mock, afterEach, beforeEach, describe, expect, test, vi } from 'vitest' +import { Mock, afterAll, afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import BaseCommand from '../../../../src/commands/base-command.js' import { createLogsFunctionCommand } from '../../../../src/commands/logs/index.js' @@ -65,10 +65,13 @@ const routes = [ ] describe('logs:function command', () => { + const originalEnv = process.env + let program: BaseCommand afterEach(() => { vi.clearAllMocks() + process.env = { ...originalEnv } }) beforeEach(() => { @@ -77,6 +80,13 @@ describe('logs:function command', () => { createLogsFunctionCommand(program) }) + afterAll(() => { + vi.restoreAllMocks() + vi.resetModules() + + process.env = { ...originalEnv } + }) + test('should setup the functions stream correctly', async ({}) => { const { apiUrl } = await startMockApi({ routes }) const spyWebsocket = getWebSocket as unknown as Mock From 34d0b88fa8dd645a8e634d6859dad3e1fc5fc20d Mon Sep 17 00:00:00 2001 From: Will Date: Wed, 30 Oct 2024 11:54:00 -0500 Subject: [PATCH 38/39] fix: removed a console.log() statement Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> --- src/commands/integration/deploy.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/commands/integration/deploy.ts b/src/commands/integration/deploy.ts index bfd6551f4aa..0b5c258cc5f 100644 --- a/src/commands/integration/deploy.ts +++ b/src/commands/integration/deploy.ts @@ -426,7 +426,6 @@ export const deploy = async (options: OptionValues, command: BaseCommand) => { headers, }, ).then(async (res) => { - console.log('res', res) const body = await res.json() return { body, statusCode: res.status } }) From ed3c6f356d4c904f02e5ef628e855a333c885edb Mon Sep 17 00:00:00 2001 From: Will Date: Thu, 31 Oct 2024 09:21:42 -0500 Subject: [PATCH 39/39] fix: cleaned up unused functions and comments Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> --- src/commands/base-command.ts | 7 ------- src/commands/blobs/blobs-delete.ts | 2 -- src/commands/blobs/blobs-set.ts | 2 -- src/commands/env/env-clone.ts | 2 -- src/commands/env/env-set.ts | 2 -- src/commands/env/env-unset.ts | 2 -- 6 files changed, 17 deletions(-) diff --git a/src/commands/base-command.ts b/src/commands/base-command.ts index 9b14685014f..129197271fe 100644 --- a/src/commands/base-command.ts +++ b/src/commands/base-command.ts @@ -167,13 +167,6 @@ export default class BaseCommand extends Command { // eslint-disable-next-line workspace/no-process-cwd workingDir = process.cwd() - /** - * Determines if the command is scripted or not. - * If the command is scripted (SHLVL is greater than 1 or CI/CONTINUOUS_INTEGRATION is true) then some commands - * might behave differently. - */ - scriptedCommand = Boolean(!process.stdin.isTTY || isCI || process.env.CI) - /** * The workspace root if inside a mono repository. * Must not be the repository root! diff --git a/src/commands/blobs/blobs-delete.ts b/src/commands/blobs/blobs-delete.ts index b037f923012..4a8bd52cadd 100644 --- a/src/commands/blobs/blobs-delete.ts +++ b/src/commands/blobs/blobs-delete.ts @@ -7,8 +7,6 @@ import { promptBlobDelete } from '../../utils/prompts/blob-delete-prompts.js' * The blobs:delete command */ export const blobsDelete = async (storeName: string, key: string, _options: Record, command: any) => { - // Prevents prompts from blocking scripted commands - const { api, siteInfo } = command.netlify const { force } = _options diff --git a/src/commands/blobs/blobs-set.ts b/src/commands/blobs/blobs-set.ts index 8f6c5ae2cd6..ca481f7fb7d 100644 --- a/src/commands/blobs/blobs-set.ts +++ b/src/commands/blobs/blobs-set.ts @@ -20,8 +20,6 @@ export const blobsSet = async ( options: Options, command: BaseCommand, ) => { - // Prevents prompts from blocking scripted commands - const { api, siteInfo } = command.netlify const { force, input } = options const store = getStore({ diff --git a/src/commands/env/env-clone.ts b/src/commands/env/env-clone.ts index 7901cee341c..5d08117d980 100644 --- a/src/commands/env/env-clone.ts +++ b/src/commands/env/env-clone.ts @@ -56,8 +56,6 @@ const cloneEnvVars = async ({ api, force, siteFrom, siteTo }): Promise } export const envClone = async (options: OptionValues, command: BaseCommand) => { - // Prevents prompts from blocking scripted commands - const { api, site } = command.netlify const { force } = options diff --git a/src/commands/env/env-set.ts b/src/commands/env/env-set.ts index 9aad3ba338f..8818d71861f 100644 --- a/src/commands/env/env-set.ts +++ b/src/commands/env/env-set.ts @@ -113,8 +113,6 @@ const setInEnvelope = async ({ api, context, force, key, scope, secret, siteInfo } export const envSet = async (key: string, value: string, options: OptionValues, command: BaseCommand) => { - // Prevents prompts from ci and non ineractive shells - const { context, force, scope, secret } = options const { api, cachedConfig, site } = command.netlify const siteId = site.id diff --git a/src/commands/env/env-unset.ts b/src/commands/env/env-unset.ts index a1695ecf751..14668251e2e 100644 --- a/src/commands/env/env-unset.ts +++ b/src/commands/env/env-unset.ts @@ -68,8 +68,6 @@ const unsetInEnvelope = async ({ api, context, force, key, siteInfo }) => { } export const envUnset = async (key: string, options: OptionValues, command: BaseCommand) => { - // Prevents prompts from blocking scripted commands - const { context, force } = options const { api, cachedConfig, site } = command.netlify const siteId = site.id