diff --git a/packages/eas-cli/src/commands/env/__tests__/EnvironmentVariableList.test.ts b/packages/eas-cli/src/commands/env/__tests__/EnvironmentVariableList.test.ts new file mode 100644 index 0000000000..29eebb6d05 --- /dev/null +++ b/packages/eas-cli/src/commands/env/__tests__/EnvironmentVariableList.test.ts @@ -0,0 +1,211 @@ +import { Config } from '@oclif/core'; + +import { getMockAppFragment } from '../../../__tests__/commands/utils'; +import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient'; +import { testProjectId } from '../../../credentials/__tests__/fixtures-constants'; +import { + EnvironmentVariableEnvironment, + EnvironmentVariableFragment, + EnvironmentVariableScope, + EnvironmentVariableVisibility, +} from '../../../graphql/generated'; +import { AppQuery } from '../../../graphql/queries/AppQuery'; +import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery'; +import EnvironmentVariableList from '../list'; +import Log from '../../../log'; + +jest.mock('../../../graphql/queries/EnvironmentVariablesQuery'); +jest.mock('../../../graphql/queries/AppQuery'); +jest.mock('../../../log'); + +describe(EnvironmentVariableList, () => { + const graphqlClient = {} as any as ExpoGraphqlClient; + const mockConfig = {} as unknown as Config; + + beforeEach(() => { + jest.clearAllMocks(); + jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment()); + }); + + it('lists project environment variables successfully', async () => { + const mockVariables: EnvironmentVariableFragment[] = [ + { + id: 'var1', + name: 'TEST_VAR_1', + value: 'value1', + environments: [EnvironmentVariableEnvironment.Production], + scope: EnvironmentVariableScope.Project, + visibility: EnvironmentVariableVisibility.Public, + createdAt: undefined, + updatedAt: undefined, + }, + { + id: 'var2', + name: 'TEST_VAR_2', + value: 'value2', + environments: [EnvironmentVariableEnvironment.Development], + scope: EnvironmentVariableScope.Project, + visibility: EnvironmentVariableVisibility.Public, + createdAt: undefined, + updatedAt: undefined, + }, + ]; + + jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValueOnce(mockVariables); + + const command = new EnvironmentVariableList([], mockConfig); + + // @ts-expect-error + jest.spyOn(command, 'getContextAsync').mockReturnValue({ + loggedIn: { graphqlClient }, + privateProjectConfig: { projectId: testProjectId }, + }); + await command.runAsync(); + + expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, { + appId: testProjectId, + environment: undefined, + }); + expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1')); + expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2')); + }); + + it('lists project environment variables including sensitive values', async () => { + const mockVariables: EnvironmentVariableFragment[] = [ + { + id: 'var1', + name: 'TEST_VAR_1', + value: 'value1', + environments: [EnvironmentVariableEnvironment.Production], + scope: EnvironmentVariableScope.Project, + visibility: EnvironmentVariableVisibility.Public, + createdAt: undefined, + updatedAt: undefined, + }, + { + id: 'var2', + name: 'TEST_VAR_2', + value: 'value2', + environments: [EnvironmentVariableEnvironment.Development], + scope: EnvironmentVariableScope.Project, + visibility: EnvironmentVariableVisibility.Sensitive, + createdAt: undefined, + updatedAt: undefined, + }, + ]; + + jest + .mocked(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync) + .mockResolvedValueOnce(mockVariables); + + const command = new EnvironmentVariableList(['--include-sensitive'], mockConfig); + + // @ts-expect-error + jest.spyOn(command, 'getContextAsync').mockReturnValue({ + loggedIn: { graphqlClient }, + privateProjectConfig: { projectId: testProjectId }, + }); + await command.runAsync(); + + expect(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync).toHaveBeenCalledWith( + graphqlClient, + { + appId: testProjectId, + environment: undefined, + } + ); + expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1')); + expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2')); + }); + + it('lists shared environment variables successfully', async () => { + const mockVariables: EnvironmentVariableFragment[] = [ + { + id: 'var1', + name: 'TEST_VAR_1', + value: 'value1', + environments: [EnvironmentVariableEnvironment.Production], + scope: EnvironmentVariableScope.Shared, + visibility: EnvironmentVariableVisibility.Public, + createdAt: undefined, + updatedAt: undefined, + }, + { + id: 'var2', + name: 'TEST_VAR_2', + value: 'value2', + environments: [EnvironmentVariableEnvironment.Development], + scope: EnvironmentVariableScope.Shared, + visibility: EnvironmentVariableVisibility.Public, + createdAt: undefined, + updatedAt: undefined, + }, + ]; + + jest.mocked(EnvironmentVariablesQuery.sharedAsync).mockResolvedValueOnce(mockVariables); + + const command = new EnvironmentVariableList(['--scope', 'shared'], mockConfig); + + // @ts-expect-error + jest.spyOn(command, 'getContextAsync').mockReturnValue({ + loggedIn: { graphqlClient }, + privateProjectConfig: { projectId: testProjectId }, + }); + await command.runAsync(); + + expect(EnvironmentVariablesQuery.sharedAsync).toHaveBeenCalledWith(graphqlClient, { + appId: testProjectId, + environment: undefined, + }); + expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1')); + expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2')); + }); + + it('lists shared environment variables including sensitive values', async () => { + const mockVariables: EnvironmentVariableFragment[] = [ + { + id: 'var1', + name: 'TEST_VAR_1', + value: 'value1', + environments: [EnvironmentVariableEnvironment.Production], + scope: EnvironmentVariableScope.Shared, + visibility: EnvironmentVariableVisibility.Public, + createdAt: undefined, + updatedAt: undefined, + }, + { + id: 'var2', + name: 'TEST_VAR_2', + value: 'value2', + environments: [EnvironmentVariableEnvironment.Development], + scope: EnvironmentVariableScope.Shared, + visibility: EnvironmentVariableVisibility.Sensitive, + createdAt: undefined, + updatedAt: undefined, + }, + ]; + + jest + .mocked(EnvironmentVariablesQuery.sharedWithSensitiveAsync) + .mockResolvedValueOnce(mockVariables); + + const command = new EnvironmentVariableList( + ['--include-sensitive', '--scope', 'shared'], + mockConfig + ); + + // @ts-expect-error + jest.spyOn(command, 'getContextAsync').mockReturnValue({ + loggedIn: { graphqlClient }, + privateProjectConfig: { projectId: testProjectId }, + }); + await command.runAsync(); + + expect(EnvironmentVariablesQuery.sharedWithSensitiveAsync).toHaveBeenCalledWith(graphqlClient, { + appId: testProjectId, + environment: undefined, + }); + expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1')); + expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2')); + }); +}); diff --git a/packages/eas-cli/src/commands/env/list.ts b/packages/eas-cli/src/commands/env/list.ts index 8b1e4797f9..46b3102bbc 100644 --- a/packages/eas-cli/src/commands/env/list.ts +++ b/packages/eas-cli/src/commands/env/list.ts @@ -4,7 +4,7 @@ import chalk from 'chalk'; import EasCommand from '../../commandUtils/EasCommand'; import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient'; import { - EASEnvironmentFlag, + EASMultiEnvironmentFlag, EASVariableFormatFlag, EASVariableScopeFlag, } from '../../commandUtils/flags'; @@ -16,7 +16,42 @@ import { import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery'; import Log from '../../log'; import { promptVariableEnvironmentAsync } from '../../utils/prompts'; -import { formatVariable } from '../../utils/variableUtils'; +import { formatVariable, performForEnvironmentsAsync } from '../../utils/variableUtils'; + +async function getVariablesForScopeAsync( + graphqlClient: ExpoGraphqlClient, + { + scope, + includingSensitive, + environment, + projectId, + }: { + scope: EnvironmentVariableScope; + includingSensitive: boolean; + environment?: EnvironmentVariableEnvironment; + projectId: string; + } +): Promise { + if (scope === EnvironmentVariableScope.Project) { + if (includingSensitive) { + return await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(graphqlClient, { + appId: projectId, + environment, + }); + } + return await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, { + appId: projectId, + environment, + }); + } + + return includingSensitive + ? await EnvironmentVariablesQuery.sharedWithSensitiveAsync(graphqlClient, { + appId: projectId, + environment, + }) + : await EnvironmentVariablesQuery.sharedAsync(graphqlClient, { appId: projectId, environment }); +} export default class EnvironmentValueList extends EasCommand { static override description = 'list environment variables for the current project'; @@ -35,12 +70,18 @@ export default class EnvironmentValueList extends EasCommand { }), ...EASVariableFormatFlag, ...EASVariableScopeFlag, - ...EASEnvironmentFlag, + ...EASMultiEnvironmentFlag, }; async runAsync(): Promise { let { - flags: { environment, format, scope, 'include-sensitive': includeSensitive }, + flags: { + environments, + format, + scope, + 'include-sensitive': includeSensitive, + 'non-interactive': nonInteractive, + }, } = await this.parse(EnvironmentValueList); const { privateProjectConfig: { projectId }, @@ -49,70 +90,43 @@ export default class EnvironmentValueList extends EasCommand { nonInteractive: true, }); - if (scope === EnvironmentVariableScope.Project && !environment) { - environment = await promptVariableEnvironmentAsync({ nonInteractive: false }); + if (!environments) { + environments = await promptVariableEnvironmentAsync({ nonInteractive, multiple: true }); } - const variables = await this.getVariablesForScopeAsync(graphqlClient, { - scope, - includingSensitive: includeSensitive, - environment, - projectId, - }); + await performForEnvironmentsAsync(environments, async environment => { + const variables = await getVariablesForScopeAsync(graphqlClient, { + scope, + includingSensitive: includeSensitive, + environment, + projectId, + }); - if (format === 'short') { - for (const variable of variables) { - // TODO: Add Learn more link - Log.log( - `${chalk.bold(variable.name)}=${ - variable.value ?? - "***** (This is a secret env variable that can only be accessed on EAS builder and can't be read in any UI. Learn more.)" - }` - ); + Log.addNewLineIfNone(); + if (environment) { + Log.log(chalk.bold(`Environment: ${environment}`)); } - } else { - if (scope === EnvironmentVariableScope.Shared) { - Log.log(chalk.bold('Shared variables for this account:')); - } else { - Log.log(chalk.bold(`Variables for this project for environment ${environment}:`)); - } - Log.log( - variables.map(variable => formatVariable(variable)).join(`\n\n${chalk.dim('———')}\n\n`) - ); - } - } - private async getVariablesForScopeAsync( - graphqlClient: ExpoGraphqlClient, - { - scope, - includingSensitive, - environment, - projectId, - }: { - scope: EnvironmentVariableScope; - includingSensitive: boolean; - environment?: EnvironmentVariableEnvironment; - projectId: string; - } - ): Promise { - if (scope === EnvironmentVariableScope.Project && environment) { - if (includingSensitive) { - return await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(graphqlClient, { - appId: projectId, - environment, - }); + if (format === 'short') { + for (const variable of variables) { + // TODO: Add Learn more link + Log.log( + `${chalk.bold(variable.name)}=${ + variable.value ?? + "***** (This is a secret env variable that can only be accessed on EAS builder and can't be read in any UI. Learn more.)" + }` + ); + } + } else { + if (scope === EnvironmentVariableScope.Shared) { + Log.log(chalk.bold('Shared variables for this account:')); + } else { + Log.log(chalk.bold(`Variables for this project:`)); + } + Log.log( + variables.map(variable => formatVariable(variable)).join(`\n\n${chalk.dim('———')}\n\n`) + ); } - return await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, { - appId: projectId, - environment, - }); - } - - return includingSensitive - ? await EnvironmentVariablesQuery.sharedWithSensitiveAsync(graphqlClient, { - appId: projectId, - }) - : await EnvironmentVariablesQuery.sharedAsync(graphqlClient, { appId: projectId }); + }); } }