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..cbf5efde38 --- /dev/null +++ b/packages/eas-cli/src/commands/env/__tests__/EnvironmentVariableDelete.test.ts @@ -0,0 +1,127 @@ +import { Config } from '@oclif/core'; + +import { + EnvironmentVariableEnvironment, + 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, + environments: [EnvironmentVariableEnvironment.Production], + }, + ]; + (EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables); + + const command = new EnvironmentVariableDelete( + [ + '--variable-name', + 'TEST_VARIABLE', + '--variable-environment', + 'production', + '--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( + ['--variable-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(['--variable-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 --variable-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..e746669c17 --- /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 --variable-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..96540d12f9 100644 --- a/packages/eas-cli/src/commands/env/delete.ts +++ b/packages/eas-cli/src/commands/env/delete.ts @@ -1,22 +1,23 @@ import { Flags } from '@oclif/core'; +import assert from 'assert'; import chalk from 'chalk'; import EasCommand from '../../commandUtils/EasCommand'; import { - EASEnvironmentFlag, EASNonInteractiveFlag, EASVariableScopeFlag, + EasEnvironmentFlagParameters, } from '../../commandUtils/flags'; import { EnvironmentVariableEnvironment, 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 { promptVariableEnvironmentAsync } from '../../utils/prompts'; +import { formatVariableName } from '../../utils/variableUtils'; type DeleteFlags = { - name?: string; - environment?: EnvironmentVariableEnvironment; + 'variable-name'?: string; + 'variable-environment'?: EnvironmentVariableEnvironment; 'non-interactive': boolean; scope?: EnvironmentVariableScope; }; @@ -27,11 +28,14 @@ export default class EnvironmentVariableDelete extends EasCommand { static override hidden = true; static override flags = { - name: Flags.string({ + 'variable-name': Flags.string({ description: 'Name of the variable to delete', }), + 'variable-environment': Flags.enum({ + ...EasEnvironmentFlagParameters, + description: 'Current environment of the variable to delete', + }), ...EASVariableScopeFlag, - ...EASEnvironmentFlag, ...EASNonInteractiveFlag, }; @@ -42,7 +46,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 { + 'variable-name': name, + 'variable-environment': environment, + 'non-interactive': nonInteractive, + scope, + } = this.validateFlags(flags); const { privateProjectConfig: { projectId }, loggedIn: { graphqlClient }, @@ -50,48 +59,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 --variable-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 +121,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.`); } } @@ -117,10 +132,10 @@ export default class EnvironmentVariableDelete extends EasCommand { private validateFlags(flags: DeleteFlags): DeleteFlags { if (flags['non-interactive']) { - if (!flags.name) { + if (!flags['variable-name']) { throw new Error( `Environment variable needs 'name' to be specified when running in non-interactive mode. Run the command with ${chalk.bold( - '--name VARIABLE_NAME' + '--variable-name VARIABLE_NAME' )} flag to fix the issue` ); } 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,