From fd9e8302a1a22265960991464f8f08121ad6649e Mon Sep 17 00:00:00 2001 From: Piotr Szeremeta Date: Wed, 18 Sep 2024 17:21:51 +0200 Subject: [PATCH] Add tests for EnvironmentVariableDelete --- .../EnvironmentVariableDelete.test.ts | 110 ++++++++++++++++++ .../EnvironmentVariableDelete.test.ts.snap | 3 + packages/eas-cli/src/commands/env/delete.ts | 70 ++++++----- packages/eas-cli/src/utils/variableUtils.ts | 14 ++- 4 files changed, 167 insertions(+), 30 deletions(-) create mode 100644 packages/eas-cli/src/commands/env/__tests__/EnvironmentVariableDelete.test.ts create mode 100644 packages/eas-cli/src/commands/env/__tests__/__snapshots__/EnvironmentVariableDelete.test.ts.snap diff --git a/packages/eas-cli/src/commands/env/__tests__/EnvironmentVariableDelete.test.ts b/packages/eas-cli/src/commands/env/__tests__/EnvironmentVariableDelete.test.ts new file mode 100644 index 0000000000..a7f185053b --- /dev/null +++ b/packages/eas-cli/src/commands/env/__tests__/EnvironmentVariableDelete.test.ts @@ -0,0 +1,110 @@ +import { Config } from '@oclif/core'; + +import { EnvironmentVariableScope } from '../../../graphql/generated'; +import { EnvironmentVariableMutation } from '../../../graphql/mutations/EnvironmentVariableMutation'; +import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery'; +import Log from '../../../log'; +import { promptAsync, toggleConfirmAsync } from '../../../prompts'; +import EnvironmentVariableDelete from '../delete'; + +jest.mock('../../../graphql/queries/EnvironmentVariablesQuery'); +jest.mock('../../../graphql/mutations/EnvironmentVariableMutation'); +jest.mock('../../../prompts'); +jest.mock('../../../log'); + +describe(EnvironmentVariableDelete, () => { + const projectId = 'test-project-id'; + const variableId = '1'; + const graphqlClient = {}; + const mockConfig = {} as unknown as Config; + const mockContext = { + privateProjectConfig: { projectId }, + loggedIn: { graphqlClient }, + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('deletes a variable by name in non-interactive mode', async () => { + const mockVariables = [ + { id: variableId, name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project }, + ]; + (EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables); + + const command = new EnvironmentVariableDelete( + ['--name', 'TEST_VARIABLE', '--non-interactive'], + mockConfig + ); + // @ts-expect-error + jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext); + await command.runAsync(); + + expect(EnvironmentVariableMutation.deleteAsync).toHaveBeenCalledWith(graphqlClient, variableId); + expect(Log.withTick).toHaveBeenCalledWith('️Deleted variable TEST_VARIABLE".'); + }); + + it('prompts for variable selection when name is not provided', async () => { + const mockVariables = [ + { id: variableId, name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project }, + ]; + (EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables); + (promptAsync as jest.Mock).mockResolvedValue({ variable: mockVariables[0] }); + (toggleConfirmAsync as jest.Mock).mockResolvedValue(true); + + const command = new EnvironmentVariableDelete([], mockConfig); + // @ts-expect-error + jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext); + await command.runAsync(); + + expect(promptAsync).toHaveBeenCalled(); + expect(EnvironmentVariableMutation.deleteAsync).toHaveBeenCalledWith(graphqlClient, variableId); + expect(Log.withTick).toHaveBeenCalledWith('️Deleted variable TEST_VARIABLE".'); + }); + + it('throws an error when variable name is not found', async () => { + const mockVariables = [ + { id: variableId, name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project }, + ]; + (EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables); + + const command = new EnvironmentVariableDelete(['--name', 'NON_EXISTENT_VARIABLE'], mockConfig); + + // @ts-expect-error + jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext); + await expect(command.runAsync()).rejects.toThrow('Variable "NON_EXISTENT_VARIABLE" not found.'); + }); + + it('throws an error when multiple variables with the same name are found', async () => { + const mockVariables = [ + { id: variableId, name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project }, + { id: '2', name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project }, + ]; + (EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables); + + const command = new EnvironmentVariableDelete(['--name', 'TEST_VARIABLE'], mockConfig); + + // @ts-expect-error + jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext); + await expect(command.runAsync()).rejects.toThrow( + 'Multiple variables with name "TEST_VARIABLE" found. Please select the variable to delete interactively or run command with --environment ENVIRONMENT option.' + ); + }); + + it('cancels deletion when user does not confirm', async () => { + const mockVariables = [ + { id: variableId, name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project }, + ]; + (EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables); + (promptAsync as jest.Mock).mockResolvedValue({ variable: mockVariables[0] }); + (toggleConfirmAsync as jest.Mock).mockResolvedValue(false); + + const command = new EnvironmentVariableDelete(['--non-interactive'], mockConfig); + + // @ts-expect-error + jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext); + await expect(command.runAsync()).rejects.toThrowErrorMatchingSnapshot(); + + expect(EnvironmentVariableMutation.deleteAsync).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/eas-cli/src/commands/env/__tests__/__snapshots__/EnvironmentVariableDelete.test.ts.snap b/packages/eas-cli/src/commands/env/__tests__/__snapshots__/EnvironmentVariableDelete.test.ts.snap new file mode 100644 index 0000000000..65366b4c48 --- /dev/null +++ b/packages/eas-cli/src/commands/env/__tests__/__snapshots__/EnvironmentVariableDelete.test.ts.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EnvironmentVariableDelete cancels deletion when user does not confirm 1`] = `"Environment variable needs 'name' to be specified when running in non-interactive mode. Run the command with --name VARIABLE_NAME flag to fix the issue"`; diff --git a/packages/eas-cli/src/commands/env/delete.ts b/packages/eas-cli/src/commands/env/delete.ts index d47935fdc0..1c496e36a2 100644 --- a/packages/eas-cli/src/commands/env/delete.ts +++ b/packages/eas-cli/src/commands/env/delete.ts @@ -1,4 +1,5 @@ import { Flags } from '@oclif/core'; +import assert from 'assert'; import chalk from 'chalk'; import EasCommand from '../../commandUtils/EasCommand'; @@ -12,7 +13,7 @@ import { EnvironmentVariableMutation } from '../../graphql/mutations/Environment import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery'; import Log from '../../log'; import { promptAsync, toggleConfirmAsync } from '../../prompts'; -import { promptVariableEnvironmentAsync } from '../../utils/prompts'; +import { formatVariableName } from '../../utils/variableUtils'; type DeleteFlags = { name?: string; @@ -42,7 +43,12 @@ export default class EnvironmentVariableDelete extends EasCommand { async runAsync(): Promise { const { flags } = await this.parse(EnvironmentVariableDelete); - let { name, environment, 'non-interactive': nonInteractive, scope } = this.validateFlags(flags); + const { + name, + environment, + 'non-interactive': nonInteractive, + scope, + } = this.validateFlags(flags); const { privateProjectConfig: { projectId }, loggedIn: { graphqlClient }, @@ -50,48 +56,54 @@ export default class EnvironmentVariableDelete extends EasCommand { nonInteractive, }); - if (scope === EnvironmentVariableScope.Project) { - if (!environment) { - environment = await promptVariableEnvironmentAsync({ nonInteractive }); - } - } - const variables = - scope === EnvironmentVariableScope.Project && environment + scope === EnvironmentVariableScope.Project ? await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, { appId: projectId, environment, }) - : await EnvironmentVariablesQuery.sharedAsync(graphqlClient, { appId: projectId }); + : await EnvironmentVariablesQuery.sharedAsync(graphqlClient, { + appId: projectId, + environment, + }); + + let selectedVariable; if (!name) { - ({ name } = await promptAsync({ + ({ variable: selectedVariable } = await promptAsync({ type: 'select', - name: 'name', + name: 'variable', message: 'Pick the variable to be deleted:', choices: variables .filter(({ scope: variableScope }) => scope === variableScope) - .map(variable => ({ - title: variable.name, - value: variable.name, - })), + .map(variable => { + return { + title: formatVariableName(variable), + value: variable, + }; + }), })); - - if (!name) { - throw new Error( - `Environment variable wasn't selected. Run the command again and select existing variable or run it with ${chalk.bold( - '--name VARIABLE_NAME' - )} flag to fix the issue.` - ); + } else { + const selectedVariables = variables.filter( + variable => + variable.name === name && (!environment || variable.environments?.includes(environment)) + ); + + if (selectedVariables.length !== 1) { + if (selectedVariables.length === 0) { + throw new Error(`Variable "${name}" not found.`); + } else { + throw new Error( + `Multiple variables with name "${name}" found. Please select the variable to delete interactively or run command with --environment ENVIRONMENT option.` + ); + } } - } - - const selectedVariable = variables.find(variable => variable.name === name); - if (!selectedVariable) { - throw new Error(`Variable "${name}" not found.`); + selectedVariable = selectedVariables[0]; } + assert(selectedVariable, `Variable "${name}" not found.`); + if (!nonInteractive) { Log.addNewLineIfNone(); Log.warn(`You are about to permanently delete variable ${selectedVariable.name}.`); @@ -106,7 +118,7 @@ export default class EnvironmentVariableDelete extends EasCommand { }); if (!confirmed) { Log.error(`Canceled deletion of variable ${selectedVariable.name}.`); - throw new Error(`Variable "${name}" not deleted.`); + throw new Error(`Variable "${selectedVariable.name}" not deleted.`); } } diff --git a/packages/eas-cli/src/utils/variableUtils.ts b/packages/eas-cli/src/utils/variableUtils.ts index 8cb38751f6..62b91519db 100644 --- a/packages/eas-cli/src/utils/variableUtils.ts +++ b/packages/eas-cli/src/utils/variableUtils.ts @@ -1,7 +1,19 @@ import dateFormat from 'dateformat'; import formatFields from './formatFields'; -import { EnvironmentVariableEnvironment, EnvironmentVariableFragment } from '../graphql/generated'; +import { + EnvironmentVariableEnvironment, + EnvironmentVariableFragment, + EnvironmentVariableScope, +} from '../graphql/generated'; + +export function formatVariableName(variable: EnvironmentVariableFragment): string { + const name = variable.name; + const scope = variable.scope === EnvironmentVariableScope.Project ? 'project' : 'shared'; + const environments = variable.environments?.join(', ') ?? ''; + const updatedAt = variable.updatedAt ? new Date(variable.updatedAt).toLocaleString() : ''; + return `${name} | ${scope} | ${environments} | Updated at: ${updatedAt}`; +} export async function performForEnvironmentsAsync( environments: EnvironmentVariableEnvironment[] | null,