diff --git a/docs/content/reference/cli-test/classes/SlackCLIProcess.md b/docs/content/reference/cli-test/classes/SlackCLIProcess.md new file mode 100644 index 000000000..dcc7360a2 --- /dev/null +++ b/docs/content/reference/cli-test/classes/SlackCLIProcess.md @@ -0,0 +1,150 @@ +# Class: SlackCLIProcess + +## Constructors + +### new SlackCLIProcess() + +```ts +new SlackCLIProcess( + command, + globalOptions?, + commandOptions?): SlackCLIProcess +``` + +#### Parameters + +• **command**: `string` + +• **globalOptions?**: `SlackCLIGlobalOptions` + +• **commandOptions?**: `SlackCLICommandOptions` + +#### Returns + +[`SlackCLIProcess`](SlackCLIProcess.md) + +#### Defined in + +[cli/cli-process.ts:64](https://github.com/slackapi/node-slack-sdk/blob/main/packages/cli-test/src/cli/cli-process.ts#L64) + +## Properties + +### command + +```ts +command: string; +``` + +#### Description + +The CLI command to invoke + +#### Defined in + +[cli/cli-process.ts:52](https://github.com/slackapi/node-slack-sdk/blob/main/packages/cli-test/src/cli/cli-process.ts#L52) + +*** + +### commandOptions + +```ts +commandOptions: undefined | SlackCLICommandOptions; +``` + +#### Description + +The CLI command-specific options to pass to the command + +#### Defined in + +[cli/cli-process.ts:62](https://github.com/slackapi/node-slack-sdk/blob/main/packages/cli-test/src/cli/cli-process.ts#L62) + +*** + +### globalOptions + +```ts +globalOptions: undefined | SlackCLIGlobalOptions; +``` + +#### Description + +The global CLI options to pass to the command + +#### Defined in + +[cli/cli-process.ts:57](https://github.com/slackapi/node-slack-sdk/blob/main/packages/cli-test/src/cli/cli-process.ts#L57) + +## Methods + +### execAsync() + +```ts +execAsync(shellOpts?): Promise +``` + +#### Parameters + +• **shellOpts?**: `Partial`\<`SpawnOptionsWithoutStdio`\> + +#### Returns + +`Promise`\<`ShellProcess`\> + +#### Description + +Executes the command asynchronously, returning the process details once the process finishes executing + +#### Defined in + +[cli/cli-process.ts:76](https://github.com/slackapi/node-slack-sdk/blob/main/packages/cli-test/src/cli/cli-process.ts#L76) + +*** + +### execAsyncUntilOutputPresent() + +```ts +execAsyncUntilOutputPresent(output, shellOpts?): Promise +``` + +#### Parameters + +• **output**: `string` + +• **shellOpts?**: `Partial`\<`SpawnOptionsWithoutStdio`\> + +#### Returns + +`Promise`\<`ShellProcess`\> + +#### Description + +Executes the command asynchronously, returning the process details once the process finishes executing + +#### Defined in + +[cli/cli-process.ts:86](https://github.com/slackapi/node-slack-sdk/blob/main/packages/cli-test/src/cli/cli-process.ts#L86) + +*** + +### execSync() + +```ts +execSync(shellOpts?): string +``` + +#### Parameters + +• **shellOpts?**: `Partial`\<`SpawnOptionsWithoutStdio`\> + +#### Returns + +`string` + +#### Description + +Executes the command synchronously, returning the process standard output + +#### Defined in + +[cli/cli-process.ts:99](https://github.com/slackapi/node-slack-sdk/blob/main/packages/cli-test/src/cli/cli-process.ts#L99) diff --git a/docs/content/reference/cli-test/index.md b/docs/content/reference/cli-test/index.md new file mode 100644 index 000000000..71487f118 --- /dev/null +++ b/docs/content/reference/cli-test/index.md @@ -0,0 +1,12 @@ +# @slack/cli-test + +## Classes + +- [SlackCLIProcess](classes/SlackCLIProcess.md) + +## Variables + +- [SlackCLI](variables/SlackCLI.md) +- [SlackProduct](variables/SlackProduct.md) +- [SlackTracerId](variables/SlackTracerId.md) +- [shell](variables/shell.md) diff --git a/docs/content/reference/cli-test/typedoc-sidebar.cjs b/docs/content/reference/cli-test/typedoc-sidebar.cjs new file mode 100644 index 000000000..348b1706d --- /dev/null +++ b/docs/content/reference/cli-test/typedoc-sidebar.cjs @@ -0,0 +1,4 @@ +// @ts-check +/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ +const typedocSidebar = { items: [{"type":"category","label":"Classes","items":[{"type":"doc","id":"reference/cli-test/classes/SlackCLIProcess","label":"SlackCLIProcess"}]},{"type":"category","label":"Variables","items":[{"type":"doc","id":"reference/cli-test/variables/SlackCLI","label":"SlackCLI"},{"type":"doc","id":"reference/cli-test/variables/SlackProduct","label":"SlackProduct"},{"type":"doc","id":"reference/cli-test/variables/SlackTracerId","label":"SlackTracerId"},{"type":"doc","id":"reference/cli-test/variables/shell","label":"shell"}]}]}; +module.exports = typedocSidebar.items; \ No newline at end of file diff --git a/docs/content/reference/cli-test/variables/SlackCLI.md b/docs/content/reference/cli-test/variables/SlackCLI.md new file mode 100644 index 000000000..252176f80 --- /dev/null +++ b/docs/content/reference/cli-test/variables/SlackCLI.md @@ -0,0 +1,690 @@ +# Variable: SlackCLI + +```ts +const SlackCLI: object; +``` + +Set of functions to spawn and interact with Slack Platform CLI processes and commands + +## Type declaration + +### app + +```ts +app: object; +``` + +### app.delete() + +```ts +delete: (args) => Promise = del; +``` + +`slack app delete` + +#### Parameters + +• **args**: `ProjectCommandArguments` + +#### Returns + +`Promise`\<`string`\> + +command output + +### app.install() + +```ts +install: (args) => Promise; +``` + +`slack app install` + +#### Parameters + +• **args**: `ProjectCommandArguments` + +#### Returns + +`Promise`\<`string`\> + +command output + +### app.list() + +```ts +list: (args) => Promise; +``` + +`slack app list` + +#### Parameters + +• **args**: `ProjectCommandArguments` + +#### Returns + +`Promise`\<`string`\> + +command output + +### auth + +```ts +auth: object; +``` + +### auth.loginChallengeExchange() + +```ts +loginChallengeExchange: (args) => Promise; +``` + +#### Parameters + +• **args**: `SlackCLIHostTargetOptions` & `object` + +#### Returns + +`Promise`\<`string`\> + +### auth.loginNoPrompt() + +```ts +loginNoPrompt: (args?) => Promise; +``` + +`slack login --no-prompt`; initiates a CLI login flow. The `authTicketSlashCommand` returned should be entered + into the Slack client, and the challenge code retrieved and fed into the `loginChallengeExchange` method to + complete the CLI login flow. + +#### Parameters + +• **args?**: `SlackCLIHostTargetOptions` + +#### Returns + +`Promise`\<`object`\> + +##### authTicket + +```ts +authTicket: string; +``` + +An auth ticket is a A UUID sequence granted by Slack to a CLI auth requestor. +That ticket must then be submitted via slash command by a user logged in to Slack and permissions accepted to be +granted a token for use. This `authTicket` is included in the `authTicketSlashCommand`. + +##### authTicketSlashCommand + +```ts +authTicketSlashCommand: string; +``` + +###### Description + +Slash command that a Slack client user must enter into the message composer in order to initiate +login flow. + +###### Example + +```ts +`/slackauthticket MTMxNjgxMDUtYTYwOC00NzRhLWE3M2YtMjVmZTQyMjc1MDg4` +``` + +##### output + +```ts +output: string; +``` + +###### Description + +Command output + +### auth.logout() + +```ts +logout: (args?) => Promise; +``` + +#### Parameters + +• **args?**: Omit\ & (Pick\ \| \{ all?: boolean \| undefined; \}) + +#### Returns + +`Promise`\<`string`\> + +command output + +### collaborator + +```ts +collaborator: object; +``` + +### collaborator.add() + +```ts +add: (args) => Promise; +``` + +`slack collaborators add` + +#### Parameters + +• **args**: `ProjectCommandArguments` & `CollaboratorEmail` + +#### Returns + +`Promise`\<`string`\> + +command output + +### collaborator.list() + +```ts +list: (args) => Promise; +``` + +`slack collaborators list` + +#### Parameters + +• **args**: `ProjectCommandArguments` + +#### Returns + +`Promise`\<`string`\> + +command output + +### collaborator.remove() + +```ts +remove: (args) => Promise; +``` + +`slack collaborators remove` + +#### Parameters + +• **args**: `ProjectCommandArguments` & `CollaboratorEmail` + +#### Returns + +`Promise`\<`string`\> + +command output + +### create() + +```ts +create: (args) => Promise; +``` + +`slack create` + +#### Parameters + +• **args**: `ProjectCommandArguments` & `object` + +#### Returns + +`Promise`\<`string`\> + +command output + +### env + +```ts +env: object; +``` + +### env.add() + +```ts +add: (args) => Promise; +``` + +`slack env add` + +#### Parameters + +• **args**: `ProjectCommandArguments` & `EnvCommandArguments` + +#### Returns + +`Promise`\<`string`\> + +command output + +### env.list() + +```ts +list: (args) => Promise; +``` + +`slack env list` + +#### Parameters + +• **args**: `ProjectCommandArguments` + +#### Returns + +`Promise`\<`string`\> + +command output + +### env.remove() + +```ts +remove: (args) => Promise; +``` + +`slack env remove` + +#### Parameters + +• **args**: `ProjectCommandArguments` & `Pick`\<`EnvCommandArguments`, `"secretKey"`\> + +#### Returns + +`Promise`\<`string`\> + +command output + +### externalAuth + +```ts +externalAuth: object; +``` + +### externalAuth.add() + +```ts +add: (args) => Promise; +``` + +`slack external-auth add` + +#### Parameters + +• **args**: `ProjectCommandArguments` & `Pick`\<`ExternalAuthCommandArguments`, `"provider"`\> + +#### Returns + +`Promise`\<`string`\> + +command output + +### externalAuth.addSecret() + +```ts +addSecret: (args) => Promise; +``` + +`slack external-auth add-secret` + +#### Parameters + +• **args**: `ProjectCommandArguments` & `Omit`\<`ExternalAuthCommandArguments`, `"all"`\> + +#### Returns + +`Promise`\<`string`\> + +command output + +### externalAuth.remove() + +```ts +remove: (args) => Promise; +``` + +`slack external-auth remove` + +#### Parameters + +• **args**: `ProjectCommandArguments` & `Omit`\<`ExternalAuthCommandArguments`, `"secret"`\> + +#### Returns + +`Promise`\<`string`\> + +command output + +### externalAuth.selectAuth() + +```ts +selectAuth: (args) => Promise; +``` + +`slack external-auth select-auth` + +#### Parameters + +• **args**: `ProjectCommandArguments` & `Pick`\<`ExternalAuthCommandArguments`, `"provider"`\> & `object` + +#### Returns + +`Promise`\<`string`\> + +command output + +### function + +```ts +function: object = func; +``` + +### function.access() + +```ts +access: (args) => Promise; +``` + +`slack function access` + +#### Parameters + +• **args**: `ProjectCommandArguments` & `FunctionAccessArguments` + +#### Returns + +`Promise`\<`string`\> + +command output + +### manifest + +```ts +manifest: object; +``` + +### manifest.info() + +```ts +info: (args) => Promise; +``` + +`slack manifest info` + +#### Parameters + +• **args**: `ProjectCommandArguments` & `object` + +#### Returns + +`Promise`\<`string`\> + +command output + +### manifest.validate() + +```ts +validate: (args) => Promise; +``` + +`slack manifest validate` + +#### Parameters + +• **args**: `ProjectCommandArguments` + +#### Returns + +`Promise`\<`string`\> + +command output + +### platform + +```ts +platform: object; +``` + +### platform.activity() + +```ts +activity: (args) => Promise; +``` + +`slack platform activity` + +#### Parameters + +• **args**: `ProjectCommandArguments` & `object` + +#### Returns + +`Promise`\<`string`\> + +command output + +### platform.activityTailStart() + +```ts +activityTailStart: (args) => Promise; +``` + +`slack platform activity` but waits for a specified sequence then returns the shell +At the specific point where the sequence is found to continue with test + +#### Parameters + +• **args**: `ProjectCommandArguments` & `StringWaitArgument` + +#### Returns + +`Promise`\<`ShellProcess`\> + +command output + +### platform.activityTailStop() + +```ts +activityTailStop: (args) => Promise; +``` + +Waits for a specified string in the provided `activityTailStart` process output, +kills the process then returns the output + +#### Parameters + +• **args**: `StringWaitArgument` & `ProcessArgument` + +#### Returns + +`Promise`\<`string`\> + +command output + +### platform.deploy() + +```ts +deploy: (args) => Promise; +``` + +`slack deploy` + +#### Parameters + +• **args**: `ProjectCommandArguments` & `Omit`\<`RunDeployArguments`, `"cleanup"`\> + +#### Returns + +`Promise`\<`string`\> + +command output + +### platform.runStart() + +```ts +runStart: (args) => Promise; +``` + +start `slack run`. `runStop` must be used to stop the `run` process returned by this method. + +#### Parameters + +• **args**: `ProjectCommandArguments` & `RunDeployArguments` + +#### Returns + +`Promise`\<`ShellProcess`\> + +shell object to kill it explicitly in the test case via `runStop` + +### platform.runStop() + +```ts +runStop: (args) => Promise; +``` + +stop `slack run` + +#### Parameters + +• **args**: `ProcessArgument` & `object` + +#### Returns + +`Promise`\<`void`\> + +### stopSession() + +```ts +stopSession: (args) => Promise; +``` + +Delete app and Log out of current team session + +#### Parameters + +• **args**: `Partial`\<`ProjectCommandArguments`\> & `object` + +#### Returns + +`Promise`\<`void`\> + +### trigger + +```ts +trigger: object; +``` + +### trigger.access() + +```ts +access: (args) => Promise; +``` + +`slack trigger access` + +#### Parameters + +• **args**: `ProjectCommandArguments` & `TriggerAccessArguments` + +#### Returns + +`Promise`\<`string`\> + +command output + +### trigger.create() + +```ts +create: (args) => Promise; +``` + +`slack trigger create` + +#### Parameters + +• **args**: `ProjectCommandArguments` & `CreateArguments` + +#### Returns + +`Promise`\<`string`\> + +command output + +### trigger.delete() + +```ts +delete: (args) => Promise = del; +``` + +`slack trigger delete` + +#### Parameters + +• **args**: `ProjectCommandArguments` & `TriggerIdArgument` + +#### Returns + +`Promise`\<`string`\> + +command output + +### trigger.info() + +```ts +info: (args) => Promise; +``` + +`slack trigger info` + +#### Parameters + +• **args**: `ProjectCommandArguments` & `TriggerIdArgument` + +#### Returns + +`Promise`\<`string`\> + +command output + +### trigger.list() + +```ts +list: (args) => Promise; +``` + +`slack trigger list` + +#### Parameters + +• **args**: `ProjectCommandArguments` & `object` + +#### Returns + +`Promise`\<`string`\> + +command output + +### trigger.update() + +```ts +update: (args) => Promise; +``` + +`slack trigger update` + +#### Parameters + +• **args**: `ProjectCommandArguments` & `TriggerIdArgument` & `CreateFromFile` \| `Partial`\<`CreateFromArguments`\> + +#### Returns + +`Promise`\<`string`\> + +command output + +## Defined in + +[cli/index.ts:20](https://github.com/slackapi/node-slack-sdk/blob/main/packages/cli-test/src/cli/index.ts#L20) diff --git a/docs/content/reference/cli-test/variables/SlackProduct.md b/docs/content/reference/cli-test/variables/SlackProduct.md new file mode 100644 index 000000000..5db1c79a5 --- /dev/null +++ b/docs/content/reference/cli-test/variables/SlackProduct.md @@ -0,0 +1,47 @@ +# Variable: SlackProduct + +```ts +const SlackProduct: object; +``` + +## Type declaration + +### BUSINESS\_PLUS + +```ts +BUSINESS_PLUS: string = 'PLUS'; +``` + +### ENTERPRISE + +```ts +ENTERPRISE: string = 'ENTERPRISE'; +``` + +### ENTERPRISE\_SANDBOX + +```ts +ENTERPRISE_SANDBOX: string = 'ENTERPRISE_SANDBOX'; +``` + +### ENTERPRISE\_SELECT + +```ts +ENTERPRISE_SELECT: string = 'ENTERPRISE_SELECT'; +``` + +### FREE + +```ts +FREE: string = 'FREE'; +``` + +### PRO + +```ts +PRO: string = 'PRO'; +``` + +## Defined in + +[utils/constants.ts:48](https://github.com/slackapi/node-slack-sdk/blob/main/packages/cli-test/src/utils/constants.ts#L48) diff --git a/docs/content/reference/cli-test/variables/SlackTracerId.md b/docs/content/reference/cli-test/variables/SlackTracerId.md new file mode 100644 index 000000000..e52bb3693 --- /dev/null +++ b/docs/content/reference/cli-test/variables/SlackTracerId.md @@ -0,0 +1,253 @@ +# Variable: SlackTracerId + +```ts +const SlackTracerId: object; +``` + +This file should be kept up to date with the source of truth in the Slack CLI repo + +## Type declaration + +### SLACK\_TRACE\_ADMIN\_APPROVAL\_REQUEST\_PENDING + +```ts +SLACK_TRACE_ADMIN_APPROVAL_REQUEST_PENDING: string = 'SLACK_TRACE_ADMIN_APPROVAL_REQUEST_PENDING'; +``` + +### SLACK\_TRACE\_ADMIN\_APPROVAL\_REQUEST\_REASON\_SUBMITTED + +```ts +SLACK_TRACE_ADMIN_APPROVAL_REQUEST_REASON_SUBMITTED: string = 'SLACK_TRACE_ADMIN_APPROVAL_REQUEST_REASON_SUBMITTED'; +``` + +### SLACK\_TRACE\_ADMIN\_APPROVAL\_REQUEST\_SEND\_ERROR + +```ts +SLACK_TRACE_ADMIN_APPROVAL_REQUEST_SEND_ERROR: string = 'SLACK_TRACE_ADMIN_APPROVAL_REQUEST_SEND_ERROR'; +``` + +### SLACK\_TRACE\_ADMIN\_APPROVAL\_REQUEST\_SHOULD\_SEND + +```ts +SLACK_TRACE_ADMIN_APPROVAL_REQUEST_SHOULD_SEND: string = 'SLACK_TRACE_ADMIN_APPROVAL_REQUEST_SHOULD_SEND'; +``` + +### SLACK\_TRACE\_ADMIN\_APPROVAL\_REQUIRED + +```ts +SLACK_TRACE_ADMIN_APPROVAL_REQUIRED: string = 'SLACK_TRACE_ADMIN_APPROVAL_REQUIRED'; +``` + +### SLACK\_TRACE\_AUTH\_LIST\_COUNT + +```ts +SLACK_TRACE_AUTH_LIST_COUNT: string = 'SLACK_TRACE_AUTH_LIST_COUNT'; +``` + +### SLACK\_TRACE\_AUTH\_LIST\_INFO + +```ts +SLACK_TRACE_AUTH_LIST_INFO: string = 'SLACK_TRACE_AUTH_LIST_INFO'; +``` + +### SLACK\_TRACE\_AUTH\_LIST\_SUCCESS + +```ts +SLACK_TRACE_AUTH_LIST_SUCCESS: string = 'SLACK_TRACE_AUTH_LIST_SUCCESS'; +``` + +### SLACK\_TRACE\_AUTH\_LOGIN\_START + +```ts +SLACK_TRACE_AUTH_LOGIN_START: string = 'SLACK_TRACE_AUTH_LOGIN_START'; +``` + +### SLACK\_TRACE\_AUTH\_LOGIN\_SUCCESS + +```ts +SLACK_TRACE_AUTH_LOGIN_SUCCESS: string = 'SLACK_TRACE_AUTH_LOGIN_SUCCESS'; +``` + +### SLACK\_TRACE\_AUTH\_LOGOUT\_START + +```ts +SLACK_TRACE_AUTH_LOGOUT_START: string = 'SLACK_TRACE_AUTH_LOGOUT_START'; +``` + +### SLACK\_TRACE\_AUTH\_LOGOUT\_SUCCESS + +```ts +SLACK_TRACE_AUTH_LOGOUT_SUCCESS: string = 'SLACK_TRACE_AUTH_LOGOUT_SUCCESS'; +``` + +### SLACK\_TRACE\_AUTH\_REVOKE\_START + +```ts +SLACK_TRACE_AUTH_REVOKE_START: string = 'SLACK_TRACE_AUTH_REVOKE_START'; +``` + +### SLACK\_TRACE\_AUTH\_REVOKE\_SUCCESS + +```ts +SLACK_TRACE_AUTH_REVOKE_SUCCESS: string = 'SLACK_TRACE_AUTH_REVOKE_SUCCESS'; +``` + +### SLACK\_TRACE\_COLLABORATOR\_ADD\_COLLABORATOR + +```ts +SLACK_TRACE_COLLABORATOR_ADD_COLLABORATOR: string = 'SLACK_TRACE_COLLABORATOR_ADD_COLLABORATOR'; +``` + +### SLACK\_TRACE\_COLLABORATOR\_ADD\_SUCCESS + +```ts +SLACK_TRACE_COLLABORATOR_ADD_SUCCESS: string = 'SLACK_TRACE_COLLABORATOR_ADD_SUCCESS'; +``` + +### SLACK\_TRACE\_COLLABORATOR\_LIST\_COLLABORATOR + +```ts +SLACK_TRACE_COLLABORATOR_LIST_COLLABORATOR: string = 'SLACK_TRACE_COLLABORATOR_LIST_COLLABORATOR'; +``` + +### SLACK\_TRACE\_COLLABORATOR\_LIST\_COUNT + +```ts +SLACK_TRACE_COLLABORATOR_LIST_COUNT: string = 'SLACK_TRACE_COLLABORATOR_LIST_COUNT'; +``` + +### SLACK\_TRACE\_COLLABORATOR\_LIST\_SUCCESS + +```ts +SLACK_TRACE_COLLABORATOR_LIST_SUCCESS: string = 'SLACK_TRACE_COLLABORATOR_LIST_SUCCESS'; +``` + +### SLACK\_TRACE\_COLLABORATOR\_REMOVE\_COLLABORATOR + +```ts +SLACK_TRACE_COLLABORATOR_REMOVE_COLLABORATOR: string = 'SLACK_TRACE_COLLABORATOR_REMOVE_COLLABORATOR'; +``` + +### SLACK\_TRACE\_COLLABORATOR\_REMOVE\_SUCCESS + +```ts +SLACK_TRACE_COLLABORATOR_REMOVE_SUCCESS: string = 'SLACK_TRACE_COLLABORATOR_REMOVE_SUCCESS'; +``` + +### SLACK\_TRACE\_CREATE\_DEPENDENCIES\_SUCCESS + +```ts +SLACK_TRACE_CREATE_DEPENDENCIES_SUCCESS: string = 'SLACK_TRACE_CREATE_DEPENDENCIES_SUCCESS'; +``` + +### SLACK\_TRACE\_CREATE\_ERROR + +```ts +SLACK_TRACE_CREATE_ERROR: string = 'SLACK_TRACE_CREATE_ERROR'; +``` + +### SLACK\_TRACE\_CREATE\_PROJECT\_PATH + +```ts +SLACK_TRACE_CREATE_PROJECT_PATH: string = 'SLACK_TRACE_CREATE_PROJECT_PATH'; +``` + +### SLACK\_TRACE\_CREATE\_START + +```ts +SLACK_TRACE_CREATE_START: string = 'SLACK_TRACE_CREATE_START'; +``` + +### SLACK\_TRACE\_CREATE\_SUCCESS + +```ts +SLACK_TRACE_CREATE_SUCCESS: string = 'SLACK_TRACE_CREATE_SUCCESS'; +``` + +### SLACK\_TRACE\_CREATE\_TEMPLATE\_OPTIONS + +```ts +SLACK_TRACE_CREATE_TEMPLATE_OPTIONS: string = 'SLACK_TRACE_CREATE_TEMPLATE_OPTIONS'; +``` + +### SLACK\_TRACE\_DATASTORE\_COUNT\_DATASTORE + +```ts +SLACK_TRACE_DATASTORE_COUNT_DATASTORE: string = 'SLACK_TRACE_DATASTORE_COUNT_DATASTORE'; +``` + +### SLACK\_TRACE\_DATASTORE\_COUNT\_SUCCESS + +```ts +SLACK_TRACE_DATASTORE_COUNT_SUCCESS: string = 'SLACK_TRACE_DATASTORE_COUNT_SUCCESS'; +``` + +### SLACK\_TRACE\_DATASTORE\_COUNT\_TOTAL + +```ts +SLACK_TRACE_DATASTORE_COUNT_TOTAL: string = 'SLACK_TRACE_DATASTORE_COUNT_TOTAL'; +``` + +### SLACK\_TRACE\_FEEDBACK\_MESSAGE + +```ts +SLACK_TRACE_FEEDBACK_MESSAGE: string = 'SLACK_TRACE_FEEDBACK_MESSAGE'; +``` + +### SLACK\_TRACE\_MANIFEST\_VALIDATE\_SUCCESS + +```ts +SLACK_TRACE_MANIFEST_VALIDATE_SUCCESS: string = 'SLACK_TRACE_MANIFEST_VALIDATE_SUCCESS'; +``` + +### SLACK\_TRACE\_PLATFORM\_DEPLOY\_SUCCESS + +```ts +SLACK_TRACE_PLATFORM_DEPLOY_SUCCESS: string = 'SLACK_TRACE_PLATFORM_DEPLOY_SUCCESS'; +``` + +### SLACK\_TRACE\_PLATFORM\_RUN\_READY + +```ts +SLACK_TRACE_PLATFORM_RUN_READY: string = 'SLACK_TRACE_PLATFORM_RUN_READY'; +``` + +### SLACK\_TRACE\_PLATFORM\_RUN\_START + +```ts +SLACK_TRACE_PLATFORM_RUN_START: string = 'SLACK_TRACE_PLATFORM_RUN_START'; +``` + +### SLACK\_TRACE\_PLATFORM\_RUN\_STOP + +```ts +SLACK_TRACE_PLATFORM_RUN_STOP: string = 'SLACK_TRACE_PLATFORM_RUN_STOP'; +``` + +### SLACK\_TRACE\_TRIGGERS\_ACCESS\_ERROR + +```ts +SLACK_TRACE_TRIGGERS_ACCESS_ERROR: string = 'SLACK_TRACE_TRIGGERS_ACCESS_ERROR'; +``` + +### SLACK\_TRACE\_TRIGGERS\_ACCESS\_SUCCESS + +```ts +SLACK_TRACE_TRIGGERS_ACCESS_SUCCESS: string = 'SLACK_TRACE_TRIGGERS_ACCESS_SUCCESS'; +``` + +### SLACK\_TRACE\_TRIGGERS\_CREATE\_SUCCESS + +```ts +SLACK_TRACE_TRIGGERS_CREATE_SUCCESS: string = 'SLACK_TRACE_TRIGGERS_CREATE_SUCCESS'; +``` + +### SLACK\_TRACE\_TRIGGERS\_CREATE\_URL + +```ts +SLACK_TRACE_TRIGGERS_CREATE_URL: string = 'SLACK_TRACE_TRIGGERS_CREATE_URL'; +``` + +## Defined in + +[utils/constants.ts:5](https://github.com/slackapi/node-slack-sdk/blob/main/packages/cli-test/src/utils/constants.ts#L5) diff --git a/docs/content/reference/cli-test/variables/shell.md b/docs/content/reference/cli-test/variables/shell.md new file mode 100644 index 000000000..0bf0fde4e --- /dev/null +++ b/docs/content/reference/cli-test/variables/shell.md @@ -0,0 +1,151 @@ +# Variable: shell + +```ts +const shell: object; +``` + +## Type declaration + +### assembleShellEnv() + +```ts +assembleShellEnv: () => Record; +``` + +#### Returns + +`Record`\<`string`, `undefined` \| `string`\> + +### checkIfFinished() + +```ts +checkIfFinished: (proc) => Promise; +``` + +Logic to wait for child process to finish executing +- Check if the close event was emitted, else wait for 1 sec +- Error out if > 30 sec + +#### Parameters + +• **proc**: `ShellProcess` + +#### Returns + +`Promise`\<`void`\> + +### kill() + +```ts +kill: (proc) => Promise; +``` + +#### Parameters + +• **proc**: `ShellProcess` + +#### Returns + +`Promise`\<`boolean`\> + +### removeANSIcolors() + +```ts +removeANSIcolors: (text) => string; +``` + +Remove all the ANSI color and style encoding + +#### Parameters + +• **text**: `string` + +string + +#### Returns + +`string` + +### runCommandSync() + +```ts +runCommandSync: (command, shellOpts?) => string; +``` + +#### Parameters + +• **command**: `string` + +cli command + +• **shellOpts?**: `Partial`\<`SpawnOptionsWithoutStdio`\> + +various shell spawning options available to customize + +#### Returns + +`string` + +command stdout + +### sleep() + +```ts +sleep: (timeout) => Promise; +``` + +Sleep function used to wait for cli to finish executing + +#### Parameters + +• **timeout**: `number` = `1000` + +#### Returns + +`Promise`\<`void`\> + +### spawnProcess() + +```ts +spawnProcess: (command, shellOpts?) => ShellProcess; +``` + +#### Parameters + +• **command**: `string` + +The command to run, e.g. `echo "hi"` + +• **shellOpts?**: `Partial`\<`SpawnOptionsWithoutStdio`\> + +Options to customize shell execution + +#### Returns + +`ShellProcess` + +command output + +### waitForOutput() + +```ts +waitForOutput: (expString, proc) => Promise; +``` + +Wait for output + +#### Parameters + +• **expString**: `string` + +expected string + +• **proc**: `ShellProcess` + +#### Returns + +`Promise`\<`void`\> + +## Defined in + +[cli/shell.ts:10](https://github.com/slackapi/node-slack-sdk/blob/main/packages/cli-test/src/cli/shell.ts#L10) diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 00a734e25..19a0fe56b 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -67,6 +67,15 @@ const config = { ], }, ], + [ + 'docusaurus-plugin-typedoc', + { + id: 'cli-test', + entryPoints: ['../packages/cli-test/src/index.ts'], + tsconfig: '../packages/cli-test/tsconfig.json', + out: "./content/reference/cli-test", + } + ], [ 'docusaurus-plugin-typedoc', { diff --git a/docs/sidebars.js b/docs/sidebars.js index 823c3de74..b30e37290 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -54,6 +54,15 @@ const sidebars = { type: 'category', label: 'Reference', items: [ + { + type: 'category', + label: '@slack/cli-test', + link: { + type: 'doc', + id: 'reference/cli-test/index', + }, + items: require('./content/reference/cli-test/typedoc-sidebar.cjs'), + }, { type: 'category', label: '@slack/events-api', diff --git a/packages/cli-test/package.json b/packages/cli-test/package.json index 653fc5079..0d6ba3dad 100644 --- a/packages/cli-test/package.json +++ b/packages/cli-test/package.json @@ -25,7 +25,7 @@ "build": "npm run build:clean && tsc", "build:clean": "shx rm -rf ./dist ./coverage", "prepare": "npm run build", - "mocha": "cross-env SLACK_CLI_PATH=/doesnt/matter mocha --config .mocharc.json src/*.spec.ts src/**/*.spec.ts", + "mocha": "cross-env SLACK_CLI_PATH=/doesnt/matter mocha --config .mocharc.json src/*.spec.ts src/**/*.spec.ts src/**/**/*.spec.ts", "test": "npm run lint && npm run build && c8 npm run mocha" }, "dependencies": { diff --git a/packages/cli-test/src/cli/cli-process.spec.ts b/packages/cli-test/src/cli/cli-process.spec.ts index 6f7aaf025..c607c867b 100644 --- a/packages/cli-test/src/cli/cli-process.spec.ts +++ b/packages/cli-test/src/cli/cli-process.spec.ts @@ -49,6 +49,16 @@ describe('SlackCLIProcess class', () => { sandbox.assert.neverCalledWithMatch(spawnProcessSpy, '--apihost qa.slack.com'); spawnProcessSpy.resetHistory(); }); + it('should map apihost option to provided host', async () => { + let cmd = new SlackCLIProcess('help', { apihost: 'dev123.slack.com' }); + await cmd.execAsync(); + sandbox.assert.calledWithMatch(spawnProcessSpy, '--apihost dev123.slack.com'); + spawnProcessSpy.resetHistory(); + cmd = new SlackCLIProcess('help'); + await cmd.execAsync(); + sandbox.assert.neverCalledWithMatch(spawnProcessSpy, '--apihost dev123.slack.com'); + spawnProcessSpy.resetHistory(); + }); it('should default to passing --skip-update but allow overriding that', async () => { let cmd = new SlackCLIProcess('help'); await cmd.execAsync(); @@ -66,6 +76,38 @@ describe('SlackCLIProcess class', () => { await cmd.execAsync(); sandbox.assert.calledWithMatch(spawnProcessSpy, '--skip-update'); }); + it('should default to `--app deployed` but allow overriding that via the `app` parameter', async () => { + let cmd = new SlackCLIProcess('help'); + await cmd.execAsync(); + sandbox.assert.calledWithMatch(spawnProcessSpy, '--app deployed'); + spawnProcessSpy.resetHistory(); + cmd = new SlackCLIProcess('help', { app: 'local' }); + await cmd.execAsync(); + sandbox.assert.calledWithMatch(spawnProcessSpy, '--app local'); + }); + it('should default to `--force` but allow overriding that via the `force` parameter', async () => { + let cmd = new SlackCLIProcess('help'); + await cmd.execAsync(); + sandbox.assert.calledWithMatch(spawnProcessSpy, '--force'); + spawnProcessSpy.resetHistory(); + cmd = new SlackCLIProcess('help', { force: true }); + await cmd.execAsync(); + sandbox.assert.calledWithMatch(spawnProcessSpy, '--force'); + spawnProcessSpy.resetHistory(); + cmd = new SlackCLIProcess('help', { force: false }); + await cmd.execAsync(); + sandbox.assert.neverCalledWithMatch(spawnProcessSpy, '--force'); + }); + it('should map token option to `--token`', async () => { + let cmd = new SlackCLIProcess('help', { token: 'xoxb-1234' }); + await cmd.execAsync(); + sandbox.assert.calledWithMatch(spawnProcessSpy, '--token xoxb-1234'); + spawnProcessSpy.resetHistory(); + cmd = new SlackCLIProcess('help'); + await cmd.execAsync(); + sandbox.assert.neverCalledWithMatch(spawnProcessSpy, '--token xoxb-1234'); + spawnProcessSpy.resetHistory(); + }); }); describe('command options', () => { it('should pass command-level key/value options to command in the form `-- value`', async () => { diff --git a/packages/cli-test/src/cli/cli-process.ts b/packages/cli-test/src/cli/cli-process.ts index bbebd8550..b59d9d52c 100644 --- a/packages/cli-test/src/cli/cli-process.ts +++ b/packages/cli-test/src/cli/cli-process.ts @@ -1,19 +1,30 @@ import { shell } from './shell'; -import type { ShellProcess } from '../utils/types'; +import type { ShellProcess } from '../types/shell'; import type { SpawnOptionsWithoutStdio } from 'node:child_process'; -/* - * some parameters used in the 'shell' calls that are CLI-specific and probably should not exist there: - * @param skipUpdate skip auto update notification - */ export interface SlackCLIGlobalOptions { + /** + * @description The API host the command should interact with, full domain name. (`--apihost qa1234.slack.com`) + * Takes precendence over `qa` and `dev` options. + * @example `qa1234.slack.com` or `dev2345.slack.com` + */ + apihost?: string; /** * @description Whether the command should interact with dev.slack (`--slackdev`) + * `qa` and `apihost` will both supersede this option. */ dev?: boolean; + /** @description Ignore warnings and continue executing command. Defaults to `true`. */ + force?: boolean; + /** + * @description Application instance to target. Can be `local`, `deployed` or an app ID string. + * Defaults to `deployed`. + */ + app?: 'local' | 'deployed' | string; /** * @description Whether the command should interact with qa.slack (`--apihost qa.slack.com`) + * Takes precendence over `dev` option but is superseded by `apihost`. */ qa?: boolean; /** @@ -21,12 +32,18 @@ export interface SlackCLIGlobalOptions { */ skipUpdate?: boolean; /** - * @description workspace or organization name or ID to scope command to + * @description The ID of your team. If you are using a Standard Slack plan, this is your workspace ID. + * If you are using an Enterprise Grid plan, this is the organization ID, even if your app is only granted to a + * subset of workspaces within the org. */ team?: string; + /** @description Access token to use when making Slack API calls. */ + token?: string; } -export type SlackCLICommandOptions = Record; +export type SlackCLIHostTargetOptions = Pick; + +export type SlackCLICommandOptions = Record; export class SlackCLIProcess { /** @@ -88,20 +105,38 @@ export class SlackCLIProcess { let cmd = `${process.env.SLACK_CLI_PATH}`; if (this.globalOptions) { const opts = this.globalOptions; - if (opts.qa) { + // Determine API host target + if (opts.apihost) { + cmd += ` --apihost ${opts.apihost}`; + } else if (opts.qa) { cmd += ' --apihost qa.slack.com'; - } - if (opts.dev) { + } else if (opts.dev) { cmd += ' --slackdev'; } + // Always skip update unless explicitly set to something falsy if (opts.skipUpdate || opts.skipUpdate === undefined) { cmd += ' --skip-update'; } + // Target team if (opts.team) { cmd += ` --team ${opts.team}`; } + // App instance; defaults to `deployed` + if (opts.app) { + cmd += ` --app ${opts.app}`; + } else { + cmd += ' --app deployed'; + } + // Ignore warnings via --force; defaults to true + if (opts.force || typeof opts.force === 'undefined') { + cmd += ' --force'; + } + // Specifying custom token + if (opts.token) { + cmd += ` --token ${opts.token}`; + } } else { - cmd += ' --skip-update'; + cmd += ' --skip-update --force --app deployed'; } cmd += ` ${this.command}`; if (this.commandOptions) { diff --git a/packages/cli-test/src/cli/command-error.ts b/packages/cli-test/src/cli/command-error.ts deleted file mode 100644 index 8c672f624..000000000 --- a/packages/cli-test/src/cli/command-error.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { CustomError } from '../utils/custom-errors'; - -// TODO: this error wrapper should maybe look at official node docs for how to extend errors -// https://nodejs.org/api/errors.html#errors -/** - * Error handler for Lib - * @param error error object to wrap - * @param command command used - * @param additionalInfo any extra info - * @returns The wrapped CustomError object - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export default function commandError(error: any, command: string, additionalInfo?: string): CustomError { - // Specify error name, if it's a generic Error - if (error.name) { - // eslint-disable-next-line no-param-reassign - error.name = error.name.toString() === 'Error' ? 'commandError' : 'Error'; - } - - // Create new error and return it - const newError = new CustomError(error.message, error.name, error.stack, { - command, - additionalInfo, - }); - return newError; -} diff --git a/packages/cli-test/src/cli/commands/app.spec.ts b/packages/cli-test/src/cli/commands/app.spec.ts new file mode 100644 index 000000000..f618a221d --- /dev/null +++ b/packages/cli-test/src/cli/commands/app.spec.ts @@ -0,0 +1,52 @@ +import sinon from 'sinon'; + +import app from './app'; +import { mockProcess } from '../../utils/test'; +import { shell } from '../shell'; + +describe('app commands', () => { + const sandbox = sinon.createSandbox(); + let spawnSpy: sinon.SinonStub; + + beforeEach(() => { + const process = mockProcess(); + spawnSpy = sandbox.stub(shell, 'spawnProcess').returns({ + command: 'something', + finished: true, + output: 'hi', + process, + }); + sandbox.stub(shell, 'checkIfFinished').resolves(); + }); + afterEach(() => { + sandbox.restore(); + }); + + describe('delete method', () => { + it('should invoke `app delete` and default force=true', async () => { + await app.delete({ appPath: '/some/path' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('--force')); + sandbox.assert.calledWith(spawnSpy, sinon.match('app delete')); + }); + it('should invoke with `--force` if force=true', async () => { + await app.delete({ appPath: '/some/path', force: true }); + sandbox.assert.calledWith(spawnSpy, sinon.match('--force')); + }); + it('should invoke without `--force` if force=false', async () => { + await app.delete({ appPath: '/some/path', force: false }); + sandbox.assert.neverCalledWith(spawnSpy, sinon.match('--force')); + }); + }); + describe('install method', () => { + it('should invoke a CLI process with `app install`', async () => { + await app.install({ appPath: '/some/path' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('app install')); + }); + }); + describe('list method', () => { + it('should invoke a CLI process with `app list`', async () => { + await app.list({ appPath: '/some/path' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('app list')); + }); + }); +}); diff --git a/packages/cli-test/src/cli/commands/app.ts b/packages/cli-test/src/cli/commands/app.ts index bb6135320..07c799aaf 100644 --- a/packages/cli-test/src/cli/commands/app.ts +++ b/packages/cli-test/src/cli/commands/app.ts @@ -1,79 +1,45 @@ import { SlackCLIProcess } from '../cli-process'; -import commandError from '../command-error'; + +import type { ProjectCommandArguments } from '../../types/commands/common_arguments'; /** * `slack app delete` - * @param appPath path to app - * @param teamFlag team domain for the function's app * @returns command output */ -export const del = async function appDelete( - appPath: string, - teamFlag: string, - options?: { isLocalApp?: boolean, qa?: boolean }, -): Promise { - // TODO: breaking change, separate params vs single-param-object, probably should reflect global vs command CLI flags - const appEnvironment = options?.isLocalApp ? 'local' : 'deployed'; - const cmd = new SlackCLIProcess('app delete --force', { team: teamFlag, qa: options?.qa }, { - '--app': appEnvironment, +export const del = async function appDelete(args: ProjectCommandArguments): Promise { + const cmd = new SlackCLIProcess('app delete', args); + const proc = await cmd.execAsync({ + cwd: args.appPath, }); - try { - const proc = await cmd.execAsync({ - cwd: appPath, - }); - return proc.output; - } catch (error) { - throw commandError(error, 'appDelete'); - } + return proc.output; }; /** * `slack app install` - * @param appPath path to app - * @param teamFlag team domain where the app will be installed * @returns command output */ -export const install = async function workspaceInstall( - appPath: string, - teamFlag: string, - options?: { qa?: boolean }, -): Promise { - // TODO: breaking change, separate params vs single-param-object, probably should reflect global vs command CLI flags - const cmd = new SlackCLIProcess('app install', { team: teamFlag, qa: options?.qa }); - try { - const proc = await cmd.execAsync({ - cwd: appPath, - }); - return proc.output; - } catch (error) { - throw commandError(error, 'appInstall'); - } +export const install = async function workspaceInstall(args: ProjectCommandArguments): Promise { + const cmd = new SlackCLIProcess('app install', args); + const proc = await cmd.execAsync({ + cwd: args.appPath, + }); + return proc.output; }; /** * `slack app list` - * @param appPath path to app * @returns command output */ -export const list = async function appList( - appPath: string, - options?: { qa?: boolean }, -): Promise { - // TODO: (breaking change) separate parameters vs single-param-object - const cmd = new SlackCLIProcess('app list', options); - try { - const proc = await cmd.execAsync({ - cwd: appPath, - }); - return proc.output; - } catch (error) { - throw commandError(error, 'appList'); - } +export const list = async function appList(args: ProjectCommandArguments): Promise { + const cmd = new SlackCLIProcess('app list', args); + const proc = await cmd.execAsync({ + cwd: args.appPath, + }); + return proc.output; }; -// TODO: (breaking change): rename properties of this default export to match actual command names export default { - workspaceDelete: del, - workspaceInstall: install, - workspaceList: list, + delete: del, + install, + list, }; diff --git a/packages/cli-test/src/cli/commands/auth.spec.ts b/packages/cli-test/src/cli/commands/auth.spec.ts new file mode 100644 index 000000000..65c814876 --- /dev/null +++ b/packages/cli-test/src/cli/commands/auth.spec.ts @@ -0,0 +1,69 @@ +import sinon from 'sinon'; + +import auth from './auth'; +import { mockProcess } from '../../utils/test'; +import { shell } from '../shell'; + +describe('auth commands', () => { + const sandbox = sinon.createSandbox(); + let spawnSpy: sinon.SinonStub; + + beforeEach(() => { + const process = mockProcess(); + spawnSpy = sandbox.stub(shell, 'spawnProcess').returns({ + command: 'something', + finished: true, + output: 'hi', + process, + }); + sandbox.stub(shell, 'checkIfFinished').resolves(); + }); + afterEach(() => { + sandbox.restore(); + }); + + describe('loginNoPrompt method', () => { + it('should invoke `login --no-prompt`', async () => { + spawnSpy.returns({ + command: 'something', + finished: true, + output: '/slackauthticket 123456', + process: mockProcess(), + }); + const resp = await auth.loginNoPrompt(); + sandbox.assert.calledWith(spawnSpy, sinon.match('login')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--no-prompt')); + sandbox.assert.match(resp.authTicket, '123456'); + sandbox.assert.match(resp.authTicketSlashCommand, '/slackauthticket 123456'); + }); + }); + describe('loginChallengeExchange method', () => { + it('should invoke `login --no-prompt --challenge --ticket`', async () => { + await auth.loginChallengeExchange({ + authTicket: '123456', + challenge: 'batman', + }); + sandbox.assert.calledWith(spawnSpy, sinon.match('login')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--no-prompt')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--challenge batman')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--ticket 123456')); + }); + }); + describe('logout method', () => { + it('should invoke a CLI process with `logout`', async () => { + await auth.logout(); + sandbox.assert.calledWith(spawnSpy, sinon.match('logout')); + }); + it('should invoke a CLI process with `logout --team` if both `team` and `all` are specified', async () => { + await auth.logout({ team: 'T1234', all: true }); + sandbox.assert.calledWith(spawnSpy, sinon.match('logout')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--team T1234')); + sandbox.assert.neverCalledWith(spawnSpy, sinon.match('--all')); + }); + it('should invoke a CLI process with `logout --all` if `all` specified', async () => { + await auth.logout({ all: true }); + sandbox.assert.calledWith(spawnSpy, sinon.match('logout')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--all')); + }); + }); +}); diff --git a/packages/cli-test/src/cli/commands/auth.ts b/packages/cli-test/src/cli/commands/auth.ts index eabc89ccc..30a30433e 100644 --- a/packages/cli-test/src/cli/commands/auth.ts +++ b/packages/cli-test/src/cli/commands/auth.ts @@ -1,111 +1,89 @@ -import { SlackCLICommandOptions, SlackCLIGlobalOptions, SlackCLIProcess } from '../cli-process'; -import commandError from '../command-error'; +import { + SlackCLICommandOptions, + SlackCLIGlobalOptions, + SlackCLIHostTargetOptions, + SlackCLIProcess, +} from '../cli-process'; -import type { ShellProcess } from '../../utils/types'; +import type { ShellProcess } from '../../types/shell'; export default { /** - * `slack login --no-prompt` + * `slack login --no-prompt`; initiates a CLI login flow. The `authTicketSlashCommand` returned should be entered + * into the Slack client, and the challenge code retrieved and fed into the `loginChallengeExchange` method to + * complete the CLI login flow. */ - loginNoPrompt: async function loginNoPrompt(options?: { qa?: boolean }): Promise<{ + loginNoPrompt: async function loginNoPrompt(args?: SlackCLIHostTargetOptions): Promise<{ + /** @description Command output */ + output: ShellProcess['output']; /** - * Command output - */ - shellOutput: ShellProcess['output']; - /** - * Slash command with auth ticket, e.g. '/slackauthticket MTMxNjgxMDUtYTYwOC00NzRhLWE3M2YtMjVmZTQyMjc1MDg4' + * @description Slash command that a Slack client user must enter into the message composer in order to initiate + * login flow. + * @example `/slackauthticket MTMxNjgxMDUtYTYwOC00NzRhLWE3M2YtMjVmZTQyMjc1MDg4` */ authTicketSlashCommand: string; /** * An auth ticket is a A UUID sequence granted by Slack to a CLI auth requestor. * That ticket must then be submitted via slash command by a user logged in to Slack and permissions accepted to be - * granted a token for use. + * granted a token for use. This `authTicket` is included in the `authTicketSlashCommand`. */ authTicket: string; }> { - const cmd = new SlackCLIProcess('login', options, { + const cmd = new SlackCLIProcess('login', args, { '--no-prompt': true, }); - try { - const proc = await cmd.execAsync(); + const proc = await cmd.execAsync(); - // Get auth token - const authTicketSlashCommand = proc.output.match('/slackauthticket(.*)')![0]; - const authTicket = authTicketSlashCommand.split(' ')[1]; + // Get auth token + const authTicketSlashCommand = proc.output.match('/slackauthticket(.*)')![0]; + const authTicket = authTicketSlashCommand.split(' ')[1]; - return { - shellOutput: proc.output, - authTicketSlashCommand, - authTicket, - }; - } catch (error) { - throw commandError( - error, - this.loginNoPrompt.name, - 'Error running command. \nTip: You must have no active authenticated sessions in cli', - ); - } + return { + output: proc.output, + authTicketSlashCommand, + authTicket, + }; }, - // TODO: (breaking change) inconsistent use of object-as-params vs. separate parameters /** * `slack login --no-prompt --challenge --ticket` - * @param challenge challenge string from UI * @param authTicket authTicket string from loginNoPrompt * @param options * @returns */ - loginChallengeExchange: async function loginChallengeExchange( + loginChallengeExchange: async function loginChallengeExchange(args: SlackCLIHostTargetOptions & { + /** @description Challenge string extracted from the Slack client UI after submitting the auth slash command. */ challenge: string, + /** @description The `authTicket` output from `loginNoPrompt`; required to complete the login flow. */ authTicket: string, - options?: { - qa?: boolean; - }, - ): Promise { - const cmd = new SlackCLIProcess('login', options, { + }): Promise { + const cmd = new SlackCLIProcess('login', args, { '--no-prompt': true, - '--challenge': challenge, - '--ticket': authTicket, + '--challenge': args.challenge, + '--ticket': args.authTicket, }); - try { - const proc = await cmd.execAsync(); - return proc.output; - } catch (error) { - throw commandError( - error, - this.loginChallengeExchange.name, - 'Error running command. \nTip: You must be authenticated in Slack client and have valid challenge and authTicket', - ); - } + const proc = await cmd.execAsync(); + return proc.output; }, /** * `slack logout` * @returns command output */ - logout: async function logout(options?: { - // TODO: (breaking change) the two flags here are mutually exclusive; model better using an `|` of types - /** team domain to logout from */ - teamFlag?: string; - /** perform the logout for all authentications */ - allWorkspaces?: boolean; - qa?: boolean; - }): Promise { - // TODO: (breaking change) inconsistent use of object-as-params vs. separate parameters + logout: async function logout(args?: Omit & (Pick | { + /** + * @description Perform the logout for all authentications. + * The `team` argument takes precendence over this argument. + */ + all?: boolean; + })): Promise { // Create the command with workspaces to logout of - const globalOpts: SlackCLIGlobalOptions = { qa: options?.qa }; const cmdOpts: SlackCLICommandOptions = {}; - if (options?.teamFlag) { - globalOpts.team = options.teamFlag; - } else if (options?.allWorkspaces) { + if (args && 'all' in args && !('team' in args) && args.all) { cmdOpts['--all'] = true; } - const cmd = new SlackCLIProcess('logout', globalOpts, cmdOpts); - try { - const proc = await cmd.execAsync(); - return proc.output; - } catch (error) { - throw commandError(error, 'logout'); - } + const cmd = new SlackCLIProcess('logout', args, cmdOpts); + const proc = await cmd.execAsync(); + return proc.output; }, }; diff --git a/packages/cli-test/src/cli/commands/collaborator.spec.ts b/packages/cli-test/src/cli/commands/collaborator.spec.ts new file mode 100644 index 000000000..c68a70ea3 --- /dev/null +++ b/packages/cli-test/src/cli/commands/collaborator.spec.ts @@ -0,0 +1,43 @@ +import sinon from 'sinon'; + +import collaborator from './collaborator'; +import { mockProcess } from '../../utils/test'; +import { shell } from '../shell'; + +describe('collaborator commands', () => { + const sandbox = sinon.createSandbox(); + let spawnSpy: sinon.SinonStub; + + beforeEach(() => { + const process = mockProcess(); + spawnSpy = sandbox.stub(shell, 'spawnProcess').returns({ + command: 'something', + finished: true, + output: 'hi', + process, + }); + sandbox.stub(shell, 'checkIfFinished').resolves(); + }); + afterEach(() => { + sandbox.restore(); + }); + + describe('add method', () => { + it('should invoke `collaborators add `', async () => { + await collaborator.add({ appPath: '/some/path', collaboratorEmail: 'you@me.com' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('collaborators add you@me.com')); + }); + }); + describe('list method', () => { + it('should invoke `collaborators list`', async () => { + await collaborator.list({ appPath: '/some/path' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('collaborators list')); + }); + }); + describe('remove method', () => { + it('should invoke `collaborators remove `', async () => { + await collaborator.remove({ appPath: '/some/path', collaboratorEmail: 'you@me.com' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('collaborators remove you@me.com')); + }); + }); +}); diff --git a/packages/cli-test/src/cli/commands/collaborator.ts b/packages/cli-test/src/cli/commands/collaborator.ts index 3d191ef8c..698fdf421 100644 --- a/packages/cli-test/src/cli/commands/collaborator.ts +++ b/packages/cli-test/src/cli/commands/collaborator.ts @@ -1,82 +1,52 @@ +import { ProjectCommandArguments } from '../../types/commands/common_arguments'; import { SlackCLIProcess } from '../cli-process'; -import commandError from '../command-error'; + +export interface CollaboratorEmail { + /** @description email of the collaborator */ + collaboratorEmail: string, +} /** * `slack collaborators add` - * @param appPath path to app - * @param teamFlag team domain to add collaborators to - * @param collaboratorEmail email of the user to be added as a collaborator * @returns command output */ -export const add = async function collaboratorsAdd( - appPath: string, - teamFlag: string, - collaboratorEmail: string, - options?: { qa?: boolean }, -): Promise { - // TODO: (breaking change) separate parameters vs single-param-object - const cmd = new SlackCLIProcess(`collaborators add ${collaboratorEmail}`, { team: teamFlag, qa: options?.qa }); - try { - const proc = await cmd.execAsync({ - cwd: appPath, - }); - return proc.output; - } catch (error) { - throw commandError(error, 'collaboratorsAdd'); - } +export const add = async function collaboratorsAdd(args: ProjectCommandArguments & CollaboratorEmail): Promise { + const cmd = new SlackCLIProcess(`collaborators add ${args.collaboratorEmail}`, args); + const proc = await cmd.execAsync({ + cwd: args.appPath, + }); + return proc.output; }; /** * `slack collaborators list` - * @param appPath path to app - * @param teamFlag team domain to list collaborators for * @returns command output */ -export const list = async function collaboratorsList( - appPath: string, - teamFlag: string, - options?: { qa?: boolean }, -): Promise { - // TODO: (breaking change) separate parameters vs single-param-object - const cmd = new SlackCLIProcess('collaborators list', { team: teamFlag, qa: options?.qa }); - try { - const proc = await cmd.execAsync({ - cwd: appPath, - }); - return proc.output; - } catch (error) { - throw commandError(error, 'collaboratorsList'); - } +export const list = async function collaboratorsList(args: ProjectCommandArguments): Promise { + const cmd = new SlackCLIProcess('collaborators list', args); + const proc = await cmd.execAsync({ + cwd: args.appPath, + }); + return proc.output; }; /** * `slack collaborators remove` - * @param appPath path to app - * @param teamFlag team domain to remove collaborators from * @param collaboratorEmail email of the user to be removed as a collaborator * @returns command output */ export const remove = async function collaboratorsRemove( - appPath: string, - teamFlag: string, - collaboratorEmail: string, - options?: { qa?: boolean }, + args: ProjectCommandArguments & CollaboratorEmail, ): Promise { - // TODO: (breaking change) separate parameters vs single-param-object - const cmd = new SlackCLIProcess(`collaborators remove ${collaboratorEmail}`, { team: teamFlag, qa: options?.qa }); - try { - const proc = await cmd.execAsync({ - cwd: appPath, - }); - return proc.output; - } catch (error) { - throw commandError(error, 'collaboratorsRemove'); - } + const cmd = new SlackCLIProcess(`collaborators remove ${args.collaboratorEmail}`, args); + const proc = await cmd.execAsync({ + cwd: args.appPath, + }); + return proc.output; }; -// TODO: (breaking change): rename properties of this default export to match actual command names export default { - collaboratorsAdd: add, - collaboratorsList: list, - collaboratorsRemove: remove, + add, + list, + remove, }; diff --git a/packages/cli-test/src/cli/commands/create.spec.ts b/packages/cli-test/src/cli/commands/create.spec.ts new file mode 100644 index 000000000..153fbb745 --- /dev/null +++ b/packages/cli-test/src/cli/commands/create.spec.ts @@ -0,0 +1,42 @@ +import sinon from 'sinon'; + +import { create } from './create'; +import { mockProcess } from '../../utils/test'; +import { shell } from '../shell'; + +describe('create', () => { + const sandbox = sinon.createSandbox(); + let spawnSpy: sinon.SinonStub; + + beforeEach(() => { + const process = mockProcess(); + spawnSpy = sandbox.stub(shell, 'spawnProcess').returns({ + command: 'something', + finished: true, + output: 'hi', + process, + }); + sandbox.stub(shell, 'checkIfFinished').resolves(); + }); + afterEach(() => { + sandbox.restore(); + }); + + describe('method', () => { + it('should invoke `create `', async () => { + await create({ appPath: 'myApp' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('create myApp')); + }); + it('should invoke `create --template` if template specified', async () => { + await create({ appPath: 'myApp', template: 'slack-samples/deno-hello-world' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('create myApp')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--template slack-samples/deno-hello-world')); + }); + it('should invoke `create --template --branch` if both template and branch specified', async () => { + await create({ appPath: 'myApp', template: 'slack-samples/deno-hello-world', branch: 'feat-functions' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('create myApp')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--template slack-samples/deno-hello-world')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--branch feat-functions')); + }); + }); +}); diff --git a/packages/cli-test/src/cli/commands/create.ts b/packages/cli-test/src/cli/commands/create.ts index 20d68598e..ce95a6e36 100644 --- a/packages/cli-test/src/cli/commands/create.ts +++ b/packages/cli-test/src/cli/commands/create.ts @@ -1,65 +1,28 @@ -import { SpawnOptionsWithoutStdio } from 'node:child_process'; - -import { SlackCLICommandOptions, SlackCLIGlobalOptions, SlackCLIProcess } from '../cli-process'; -import commandError from '../command-error'; +import { ProjectCommandArguments } from '../../types/commands/common_arguments'; +import { SlackCLICommandOptions, SlackCLIProcess } from '../cli-process'; /** * `slack create` - * @param opts generic command options to pass to `create` * @returns command output */ export const create = async function create( - appName?: string, // TODO: bad arg name. it should be app path, because this is effectively how it is used - globalOpts?: SlackCLIGlobalOptions, - commandOpts?: SlackCLICommandOptions, - shellOpts?: SpawnOptionsWithoutStdio, + args: ProjectCommandArguments & { + /** @description URL to an app template to use when creating app. */ + template?: string; + /** @description Branch to use from the provided `template`. */ + branch?: string; + }, ): Promise { - // TODO: single object param vs separate params (breaking change) - let cmdStr = 'create'; - if (appName) { - cmdStr += ` ${appName}`; - } - const cmd = new SlackCLIProcess(cmdStr, globalOpts, commandOpts); - try { - const proc = await cmd.execAsync(shellOpts); - return proc.output; - } catch (error) { - throw commandError(error, 'create'); + const cmdOpts: SlackCLICommandOptions = {}; + if ('template' in args) { + cmdOpts['--template'] = args.template; + if ('branch' in args) { + cmdOpts['--branch'] = args.branch; + } } + const cmd = new SlackCLIProcess(`create ${args.appPath}`, args, cmdOpts); + const proc = await cmd.execAsync(); + return proc.output; }; -// TODO: (breaking change) remove this method -/** - * `slack create` using a template - * Creates an app from a specified template string. - * @param templateString template string (ex: `slack-samples/deno-hello-world`) - * @param appName desired app name - * @param branchName the branch to clone (default: `main`) - * @returns command output - */ -export const createAppFromTemplate = async function createAppFromTemplate({ - templateString, - appName = '', - branchName = 'main', - shellOpts = {}, -}: { - templateString: string; - appName?: string; // TODO: bad arg name. it should be app path, because this is effectively how it is used - branchName?: string; - shellOpts?: SpawnOptionsWithoutStdio; -}): Promise { - try { - return await create(appName, {}, { - '--template': templateString, - '--branch': branchName, - }, shellOpts); - } catch (error) { - throw commandError(error, 'createAppFromTemplate'); - } -}; - -// TODO: (breaking change): rename properties of this default export to match actual command names -export default { - createAppFromTemplate, - createApp: create, -}; +export default create; diff --git a/packages/cli-test/src/cli/commands/env.spec.ts b/packages/cli-test/src/cli/commands/env.spec.ts new file mode 100644 index 000000000..fa410726c --- /dev/null +++ b/packages/cli-test/src/cli/commands/env.spec.ts @@ -0,0 +1,43 @@ +import sinon from 'sinon'; + +import env from './env'; +import { mockProcess } from '../../utils/test'; +import { shell } from '../shell'; + +describe('env commands', () => { + const sandbox = sinon.createSandbox(); + let spawnSpy: sinon.SinonStub; + + beforeEach(() => { + const process = mockProcess(); + spawnSpy = sandbox.stub(shell, 'spawnProcess').returns({ + command: 'something', + finished: true, + output: 'hi', + process, + }); + sandbox.stub(shell, 'checkIfFinished').resolves(); + }); + afterEach(() => { + sandbox.restore(); + }); + + describe('add method', () => { + it('should invoke `env add `', async () => { + await env.add({ appPath: '/some/path', secretKey: 'key', secretValue: 'value' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('env add key value')); + }); + }); + describe('list method', () => { + it('should invoke `env list`', async () => { + await env.list({ appPath: '/some/path' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('env list')); + }); + }); + describe('remove method', () => { + it('should invoke `env remove `', async () => { + await env.remove({ appPath: '/some/path', secretKey: 'key' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('env remove key')); + }); + }); +}); diff --git a/packages/cli-test/src/cli/commands/env.ts b/packages/cli-test/src/cli/commands/env.ts index 5595ccfa5..63fd3e7a2 100644 --- a/packages/cli-test/src/cli/commands/env.ts +++ b/packages/cli-test/src/cli/commands/env.ts @@ -1,84 +1,51 @@ +import { ProjectCommandArguments } from '../../types/commands/common_arguments'; import { SlackCLIProcess } from '../cli-process'; -import commandError from '../command-error'; + +export interface EnvCommandArguments { + /** @description Environment variable key */ + secretKey: string; + /** @description Environment variable value */ + secretValue: string; +} /** * `slack env add` - * @param appPath path to app - * @param teamFlag team domain to add env var to - * @param secretKey environment variable key - * @param secretValue environment variable value * @returns command output */ -export const add = async function envAdd( - appPath: string, - teamFlag: string, - secretKey: string, - secretValue: string, - options?: { qa?: boolean }, -): Promise { - // TODO: (breaking change) separate parameters vs single-param-object - const cmd = new SlackCLIProcess(`env add ${secretKey} ${secretValue}`, { team: teamFlag, qa: options?.qa }); - try { - const proc = await cmd.execAsync({ - cwd: appPath, - }); - return proc.output; - } catch (error) { - throw commandError(error, 'envAdd'); - } +export const add = async function envAdd(args: ProjectCommandArguments & EnvCommandArguments): Promise { + const cmd = new SlackCLIProcess(`env add ${args.secretKey} ${args.secretValue}`, args); + const proc = await cmd.execAsync({ + cwd: args.appPath, + }); + return proc.output; }; /** * `slack env list` - * @param appPath path to app - * @param teamFlag team domain to list env vars for * @returns command output */ -export const list = async function envList( - appPath: string, - teamFlag: string, - options?: { qa?: boolean }, -): Promise { - // TODO: (breaking change) separate parameters vs single-param-object - const cmd = new SlackCLIProcess('env list', { team: teamFlag, qa: options?.qa }); - try { - const proc = await cmd.execAsync({ - cwd: appPath, - }); - return proc.output; - } catch (error) { - throw commandError(error, 'envList'); - } +export const list = async function envList(args: ProjectCommandArguments): Promise { + const cmd = new SlackCLIProcess('env list', args); + const proc = await cmd.execAsync({ + cwd: args.appPath, + }); + return proc.output; }; /** * `slack env remove` - * @param appPath path to app - * @param teamFlag team domain to remove env var from - * @param secretKey environment variable key * @returns command output */ -export const remove = async function envRemove( - appPath: string, - teamFlag: string, - secretKey: string, - options?: { qa?: boolean }, -): Promise { - // TODO: (breaking change) separate parameters vs single-param-object - const cmd = new SlackCLIProcess(`env remove ${secretKey}`, { team: teamFlag, qa: options?.qa }); - try { - const proc = await cmd.execAsync({ - cwd: appPath, - }); - return proc.output; - } catch (error) { - throw commandError(error, 'envRemove'); - } +export const remove = async function envRemove(args: ProjectCommandArguments & Pick): Promise { + const cmd = new SlackCLIProcess(`env remove ${args.secretKey}`, args); + const proc = await cmd.execAsync({ + cwd: args.appPath, + }); + return proc.output; }; -// TODO: (breaking change): rename properties of this default export to match actual command names export default { - envAdd: add, - envList: list, - envRemove: remove, + add, + list, + remove, }; diff --git a/packages/cli-test/src/cli/commands/external-auth.spec.ts b/packages/cli-test/src/cli/commands/external-auth.spec.ts new file mode 100644 index 000000000..99e9bc06f --- /dev/null +++ b/packages/cli-test/src/cli/commands/external-auth.spec.ts @@ -0,0 +1,66 @@ +import sinon from 'sinon'; + +import extAuth from './external-auth'; +import { mockProcess } from '../../utils/test'; +import { shell } from '../shell'; + +describe('external-auth commands', () => { + const sandbox = sinon.createSandbox(); + let spawnSpy: sinon.SinonStub; + + beforeEach(() => { + const process = mockProcess(); + spawnSpy = sandbox.stub(shell, 'spawnProcess').returns({ + command: 'something', + finished: true, + output: 'hi', + process, + }); + sandbox.stub(shell, 'checkIfFinished').resolves(); + }); + afterEach(() => { + sandbox.restore(); + }); + + describe('add method', () => { + it('should invoke `external-auth add --provider`', async () => { + await extAuth.add({ appPath: '/some/path', provider: 'bigcorp' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('external-auth add --provider bigcorp')); + }); + }); + describe('addSecret method', () => { + it('should invoke `external-auth add-secret --provider --secret`', async () => { + await extAuth.addSecret({ appPath: '/some/path', provider: 'bigcorp', secret: 'shh' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('external-auth add-secret')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--provider bigcorp')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--secret shh')); + }); + }); + describe('remove method', () => { + it('should invoke `external-auth remove --provider`', async () => { + await extAuth.remove({ appPath: '/some/path', provider: 'bigcorp' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('external-auth remove --provider bigcorp')); + }); + it('should invoke `external-auth remove --provider --all` if `all: true` specified', async () => { + await extAuth.remove({ appPath: '/some/path', provider: 'bigcorp', all: true }); + sandbox.assert.calledWith(spawnSpy, sinon.match('external-auth remove --provider bigcorp')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--all')); + }); + }); + describe('select-auth method', () => { + it('should invoke `external-auth select-auth --provider`', async () => { + await extAuth.selectAuth({ appPath: '/some/path', provider: 'bigcorp' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('external-auth select-auth --provider bigcorp')); + }); + it('should invoke `external-auth select-auth --provider --external-account` if `externalAccount` specified', async () => { + await extAuth.selectAuth({ appPath: '/some/path', provider: 'bigcorp', externalAccount: 'me@me.com' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('external-auth select-auth --provider bigcorp')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--external-account me@me.com')); + }); + it('should invoke `external-auth select-auth --provider --workflow` if `workflow` specified', async () => { + await extAuth.selectAuth({ appPath: '/some/path', provider: 'bigcorp', workflow: '#/workflow/1234' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('external-auth select-auth --provider bigcorp')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--workflow #/workflow/1234')); + }); + }); +}); diff --git a/packages/cli-test/src/cli/commands/external-auth.ts b/packages/cli-test/src/cli/commands/external-auth.ts index 7c4a707ca..f3a93300d 100644 --- a/packages/cli-test/src/cli/commands/external-auth.ts +++ b/packages/cli-test/src/cli/commands/external-auth.ts @@ -1,37 +1,102 @@ -import { SlackCLIProcess } from '../cli-process'; -import commandError from '../command-error'; +import { ProjectCommandArguments } from '../../types/commands/common_arguments'; +import { SlackCLICommandOptions, SlackCLIProcess } from '../cli-process'; + +export interface ExternalAuthCommandArguments { + /** @description the OAuth Provider key to target. */ + provider: string; + /** @description the OAuth Provider's client secret. */ + secret: string; + /** + * @description Remove all tokens? If `provider` is not specified, removes all tokens for all providers, + * otherwise removes all tokens for the specified provider. Defaults to `false.` + */ + all?: boolean; +} + +/** + * `slack external-auth add` + * @returns command output + */ +export const add = async function externalAuthAdd( + args: ProjectCommandArguments & Pick, +): Promise { + const cmd = new SlackCLIProcess('external-auth add', args, { + '--provider': args.provider, + }); + const proc = await cmd.execAsync({ + cwd: args.appPath, + }); + return proc.output; +}; + +/** + * `slack external-auth add-secret` + * @returns command output + */ +export const addSecret = async function extAuthAddSecret( + args: ProjectCommandArguments & Omit, +): Promise { + const cmd = new SlackCLIProcess('external-auth add-secret', args, { + '--provider': args.provider, + '--secret': args.secret, + }); + const proc = await cmd.execAsync({ + cwd: args.appPath, + }); + return proc.output; +}; /** - * `slack external-auth` - * @param appPath path to app - * @param teamFlag team domain of the relevant app - * @param provider provider to add external auth for - * @param flags specification of external-auth, e.g. add or add-secret + * `slack external-auth remove` * @returns command output */ -export const externalAuth = async function externalAuth( - appPath: string, - teamFlag: string, - provider: string, - flags: string, - options?: { qa?: boolean }, +export const remove = async function extAuthRemove( + args: ProjectCommandArguments & Omit, ): Promise { - // TODO: (breaking change) separate parameters vs single-param-object - // TODO: this is a generic entry point to the `external-auth` suite of commands, and today `flags` is abused to - // specify the actual sub-command. easy, but lazy, not sure if best approach - const cmd = new SlackCLIProcess(`external-auth ${flags}`, { team: teamFlag, qa: options?.qa }, { - '--provider': provider, + const cmdOpts: SlackCLICommandOptions = { + '--provider': args.provider, + }; + if (args.all) { + cmdOpts['--all'] = true; + } + const cmd = new SlackCLIProcess('external-auth remove', args, cmdOpts); + const proc = await cmd.execAsync({ + cwd: args.appPath, }); - try { - const proc = await cmd.execAsync({ - cwd: appPath, - }); - return proc.output; - } catch (error) { - throw commandError(error, 'externalAuth'); + return proc.output; +}; + +/** + * `slack external-auth select-auth` + * @returns command output + */ +export const selectAuth = async function extAuthSelectAuth( + args: ProjectCommandArguments & Pick & { + /** @description specifies an external account identifier, e.g. an email address. */ + externalAccount?: string; + /** @description specifies a workflow to set selected developer account. */ + workflow?: string; + }, +): Promise { + const cmdOpts: SlackCLICommandOptions = { + '--provider': args.provider, + }; + if (args.externalAccount) { + cmdOpts['--external-account'] = args.externalAccount; + } + if (args.workflow) { + cmdOpts['--workflow'] = args.workflow; } + const cmd = new SlackCLIProcess('external-auth select-auth', args, cmdOpts); + const proc = await cmd.execAsync({ + cwd: args.appPath, + }); + return proc.output; }; export default { - externalAuth, + add, + addSecret, + remove, + selectAuth, }; diff --git a/packages/cli-test/src/cli/commands/function.spec.ts b/packages/cli-test/src/cli/commands/function.spec.ts new file mode 100644 index 000000000..ff0d2fefa --- /dev/null +++ b/packages/cli-test/src/cli/commands/function.spec.ts @@ -0,0 +1,58 @@ +import sinon from 'sinon'; + +import func from './function'; +import { mockProcess } from '../../utils/test'; +import { shell } from '../shell'; + +describe('function commands', () => { + const sandbox = sinon.createSandbox(); + let spawnSpy: sinon.SinonStub; + + beforeEach(() => { + const process = mockProcess(); + spawnSpy = sandbox.stub(shell, 'spawnProcess').returns({ + command: 'something', + finished: true, + output: 'hi', + process, + }); + sandbox.stub(shell, 'checkIfFinished').resolves(); + }); + afterEach(() => { + sandbox.restore(); + }); + + describe('access method', () => { + it('should invoke `function access --info` if info=true', async () => { + await func.access({ appPath: '/some/path', info: true }); + sandbox.assert.calledWith(spawnSpy, sinon.match('function access')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--info')); + }); + it('should invoke `function access --name --app-collaborators` if `name` and `appCollaborators` specified', async () => { + await func.access({ appPath: '/some/path', name: 'best', appCollaborators: true }); + sandbox.assert.calledWith(spawnSpy, sinon.match('function access')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--name best')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--app-collaborators')); + }); + it('should invoke `function access --name --everyone` if `name` and `everyone` specified', async () => { + await func.access({ appPath: '/some/path', name: 'best', everyone: true }); + sandbox.assert.calledWith(spawnSpy, sinon.match('function access')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--name best')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--everyone')); + }); + it('should invoke `function access --name --grant --users` if `name`, `grant` and `users` specified', async () => { + await func.access({ appPath: '/some/path', name: 'best', grant: true, users: ['U1234'] }); + sandbox.assert.calledWith(spawnSpy, sinon.match('function access')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--name best')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--grant')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--users U1234')); + }); + it('should invoke `function access --name --revoke --users` if `name`, `revoke` and `users` specified', async () => { + await func.access({ appPath: '/some/path', name: 'best', revoke: true, users: ['U1234'] }); + sandbox.assert.calledWith(spawnSpy, sinon.match('function access')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--name best')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--revoke')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--users U1234')); + }); + }); +}); diff --git a/packages/cli-test/src/cli/commands/function.ts b/packages/cli-test/src/cli/commands/function.ts index eeee2f784..f9d60d776 100644 --- a/packages/cli-test/src/cli/commands/function.ts +++ b/packages/cli-test/src/cli/commands/function.ts @@ -1,36 +1,55 @@ -import { SlackCLIProcess } from '../cli-process'; -import commandError from '../command-error'; +import { + GroupAccessChangeArguments, + InfoArgument, + ProjectCommandArguments, + UserAccessChangeArguments, +} from '../../types/commands/common_arguments'; +import { SlackCLICommandOptions, SlackCLIProcess } from '../cli-process'; -// TODO: the "flag" param throughout here should be done in a better way. -// Perhaps expose the SlackCommandOptions type directly? +type AccessChangeArguments = { + /** @description `callback_id` of function being targeted. */ + name: string; + info?: boolean; +} & (GroupAccessChangeArguments | UserAccessChangeArguments); + +type FunctionAccessArguments = AccessChangeArguments | InfoArgument; /** * `slack function access` - * @param appPath path to app - * @param teamFlag team domain for the function's app - * @param flags specification of function distribution, i.e. --name greeting_function --app-collaborators * @returns command output */ export const access = async function functionAccess( - appPath: string, - teamFlag: string, - flags: string, - options?: { qa?: boolean }, + args: ProjectCommandArguments & FunctionAccessArguments, ): Promise { - // TODO: breaking change, separate params vs single-param-object - const cmd = new SlackCLIProcess(`function access ${flags}`, { team: teamFlag, qa: options?.qa }); - try { - const proc = await cmd.execAsync({ - cwd: appPath, - }); - return proc.output; - } catch (error) { - throw commandError(error, 'functionAccess'); + const cmdOpts: SlackCLICommandOptions = {}; + if ('info' in args && args.info) { + cmdOpts['--info'] = true; + } else { + cmdOpts['--name'] = args.name; + if ('appCollaborators' in args && args.appCollaborators) { + cmdOpts['--app-collaborators'] = true; + } else if ('everyone' in args && args.everyone) { + cmdOpts['--everyone'] = true; + } else if ('users' in args) { + cmdOpts['--users'] = args.users.join(','); + if ('grant' in args && args.grant) { + cmdOpts['--grant'] = true; + } else if ('revoke' in args && args.revoke) { + cmdOpts['--revoke'] = true; + } else { + throw new Error('When granting or revoking function access to users, you must specify one of `grant` or `revoke` as `true`.'); + } + } else { + throw new Error('When setting function access, you must specify a target for whom to give access to.'); + } } + const cmd = new SlackCLIProcess('function access', args, cmdOpts); + const proc = await cmd.execAsync({ + cwd: args.appPath, + }); + return proc.output; }; -// TODO: (breaking change): rename properties of this default export to match actual command names export default { - functionDistribute: access, // TODO: brekaing change remove this, is now called 'function access' - functionAccess: access, + access, }; diff --git a/packages/cli-test/src/cli/commands/manifest.spec.ts b/packages/cli-test/src/cli/commands/manifest.spec.ts new file mode 100644 index 000000000..41d8ba7fd --- /dev/null +++ b/packages/cli-test/src/cli/commands/manifest.spec.ts @@ -0,0 +1,43 @@ +import sinon from 'sinon'; + +import manifest from './manifest'; +import { mockProcess } from '../../utils/test'; +import { shell } from '../shell'; + +describe('manifest commands', () => { + const sandbox = sinon.createSandbox(); + let spawnSpy: sinon.SinonStub; + + beforeEach(() => { + const process = mockProcess(); + spawnSpy = sandbox.stub(shell, 'spawnProcess').returns({ + command: 'something', + finished: true, + output: 'hi', + process, + }); + sandbox.stub(shell, 'checkIfFinished').resolves(); + }); + afterEach(() => { + sandbox.restore(); + }); + + describe('info method', () => { + it('should invoke `manifest info` and default `--source project`', async () => { + await manifest.info({ appPath: '/some/path' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('manifest info')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--source project')); + }); + it('should invoke `manifest info --source remote` source=remote specified', async () => { + await manifest.info({ appPath: '/some/path', source: 'remote' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('manifest info')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--source remote')); + }); + }); + describe('validate method', () => { + it('should invoke `manifest validate`', async () => { + await manifest.validate({ appPath: '/some/path' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('manifest validate')); + }); + }); +}); diff --git a/packages/cli-test/src/cli/commands/manifest.ts b/packages/cli-test/src/cli/commands/manifest.ts index 192acf321..625fefda5 100644 --- a/packages/cli-test/src/cli/commands/manifest.ts +++ b/packages/cli-test/src/cli/commands/manifest.ts @@ -1,28 +1,39 @@ -import { SlackCLIProcess } from '../cli-process'; -import commandError from '../command-error'; +import { ProjectCommandArguments } from '../../types/commands/common_arguments'; +import { SlackCLICommandOptions, SlackCLIProcess } from '../cli-process'; + +/** + * `slack manifest info` + * @returns command output + */ +export const info = async function manifestInfo(args: ProjectCommandArguments & { + /** + * @description Whether to retrieve manifest from the local `project`, or `remote` from Slack. Defaults to `project`. + */ + source?: 'project' | 'remote'; +}): Promise { + const cmdOpts: SlackCLICommandOptions = { + '--source': args.source || 'project', + }; + const cmd = new SlackCLIProcess('manifest info', args, cmdOpts); + const proc = await cmd.execAsync({ + cwd: args.appPath, + }); + return proc.output; +}; /** * `slack manifest validate` - * @param appPath path to app * @returns command output */ -export const validate = async function manifestValidate( - appPath: string, - options?: { qa?: boolean }, -): Promise { - // TODO: breaking change, separate params vs single-param-object - const cmd = new SlackCLIProcess('manifest validate', options); - try { - const proc = await cmd.execAsync({ - cwd: appPath, - }); - return proc.output; - } catch (error) { - throw commandError(error, 'manifestValidate'); - } +export const validate = async function manifestValidate(args: ProjectCommandArguments): Promise { + const cmd = new SlackCLIProcess('manifest validate', args); + const proc = await cmd.execAsync({ + cwd: args.appPath, + }); + return proc.output; }; -// TODO: (breaking change): rename properties of this default export to match actual command names export default { - manifestValidate: validate, + info, + validate, }; diff --git a/packages/cli-test/src/cli/commands/platform.spec.ts b/packages/cli-test/src/cli/commands/platform.spec.ts new file mode 100644 index 000000000..380a24236 --- /dev/null +++ b/packages/cli-test/src/cli/commands/platform.spec.ts @@ -0,0 +1,115 @@ +import assert from 'assert'; + +import sinon from 'sinon'; + +import platform from './platform'; +import { ShellProcess } from '../../types/shell'; +import { mockProcess } from '../../utils/test'; +import { shell } from '../shell'; + +describe('platform commands', () => { + const sandbox = sinon.createSandbox(); + let spawnSpy: sinon.SinonStub; + let waitForOutputSpy: sinon.SinonStub; + let fakeProcess: ShellProcess; + + beforeEach(() => { + fakeProcess = { + command: 'something', + finished: true, + output: 'hi', + process: mockProcess(), + }; + spawnSpy = sandbox.stub(shell, 'spawnProcess').returns(fakeProcess); + sandbox.stub(shell, 'checkIfFinished').resolves(); + waitForOutputSpy = sandbox.stub(shell, 'waitForOutput').resolves(); + }); + afterEach(() => { + sandbox.restore(); + }); + + describe('activity method', () => { + it('should invoke `activity`', async () => { + await platform.activity({ appPath: '/some/path' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('activity')); + }); + it('should invoke `activity` with specified `source`', async () => { + await platform.activity({ appPath: '/some/path', source: 'slack' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('activity')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--source slack')); + }); + }); + describe('activityTailStart method', () => { + it('should invoke `activity --tail`', async () => { + await platform.activityTailStart({ appPath: '/some/path', stringToWaitFor: 'poop' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('activity')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--tail')); + }); + }); + describe('activityTailStop method', () => { + it('should reject if waitForOutput rejects', async () => { + waitForOutputSpy.rejects(); + await assert.rejects(platform.activityTailStop({ stringToWaitFor: 'poop', proc: fakeProcess })); + }); + it('should reject if shell.kill rejects', async () => { + sandbox.stub(shell, 'kill').rejects(); + await assert.rejects(platform.activityTailStop({ stringToWaitFor: 'poop', proc: fakeProcess })); + }); + it('should resolve if waitForOutput and shell.kill resolve', async () => { + sandbox.stub(shell, 'kill').resolves(); + await platform.activityTailStop({ stringToWaitFor: 'poop', proc: fakeProcess }); + assert.ok(true); + }); + }); + describe('deploy method', () => { + it('should invoke `deploy` with --hide-triggers by default', async () => { + await platform.deploy({ appPath: '/some/path' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('deploy')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--hide-triggers')); + }); + it('should invoke `deploy` without --hide-triggers if hideTriggers=false', async () => { + await platform.deploy({ appPath: '/some/path', hideTriggers: false }); + sandbox.assert.calledWith(spawnSpy, sinon.match('deploy')); + sandbox.assert.neverCalledWith(spawnSpy, sinon.match('--hide-triggers')); + }); + }); + describe('runStart method', () => { + it('should invoke `run` with --cleanup and --hide-triggers by default', async () => { + await platform.runStart({ appPath: '/some/path' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('run')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--cleanup')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--hide-triggers')); + }); + it('should invoke `run` without --hide-triggers if hideTriggers=false', async () => { + await platform.runStart({ appPath: '/some/path', hideTriggers: false }); + sandbox.assert.calledWith(spawnSpy, sinon.match('run')); + sandbox.assert.neverCalledWith(spawnSpy, sinon.match('--hide-triggers')); + }); + it('should invoke `run` without --cleanup if cleanup=false', async () => { + await platform.runStart({ appPath: '/some/path', cleanup: false }); + sandbox.assert.calledWith(spawnSpy, sinon.match('run')); + sandbox.assert.neverCalledWith(spawnSpy, sinon.match('--cleanup')); + }); + }); + describe('runStop method', () => { + it('should reject if shell.kill rejects', async () => { + sandbox.stub(shell, 'kill').rejects(); + await assert.rejects(platform.runStop({ proc: fakeProcess })); + }); + it('should reject if waitForShutdown=true and waitForOutput rejects', async () => { + sandbox.stub(shell, 'kill').resolves(); + waitForOutputSpy.rejects(); + await assert.rejects(platform.runStop({ proc: fakeProcess, waitForShutdown: true })); + }); + it('should resolve immediately if waitForShutdown=false and shell.kill resolve', async () => { + sandbox.stub(shell, 'kill').resolves(); + await platform.runStop({ proc: fakeProcess, waitForShutdown: false }); + assert.ok(true); + }); + it('should resolve if waitForShutdown=true and both shell.kill and shell.waitForOutput resolve', async () => { + sandbox.stub(shell, 'kill').resolves(); + await platform.runStop({ proc: fakeProcess, waitForShutdown: true }); + assert.ok(true); + }); + }); +}); diff --git a/packages/cli-test/src/cli/commands/platform.ts b/packages/cli-test/src/cli/commands/platform.ts index 805ce4cfb..52e6e4bf1 100644 --- a/packages/cli-test/src/cli/commands/platform.ts +++ b/packages/cli-test/src/cli/commands/platform.ts @@ -1,227 +1,158 @@ +import { ProjectCommandArguments, WorkspaceGrantArgument } from '../../types/commands/common_arguments'; import { SlackTracerId } from '../../utils/constants'; import logger from '../../utils/logger'; -import { SlackCLIProcess } from '../cli-process'; -import commandError from '../command-error'; +import { SlackCLICommandOptions, SlackCLIProcess } from '../cli-process'; import { shell } from '../shell'; -import type { ShellProcess } from '../../utils/types'; +import type { ShellProcess } from '../../types/shell'; -// TODO: the options for these methods could be DRYed up +export interface StringWaitArgument { + /** @description string to wait for in the command output before this function returns. */ + stringToWaitFor: string; +} -export default { - /** - * `slack platform activity` - * @param flag - * @returns command output - */ - activity: async function activity({ - appPath, - teamFlag, - flag, - localApp = true, - qa = false, - }: { - /** Path to app */ - appPath: string; - /** workspace or organization name or ID to deploy the app to */ - teamFlag: string; - /** Arbitrary flags to provide to the command */ - flag?: string; - /** Whether to operate on the local or deployed app */ - localApp?: boolean; // TODO: this option is provided inconsistently across commands (breaking change) - /** Whether to operate against --slackdev or production */ - qa?: boolean; // TODO: this option is provided inconsistently across commands (breaking change) - }): Promise { - const appEnvironment = localApp ? 'local' : 'deployed'; - const cmd = new SlackCLIProcess(`activity ${flag}`, { team: teamFlag, qa }, { - '--app': appEnvironment, - }); - try { - const proc = await cmd.execAsync({ - cwd: appPath, - }); - return proc.output; - } catch (error) { - throw commandError(error, 'activity'); - } - }, +export interface ProcessArgument { + /** @description CLI process previous created via a `*Start` command. */ + proc: ShellProcess; +} - /** - * waits for a specified sequence then returns the shell - * At the specific point where the sequence is found to continue with test - * @returns command output - */ - activityTailStart: async function activityTailStart({ - appPath, - teamFlag, - stringToWaitFor, - localApp = true, - qa = false, - }: { - /** Path to app */ - appPath: string; - /** workspace or organization name or ID to deploy the app to */ - teamFlag: string; - /** expected string to be present in the output before this function returns */ - stringToWaitFor: string; - /** Whether to operate on the local or deployed app */ - localApp?: boolean; - /** Whether to operate against --slackdev or production */ - qa?: boolean; // TODO: this option is provided inconsistently across commands (breaking change) - }): Promise { - const appEnvironment = localApp ? 'local' : 'deployed'; - const cmd = new SlackCLIProcess('activity --tail', { team: teamFlag, qa }, { - '--app': appEnvironment, - }); - try { - const proc = await cmd.execAsyncUntilOutputPresent(stringToWaitFor, { - cwd: appPath, - }); - return proc; - } catch (error) { - throw commandError(error, 'activityTailStart'); - } - }, +export interface RunDeployArguments extends WorkspaceGrantArgument { + /** @description Hides output and prompts related to triggers. Defaults to `true`. */ + hideTriggers?: boolean; + /** @description Delete the app after `run` process finishes. Defaults to `true`. */ + cleanup?: boolean; +} - /** - * waits for a specified string in the `activity` output, kills the process then returns the output - * @returns command output - */ - activityTailStop: async function activityTailStop({ - /** The ShellProcess to check */ - proc, - stringToWait, - }: { - proc: ShellProcess; - /** expected string to be present in the output before process is killed */ - stringToWait: string; - }): Promise { - return new Promise((resolve, reject) => { - // Wait for output - shell.waitForOutput(stringToWait, proc).then(() => { - // kill the shell process - shell.kill(proc).then(() => { - resolve(proc.output); - }, (err) => { - const msg = `activityTailStop command failed to kill process: ${err}`; - logger.warn(msg); - reject(new Error(msg)); - }); - }, reject); - }); - }, - - /** - * `slack deploy` - */ - deploy: async function deploy({ - appPath, - teamFlag, - hideTriggers = true, - orgWorkspaceGrantFlag, - qa = false, - }: { - /** Path to app */ - appPath: string; - /** workspace or organization name or ID to deploy the app to */ - teamFlag: string; - /** hides output and prompts related to triggers. Defaults to `true`. */ - hideTriggers?: boolean; - /** - * Org workspace ID, or the string `all` to request access to all workspaces in the org, - * to request grant access to in AAA scenarios - */ - orgWorkspaceGrantFlag?: string; - /** Whether to operate against --slackdev or production */ - qa?: boolean; // TODO: this option is provided inconsistently across commands (breaking change) - }): Promise { - const cmd = new SlackCLIProcess('deploy', { team: teamFlag, qa }, { - '--hide-triggers': hideTriggers, - '--org-workspace-grant': orgWorkspaceGrantFlag, - }); - try { - const proc = await cmd.execAsync({ - cwd: appPath, - }); - return proc.output; - } catch (error) { - throw commandError(error, 'deploy'); - } - }, +/** + * `slack platform activity` + * @returns command output + */ +export const activity = async function activity(args: ProjectCommandArguments & { + /** @description Source of logs to filter; can be `slack` or `developer`. */ + source?: 'slack' | 'developer'; +}): Promise { + const cmdOpts: SlackCLICommandOptions = {}; + if ('source' in args) { + cmdOpts['--source'] = args.source; + } + const cmd = new SlackCLIProcess('activity', args, cmdOpts); + const proc = await cmd.execAsync({ + cwd: args.appPath, + }); + return proc.output; +}; - /** - * start `slack run` - * - `runStop` must be used to stop `run` process - * @returns shell object to kill it explicitly in the test case - */ - runStart: async function runStart({ - appPath, - teamFlag, - cleanup = true, - hideTriggers = true, - orgWorkspaceGrantFlag, - qa = false, - }: { - /** Path to app */ - appPath: string; - /** workspace or organization name or ID to deploy the app to */ - teamFlag: string; - /** delete the app after `run` completes */ - cleanup?: boolean; - /** hides output and prompts related to triggers. Defaults to `true`. */ - hideTriggers?: boolean; - /** - * Org workspace ID, or the string `all` to request access to all workspaces in the org, - * to request grant access to in AAA scenarios - */ - orgWorkspaceGrantFlag?: string; - /** Whether to operate against --slackdev or production */ - qa?: boolean; // TODO: this option is provided inconsistently across commands (breaking change) - }): Promise { - const cmd = new SlackCLIProcess('run', { team: teamFlag, qa }, { - '--cleanup': cleanup, - '--hide-triggers': hideTriggers, - '--org-workspace-grant': orgWorkspaceGrantFlag, - }); - try { - const proc = await cmd.execAsyncUntilOutputPresent('Connected, awaiting events', { - cwd: appPath, - }); - return proc; - } catch (error) { - throw commandError(error, 'runStart'); - } - }, +/** + * `slack platform activity` but waits for a specified sequence then returns the shell + * At the specific point where the sequence is found to continue with test + * @returns command output + */ +export const activityTailStart = async function activityTailStart( + args: ProjectCommandArguments & StringWaitArgument, +): Promise { + const cmd = new SlackCLIProcess('activity', args, { '--tail': true }); + const proc = await cmd.execAsyncUntilOutputPresent(args.stringToWaitFor, { + cwd: args.appPath, + }); + return proc; +}; - /** - * stop `slack run` - * @param shell object with process to kill - * @param teamName to check that app was deleted from that team - */ - runStop: async function runStop(proc: ShellProcess, teamName?: string): Promise { - // TODO: teamName param should be changed to something else. 'wait for shutdown' or some such (breaking change) - return new Promise((resolve, reject) => { +/** + * Waits for a specified string in the provided `activityTailStart` process output, + * kills the process then returns the output + * @returns command output + */ +export const activityTailStop = async function activityTailStop( + args: StringWaitArgument & ProcessArgument, +): Promise { + return new Promise((resolve, reject) => { + // Wait for output + shell.waitForOutput(args.stringToWaitFor, args.proc).then(() => { // kill the shell process - shell.kill(proc).then(() => { - // Due to the complexity of gracefully shutting down processes on Windows / lack of interrupt signal support, - // we don't wait for the SLACK_TRACE_PLATFORM_RUN_STOP trace on Windows - if (process.platform === 'win32') { - resolve(); - } - - if (teamName) { - // TODO: this is messed up. does not match to parameter name at all - team name has nothing to do with this. - // Check if local app was deleted automatically, if --cleanup was passed to `runStart` - // Wait for the output to verify process stopped - shell.waitForOutput(SlackTracerId.SLACK_TRACE_PLATFORM_RUN_STOP, proc).then(resolve, reject); - } else { - resolve(); - } + shell.kill(args.proc).then(() => { + resolve(args.proc.output); }, (err) => { - const msg = `runStop command failed to kill process: ${err}`; + const msg = `activityTailStop command failed to kill process: ${err}`; logger.warn(msg); reject(new Error(msg)); }); + }, reject); + }); +}; + +/** + * `slack deploy` + * @returns command output + */ +export const deploy = async function deploy(args: ProjectCommandArguments & Omit): Promise { + const cmd = new SlackCLIProcess('deploy', args, { + '--hide-triggers': typeof args.hideTriggers !== 'undefined' ? args.hideTriggers : true, + '--org-workspace-grant': args.orgWorkspaceGrantFlag, + }); + const proc = await cmd.execAsync({ + cwd: args.appPath, + }); + return proc.output; +}; + +/** + * start `slack run`. `runStop` must be used to stop the `run` process returned by this method. + * @returns shell object to kill it explicitly in the test case via `runStop` + */ +export const runStart = async function runStart( + args: ProjectCommandArguments & RunDeployArguments, +): Promise { + const cmd = new SlackCLIProcess('run', args, { + '--app': 'local', + '--cleanup': typeof args.cleanup !== 'undefined' ? args.cleanup : true, + '--hide-triggers': typeof args.hideTriggers !== 'undefined' ? args.hideTriggers : true, + '--org-workspace-grant': args.orgWorkspaceGrantFlag, + }); + const proc = await cmd.execAsyncUntilOutputPresent('Connected, awaiting events', { + cwd: args.appPath, + }); + return proc; +}; + +/** + * stop `slack run` + * @param teamName to check that app was deleted from that team + */ +export const runStop = async function runStop(args: ProcessArgument & { + /** + * @description Should wait for the `run` process to spin down before exiting this function. + * On Windows, this property is always set to `true`. Defaults to `false`. + */ + waitForShutdown?: boolean; +}): Promise { + return new Promise((resolve, reject) => { + // kill the shell process + shell.kill(args.proc).then(() => { + // Due to the complexity of gracefully shutting down processes on Windows / lack of interrupt signal support, + // we don't wait for the SLACK_TRACE_PLATFORM_RUN_STOP trace on Windows + if (process.platform === 'win32') { + resolve(); + } + + if (args.waitForShutdown) { + // Wait for the output to verify process stopped + shell.waitForOutput(SlackTracerId.SLACK_TRACE_PLATFORM_RUN_STOP, args.proc).then(resolve, reject); + } else { + resolve(); + } + }, (err) => { + const msg = `runStop command failed to kill process: ${err}`; + logger.warn(msg); + reject(new Error(msg)); }); - }, + }); +}; + +export default { + activity, + activityTailStart, + activityTailStop, + deploy, + runStart, + runStop, }; diff --git a/packages/cli-test/src/cli/commands/trigger.spec.ts b/packages/cli-test/src/cli/commands/trigger.spec.ts new file mode 100644 index 000000000..18983d404 --- /dev/null +++ b/packages/cli-test/src/cli/commands/trigger.spec.ts @@ -0,0 +1,171 @@ +import sinon from 'sinon'; + +import trigger from './trigger'; +import { mockProcess } from '../../utils/test'; +import { shell } from '../shell'; + +describe('trigger commands', () => { + const sandbox = sinon.createSandbox(); + let spawnSpy: sinon.SinonStub; + + beforeEach(() => { + const process = mockProcess(); + spawnSpy = sandbox.stub(shell, 'spawnProcess').returns({ + command: 'something', + finished: true, + output: 'hi', + process, + }); + sandbox.stub(shell, 'checkIfFinished').resolves(); + }); + afterEach(() => { + sandbox.restore(); + }); + + describe('access method', () => { + it('should invoke `trigger access --info` if info=true', async () => { + await trigger.access({ appPath: '/some/path', info: true, triggerId: 'T1234' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('trigger access')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--info')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--trigger-id T1234')); + }); + it('should invoke `trigger access --app-collaborators` if `appCollaborators` specified', async () => { + await trigger.access({ appPath: '/some/path', triggerId: 'T1234', appCollaborators: true }); + sandbox.assert.calledWith(spawnSpy, sinon.match('trigger access')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--app-collaborators')); + }); + it('should invoke `trigger access --everyone` if `everyone` specified', async () => { + await trigger.access({ appPath: '/some/path', triggerId: 'T1234', everyone: true }); + sandbox.assert.calledWith(spawnSpy, sinon.match('trigger access')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--everyone')); + }); + it('should invoke `trigger access --grant --users` if `grant` and `users` specified', async () => { + await trigger.access({ appPath: '/some/path', triggerId: 'T1234', grant: true, users: ['U1234'] }); + sandbox.assert.calledWith(spawnSpy, sinon.match('trigger access')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--grant')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--users U1234')); + }); + it('should invoke `trigger access --revoke --users` if `revoke` and `users` specified', async () => { + await trigger.access({ appPath: '/some/path', triggerId: 'T1234', revoke: true, users: ['U1234'] }); + sandbox.assert.calledWith(spawnSpy, sinon.match('trigger access')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--revoke')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--users U1234')); + }); + it('should invoke `trigger access --grant --channels` if `grant` and `channels` specified', async () => { + await trigger.access({ appPath: '/some/path', triggerId: 'T1234', grant: true, channels: ['C1234'] }); + sandbox.assert.calledWith(spawnSpy, sinon.match('trigger access')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--grant')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--channels C1234')); + }); + it('should invoke `trigger access --revoke --channels` if `revoke` and `channels` specified', async () => { + await trigger.access({ appPath: '/some/path', triggerId: 'T1234', revoke: true, channels: ['C1234'] }); + sandbox.assert.calledWith(spawnSpy, sinon.match('trigger access')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--revoke')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--channels C1234')); + }); + it('should invoke `trigger access --grant --organizations` if `grant` and `organizations` specified', async () => { + await trigger.access({ appPath: '/some/path', triggerId: 'T1234', grant: true, organizations: ['E1234'] }); + sandbox.assert.calledWith(spawnSpy, sinon.match('trigger access')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--grant')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--organizations E1234')); + }); + it('should invoke `trigger access --revoke --organizations` if `revoke` and `organizations` specified', async () => { + await trigger.access({ appPath: '/some/path', triggerId: 'T1234', revoke: true, organizations: ['E1234'] }); + sandbox.assert.calledWith(spawnSpy, sinon.match('trigger access')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--revoke')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--organizations E1234')); + }); + }); + describe('create method', () => { + it('should invoke `trigger create --trigger-def` if triggerDef specified', async () => { + await trigger.create({ appPath: '/some/path', triggerDef: 'some/file.json' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('trigger create')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--trigger-def some/file.json')); + }); + it('should invoke `trigger create --workflow --title` if workflow specified', async () => { + await trigger.create({ appPath: '/some/path', workflow: 'some#/callback_id', title: 'Title' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('trigger create')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--title Title')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--workflow some#/callback_id')); + }); + it('should invoke `trigger create --description` if description specified', async () => { + await trigger.create({ appPath: '/some/path', workflow: 'some#/callback_id', title: 'Title', description: 'test' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('trigger create')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--description test')); + }); + it('should invoke `trigger create --interactivity` if interactivity specified', async () => { + await trigger.create({ appPath: '/some/path', workflow: 'some#/callback_id', title: 'Title', interactivity: true, interactivityName: 'test' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('trigger create')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--interactivity')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--interactivity-name test')); + }); + }); + describe('delete method', () => { + it('should invoke `trigger delete --trigger-id`', async () => { + await trigger.delete({ appPath: '/some/path', triggerId: 'T1234' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('trigger delete')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--trigger-id T1234')); + }); + }); + describe('info method', () => { + it('should invoke `trigger info --trigger-id`', async () => { + await trigger.info({ appPath: '/some/path', triggerId: 'T1234' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('trigger info')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--trigger-id T1234')); + }); + }); + describe('list method', () => { + it('should invoke `trigger list`', async () => { + await trigger.list({ appPath: '/some/path' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('trigger list')); + }); + it('should invoke `trigger list --limit` if limit specified', async () => { + await trigger.list({ appPath: '/some/path', limit: 10 }); + sandbox.assert.calledWith(spawnSpy, sinon.match('trigger list')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--limit 10')); + }); + it('should invoke `trigger list --type` if type specified', async () => { + await trigger.list({ appPath: '/some/path', type: 'event' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('trigger list')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--type event')); + }); + }); + describe('update method', () => { + it('should invoke `trigger update --trigger-def` if triggerDef specified', async () => { + await trigger.update({ appPath: '/some/path', triggerDef: 'some/file.json', triggerId: 'T1234' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('trigger update')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--trigger-def some/file.json')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--trigger-id T1234')); + }); + it('should invoke `trigger update --workflow` if workflow specified', async () => { + await trigger.update({ appPath: '/some/path', workflow: 'some#/callback_id', triggerId: 'T1234' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('trigger update')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--workflow some#/callback_id')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--trigger-id T1234')); + }); + it('should invoke `trigger update --title` if title specified', async () => { + await trigger.update({ appPath: '/some/path', title: 'something', triggerId: 'T1234' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('trigger update')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--title something')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--trigger-id T1234')); + }); + it('should invoke `trigger update --description` if description specified', async () => { + await trigger.update({ appPath: '/some/path', description: 'test', triggerId: 'T1234' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('trigger update')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--description test')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--trigger-id T1234')); + }); + it('should invoke `trigger update --interactivity` if interactivity specified', async () => { + await trigger.update({ appPath: '/some/path', triggerId: 'T1234', interactivity: true }); + sandbox.assert.calledWith(spawnSpy, sinon.match('trigger update')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--interactivity')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--trigger-id T1234')); + }); + it('should invoke `trigger update --interactivity-name` if interactivityName specified', async () => { + await trigger.update({ appPath: '/some/path', triggerId: 'T1234', interactivityName: 'poop' }); + sandbox.assert.calledWith(spawnSpy, sinon.match('trigger update')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--interactivity-name poop')); + sandbox.assert.calledWith(spawnSpy, sinon.match('--trigger-id T1234')); + }); + }); +}); diff --git a/packages/cli-test/src/cli/commands/trigger.ts b/packages/cli-test/src/cli/commands/trigger.ts index 3ea89d1bf..3e6dea39d 100644 --- a/packages/cli-test/src/cli/commands/trigger.ts +++ b/packages/cli-test/src/cli/commands/trigger.ts @@ -1,183 +1,238 @@ -import { SlackCLIProcess } from '../cli-process'; -import commandError from '../command-error'; +import { + ChannelAccessChangeArguments, + GroupAccessChangeArguments, + InfoArgument, + OrganizationAccessChangeArguments, + ProjectCommandArguments, + UserAccessChangeArguments, + WorkspaceGrantArgument, +} from '../../types/commands/common_arguments'; +import { SlackCLICommandOptions, SlackCLIProcess } from '../cli-process'; -// TODO: the "flag" param throughout here should be done in a better way. -// Perhaps expose the SlackCommandOptions type directly? +type AccessChangeArguments = { + info?: boolean; +} & ( + GroupAccessChangeArguments | UserAccessChangeArguments | ChannelAccessChangeArguments | + OrganizationAccessChangeArguments +); + +export interface TriggerIdArgument { + /** @description ID of the trigger being targeted. */ + triggerId: string; +} + +type TriggerAccessArguments = TriggerIdArgument & (AccessChangeArguments | InfoArgument); + +/** + * Sets grant or revoke on a set of command options based on what is provided in an arguments list. + * @param args - trigger access method arguments + * @param cmdOpts - command-level options to provide to SlackCLIProcess + * @return void + */ +function setAccessType(args: Parameters[0], cmdOpts: SlackCLICommandOptions) { + /* eslint-disable no-param-reassign */ + if ('grant' in args && args.grant) { + cmdOpts['--grant'] = true; + } else if ('revoke' in args && args.revoke) { + cmdOpts['--revoke'] = true; + } else { + throw new Error('When granting or revoking trigger access, you must specify one of `grant` or `revoke` as `true`.'); + } + /* eslint-enable no-param-reassign */ +} /** * `slack trigger access` - * @param appPath path to app - * @param teamFlag team domain of the updating trigger - * @param flags specification of trigger access, e.g. --trigger-id Ft0143UPTAV8 --everyone * @returns command output */ export const access = async function triggerAccess( - appPath: string, - teamFlag: string, - flags: string, - options?: { qa?: boolean }, + args: ProjectCommandArguments & TriggerAccessArguments, ): Promise { - // TODO: (breaking change) separate params vs. single-param-object - // TODO: access requires --trigger-id so add that to parameters (breaking change) - const cmd = new SlackCLIProcess(`trigger access ${flags}`, { team: teamFlag, qa: options?.qa }); - try { - const proc = await cmd.execAsync({ - cwd: appPath, - }); - return proc.output; - } catch (error) { - throw commandError(error, 'triggerAccess'); + const cmdOpts: SlackCLICommandOptions = { + '--trigger-id': args.triggerId, + }; + if ('info' in args && args.info) { + cmdOpts['--info'] = true; + } else if ('appCollaborators' in args && args.appCollaborators) { + cmdOpts['--app-collaborators'] = true; + } else if ('everyone' in args && args.everyone) { + cmdOpts['--everyone'] = true; + } else if ('users' in args) { + cmdOpts['--users'] = args.users.join(','); + setAccessType(args, cmdOpts); + } else if ('channels' in args) { + cmdOpts['--channels'] = args.channels.join(','); + setAccessType(args, cmdOpts); + } else if ('organizations' in args) { + cmdOpts['--organizations'] = args.organizations.join(','); + setAccessType(args, cmdOpts); + } else { + throw new Error('When setting trigger access, you must specify a target for whom to give access to.'); } + const cmd = new SlackCLIProcess('trigger access', args, cmdOpts); + const proc = await cmd.execAsync({ + cwd: args.appPath, + }); + return proc.output; }; + +export interface CreateFromArguments { + /** @description Trigger description. */ + description?: string; + /** @description Trigger title. */ + title: string; + /** @description Workflow callback ID for the trigger to trip. */ + workflow: string; + /** + * @description When `true`, adds an `interactivity` parameter to the trigger with the name specified + * by `interactivityName`. + */ + interactivity?: boolean, + /** @description Specifies the name of the interactivity parameter to use. Defaults to `interactivity`. */ + interactivityName?: string; +} + +export interface CreateFromFile { + /** @description Path to a file containing a trigger definition. Overrides any other arguments provided. */ + triggerDef: string; +} + +type CreateArguments = WorkspaceGrantArgument & (CreateFromArguments | CreateFromFile); + /** * `slack trigger create` * @returns command output */ -export const create = async function triggerCreate({ - appPath, - teamFlag, - flag, - orgWorkspaceGrantFlag, - options, -}: { - /** path to app */ - appPath: string; - /** team domain where the trigger will be created */ - teamFlag: string; - /** any additional flags to provide i.e. method of trigger creation + ref, e.g. --trigger-def triggers/add-pin.json */ - flag: string; - /** supplies additional workspace within an org to grant app access to as part of install */ - orgWorkspaceGrantFlag?: string; - options?: { // TODO: must be a better way of exposing these options - /** Local app for local run sessions */ - localApp?: boolean; - /** Whether to run against --slackdev or production */ - qa?: boolean; +export const create = async function triggerCreate( + args: ProjectCommandArguments & CreateArguments, +): Promise { + const cmdOpts: SlackCLICommandOptions = { + '--org-workspace-grant': args.orgWorkspaceGrantFlag, }; -}): Promise { - const appEnvironment = options?.localApp ? 'local' : 'deployed'; - const cmd = new SlackCLIProcess(`trigger create ${flag}`, { team: teamFlag, qa: options?.qa }, { - '--app': appEnvironment, - '--org-workspace-grant': orgWorkspaceGrantFlag, - }); - try { - const proc = await cmd.execAsync({ - cwd: appPath, - }); - return proc.output; - } catch (error) { - throw commandError(error, 'triggerCreate'); + if ('triggerDef' in args) { + cmdOpts['--trigger-def'] = args.triggerDef; + } else { + cmdOpts['--workflow'] = args.workflow; + cmdOpts['--title'] = args.title; + if ('description' in args) { + cmdOpts['--description'] = args.description; + } + if ('interactivity' in args && args.interactivity) { + cmdOpts['--interactivity'] = true; + if ('interactivityName' in args && args.interactivityName) { + cmdOpts['--interactivity-name'] = args.interactivityName; + } + } } + const cmd = new SlackCLIProcess('trigger create', args, cmdOpts); + const proc = await cmd.execAsync({ + cwd: args.appPath, + }); + return proc.output; }; /** * `slack trigger delete` - * @param appPath path to the app - * @param teamFlag team domain to delete trigger from - * @param flag * @returns command output */ export const del = async function triggerDelete( - appPath: string, - teamFlag: string, - flag: string, - options?: { qa?: boolean }, + args: ProjectCommandArguments & TriggerIdArgument, ): Promise { - // TODO: (breaking change) separate params vs. single-param-object - // TODO: delete requires --trigger-id so add that to parameters (breaking change) - const cmd = new SlackCLIProcess(`trigger delete ${flag}`, { team: teamFlag, qa: options?.qa }); - try { - const proc = await cmd.execAsync({ - cwd: appPath, - }); - return proc.output; - } catch (error) { - throw commandError(error, 'triggerDelete'); - } + const cmd = new SlackCLIProcess('trigger delete', args, { + '--trigger-id': args.triggerId, + }); + const proc = await cmd.execAsync({ + cwd: args.appPath, + }); + return proc.output; }; /** * `slack trigger info` - * @param appPath path to the app - * @param teamFlag team domain of the trigger - * @param flag arbitrary additional flags * @returns command output */ export const info = async function triggerInfo( - appPath: string, - teamFlag: string, - flag: string, - options?: { qa?: boolean }, + args: ProjectCommandArguments & TriggerIdArgument, ): Promise { - // TODO: getting trigger info necessitates passing a trigger ID, so that should be exposed in the parameters here - // TODO: (breaking change) separate params vs. single-param-object - const cmd = new SlackCLIProcess(`trigger info ${flag}`, { team: teamFlag, qa: options?.qa }); - try { - const proc = await cmd.execAsync({ - cwd: appPath, - }); - return proc.output; - } catch (error) { - throw commandError(error, 'triggerInfo'); - } + const cmd = new SlackCLIProcess('trigger info', args, { + '--trigger-id': args.triggerId, + }); + const proc = await cmd.execAsync({ + cwd: args.appPath, + }); + return proc.output; }; /** * `slack trigger list` - * @param appPath path to app - * @param teamFlag team domain for listing all triggers - * @param flag arbitrary additional flags to pass * @returns command output */ export const list = async function triggerList( - appPath: string, - teamFlag: string, - flag: string, - options?: { qa?: boolean }, + args: ProjectCommandArguments & { + /** @description Limit the number of triggers to show. Defaults to `4`. */ + limit?: number; + /** + * @description Only display triggers of the given type, can be one of: + * `all`, `shortcut`, `event`, `scheduled`, `webhook` and `external`. Defaults to `all`. + */ + type?: 'all' | 'shortcut' | 'event' | 'scheduled' | 'webhook' | 'external'; + }, ): Promise { - // TODO: (breaking change) separate params vs. single-param-object - const cmd = new SlackCLIProcess(`trigger list ${flag}`, { team: teamFlag, qa: options?.qa }); - try { - const proc = await cmd.execAsync({ - cwd: appPath, - }); - return proc.output; - } catch (error) { - throw commandError(error, 'triggerList'); + const cmdOpts: SlackCLICommandOptions = {}; + if (args.limit) { + cmdOpts['--limit'] = args.limit; + } + if (args.type) { + cmdOpts['--type'] = args.type; } + const cmd = new SlackCLIProcess('trigger list', args, cmdOpts); + const proc = await cmd.execAsync({ + cwd: args.appPath, + }); + return proc.output; }; /** * `slack trigger update` - * @param appPath path to the app - * @param teamFlag team domain for the updating trigger - * @param flag arbitrary additional flags to pass to command * @returns command output */ export const update = async function triggerUpdate( - appPath: string, - teamFlag: string, - flag: string, - options?: { qa?: boolean }, + args: ProjectCommandArguments & TriggerIdArgument & (Partial | CreateFromFile), ): Promise { - // TODO: (breaking change) separate params vs. single-param-object - const cmd = new SlackCLIProcess(`trigger update ${flag}`, { team: teamFlag, qa: options?.qa }); - try { - const proc = await cmd.execAsync({ - cwd: appPath, - }); - return proc.output; - } catch (error) { - throw commandError(error, 'triggerUpdate'); + const cmdOpts: SlackCLICommandOptions = { + '--trigger-id': args.triggerId, + }; + if ('triggerDef' in args) { + cmdOpts['--trigger-def'] = args.triggerDef; + } else { + if ('workflow' in args) { + cmdOpts['--workflow'] = args.workflow; + } + if ('title' in args) { + cmdOpts['--title'] = args.title; + } + if ('description' in args) { + cmdOpts['--description'] = args.description; + } + if ('interactivity' in args) { + cmdOpts['--interactivity'] = args.interactivity; + } + if ('interactivityName' in args) { + cmdOpts['--interactivity-name'] = args.interactivityName; + } } + const cmd = new SlackCLIProcess('trigger update', args, cmdOpts); + const proc = await cmd.execAsync({ + cwd: args.appPath, + }); + return proc.output; }; -// TODO: (breaking change): rename properties of this default export to match actual command names export default { - triggerAccess: access, - triggerCreate: create, - triggerDelete: del, - triggerInfo: info, - triggerList: list, - triggerUpdate: update, + access, + create, + delete: del, + info, + list, + update, }; diff --git a/packages/cli-test/src/cli/index.spec.ts b/packages/cli-test/src/cli/index.spec.ts index 87887a8ae..f84a74b02 100644 --- a/packages/cli-test/src/cli/index.spec.ts +++ b/packages/cli-test/src/cli/index.spec.ts @@ -10,7 +10,7 @@ describe('cli module', () => { let deleteSpy: sinon.SinonStub; beforeEach(() => { - logoutSpy = sandbox.stub(SlackCLI, 'logout').resolves(); + logoutSpy = sandbox.stub(SlackCLI.auth, 'logout').resolves(); warnSpy = sandbox.stub(logger, 'warn'); sandbox.stub(SlackCLI.app, 'list').resolves('This thing has so many apps you would not believe'); deleteSpy = sandbox.stub(SlackCLI.app, 'delete').resolves(); @@ -20,22 +20,22 @@ describe('cli module', () => { }); describe('stopSession method', () => { - it('should invoke logout', async () => { - await SlackCLI.stopSession({ appTeamID: 'T123' }); + it('should invoke logout if `shouldLogOut` is truthy', async () => { + await SlackCLI.stopSession({ team: 'T123', shouldLogOut: true }); sandbox.assert.called(logoutSpy); }); it('should warn if logout failed', async () => { logoutSpy.rejects('boomsies'); - await SlackCLI.stopSession({ appTeamID: 'T123' }); + await SlackCLI.stopSession({ team: 'T123', shouldLogOut: true }); sandbox.assert.calledWithMatch(warnSpy, 'boomsies'); }); it('should attempt to delete app if appPath is provided', async () => { - await SlackCLI.stopSession({ appTeamID: 'T123', appPath: '/some/path' }); + await SlackCLI.stopSession({ team: 'T123', appPath: '/some/path' }); sandbox.assert.called(deleteSpy); }); it('should warn if app deletion fails', async () => { deleteSpy.rejects('explosions'); - await SlackCLI.stopSession({ appTeamID: 'T123', appPath: '/some/path' }); + await SlackCLI.stopSession({ team: 'T123', appPath: '/some/path' }); sandbox.assert.calledWithMatch(warnSpy, 'explosions'); }); }); diff --git a/packages/cli-test/src/cli/index.ts b/packages/cli-test/src/cli/index.ts index 0a6354fa2..50e60bf6f 100644 --- a/packages/cli-test/src/cli/index.ts +++ b/packages/cli-test/src/cli/index.ts @@ -1,112 +1,72 @@ -import * as fs from 'node:fs'; -import * as path from 'node:path'; +import fs from 'node:fs'; +import path from 'node:path'; -import appCommands from './commands/app'; -import authCommands from './commands/auth'; -import collaboratorCommands from './commands/collaborator'; -import createCommands from './commands/create'; -import envCommands from './commands/env'; -import externalAuthCommands from './commands/external-auth'; -import functionCommands from './commands/function'; -import manifestCommands from './commands/manifest'; -import platformCommands from './commands/platform'; -import triggerCommands from './commands/trigger'; +import app from './commands/app'; +import auth from './commands/auth'; +import collaborator from './commands/collaborator'; +import { create } from './commands/create'; +import env from './commands/env'; +import externalAuth from './commands/external-auth'; +import func from './commands/function'; +import manifest from './commands/manifest'; +import platform from './commands/platform'; +import trigger from './commands/trigger'; +import { ProjectCommandArguments } from '../types/commands/common_arguments'; import logger from '../utils/logger'; /** * Set of functions to spawn and interact with Slack Platform CLI processes and commands */ export const SlackCLI = { - ...appCommands, - app: { - delete: appCommands.workspaceDelete, - install: appCommands.workspaceInstall, - list: appCommands.workspaceList, - }, - ...authCommands, - auth: authCommands, - ...collaboratorCommands, // TODO: (breaking change) remove, mimic same 'namespacing' as the actual CLI - collaborators: { - add: collaboratorCommands.collaboratorsAdd, - list: collaboratorCommands.collaboratorsList, - remove: collaboratorCommands.collaboratorsRemove, - }, - ...createCommands, - ...envCommands, // TODO: (breaking change) remove, mimic same 'namespacing' as the actual CLI - env: { - add: envCommands.envAdd, - list: envCommands.envList, - remove: envCommands.envRemove, - }, - ...externalAuthCommands, - ...functionCommands, - function: { - access: functionCommands.functionAccess, - }, - ...manifestCommands, // TODO: (breaking change) remove, mimic same 'namespacing' as the actual CLI - manifest: { - validate: manifestCommands.manifestValidate, - }, - ...platformCommands, - platform: platformCommands, - ...triggerCommands, // TODO: (breaking change) remove, mimic same 'namespacing' as the actual CLI - trigger: { - access: triggerCommands.triggerAccess, - create: triggerCommands.triggerCreate, - delete: triggerCommands.triggerDelete, - info: triggerCommands.triggerInfo, - list: triggerCommands.triggerList, - update: triggerCommands.triggerUpdate, - }, + app, + auth, + collaborator, + create, + env, + externalAuth, + function: func, + manifest, + platform, + trigger, /** * Delete app and Log out of current team session * @param options */ - stopSession: async function stopSession({ - appPath, - appTeamID, - shouldLogOut = true, - isLocalApp, - qa, - }: { - // TODO: (breaking change) model these types better, if appPath isn't provided then appTeamId - // and isLocalApp are not needed either - /** Path to app. If not provided, will not interact with any app */ - appPath?: string; - /** Team domain or ID where app is installed */ - appTeamID: string; + stopSession: async function stopSession(args: Partial & { /** Should the CLI log out of its session with the team specified by `appTeamID`. Defaults to `true` */ shouldLogOut?: boolean; - isLocalApp?: boolean; - qa?: boolean; }): Promise { - // TODO: perhaps appPath does not exist, should guard against that. - if (appPath) { + if (args.appPath) { // List instances of app installation if app path provided - const installedAppsOutput = await SlackCLI.app.list(appPath, { qa }); + const installedAppsOutput = await SlackCLI.app.list({ + ...args, + appPath: args.appPath!, // very dumb https://github.com/microsoft/TypeScript/issues/42384#issuecomment-872906445 + }); // If app is installed if (!installedAppsOutput.includes('This project has no apps')) { // Soft app delete try { - await SlackCLI.app.delete(appPath, appTeamID, { isLocalApp, qa }); + await SlackCLI.app.delete({ + ...args, + appPath: args.appPath!, // same crap https://github.com/microsoft/TypeScript/issues/42384#issuecomment-872906445 + }); } catch (error) { logger.warn(`stopSession could not delete app gracefully, continuing. Error: ${error}`); } // Delete app.json file. Needed for retries. Otherwise asks for collaborator, if old file is present - fs.rmSync(path.join(appPath, '.slack'), { + fs.rmSync(path.join(args.appPath, '.slack'), { force: true, recursive: true, }); } } - if (shouldLogOut) { + if (args.shouldLogOut) { try { - await SlackCLI.logout({ teamFlag: appTeamID, qa }); + await SlackCLI.auth.logout(args); } catch (error) { - // TODO: maybe should error instead? this seems pretty bad logger.warn(`Could not logout gracefully. Error: ${error}`); } } diff --git a/packages/cli-test/src/cli/shell.spec.ts b/packages/cli-test/src/cli/shell.spec.ts index 57cb5d67d..a98207c2f 100644 --- a/packages/cli-test/src/cli/shell.spec.ts +++ b/packages/cli-test/src/cli/shell.spec.ts @@ -1,13 +1,12 @@ import child from 'child_process'; -import EventEmitter from 'events'; -import stream from 'stream'; import { assert } from 'chai'; import sinon from 'sinon'; import { shell } from './shell'; +import { mockProcess } from '../utils/test'; -import type { ShellProcess } from '../utils/types'; +import type { ShellProcess } from '../types/shell'; describe('shell module', () => { const sandbox = sinon.createSandbox(); @@ -17,10 +16,7 @@ describe('shell module', () => { let runOutput: child.SpawnSyncReturns; beforeEach(() => { - spawnProcess = new EventEmitter() as child.ChildProcessWithoutNullStreams; - spawnProcess.stdout = new EventEmitter() as stream.Readable; - spawnProcess.stderr = new EventEmitter() as stream.Readable; - spawnProcess.stdin = new stream.Writable(); + spawnProcess = mockProcess(); spawnSpy = sandbox.stub(child, 'spawn').returns(spawnProcess); runOutput = { pid: 1337, output: [], stdout: Buffer.from([]), stderr: Buffer.from([]), status: 0, signal: null }; runSpy = sandbox.stub(child, 'spawnSync').returns(runOutput); diff --git a/packages/cli-test/src/cli/shell.ts b/packages/cli-test/src/cli/shell.ts index 7c6766086..a53a6f277 100644 --- a/packages/cli-test/src/cli/shell.ts +++ b/packages/cli-test/src/cli/shell.ts @@ -5,7 +5,7 @@ import treekill from 'tree-kill'; import { timeouts } from '../utils/constants'; import logger from '../utils/logger'; -import type { ShellProcess } from '../utils/types'; +import type { ShellProcess } from '../types/shell'; export const shell = { /** @@ -71,7 +71,7 @@ export const shell = { * Run shell command synchronously * - Execute child process with the command * - Wait for the command to complete and return the standard output - * @param command cli command, e.g. --version or any shell command + * @param command cli command * @param shellOpts various shell spawning options available to customize * @returns command stdout */ @@ -93,7 +93,6 @@ export const shell = { // Log command logger.info(`CLI Command finished: ${command}`); - // TODO: this method only returns stdout and not stderr... return this.removeANSIcolors(result.stdout.toString()); } catch (error) { throw new Error(`runCommandSync failed!\nCommand: ${command}\nError: ${error}`); diff --git a/packages/cli-test/src/types/commands/common_arguments.ts b/packages/cli-test/src/types/commands/common_arguments.ts new file mode 100644 index 000000000..c14b8bff3 --- /dev/null +++ b/packages/cli-test/src/types/commands/common_arguments.ts @@ -0,0 +1,68 @@ +import type { SlackCLIGlobalOptions } from '../../cli/cli-process'; + +export interface ProjectCommandArguments extends SlackCLIGlobalOptions { + /** + * @description Path to the Slack CLI-generated application to run the command in. + * Sets the `cwd` on the shell process executing the CLI. + */ + appPath: string; +} + +export interface WorkspaceGrantArgument { + /** + * @description Org workspace ID, or the string `all` to request access to all workspaces in the org, + * to request grant access to in AAA scenarios + */ + orgWorkspaceGrantFlag?: string; +} + +// Various access-modification arguments (applies to `function access` and `trigger access` calls. +/** + * @description Single-shot grant-to-group access change arguments. + * @example `appCollaborators: true`, or `everyone: true` + */ +export type GroupAccessChangeArguments = GrantAppCollaboratorsArgument | GrantEveryoneArgument; + +interface GrantAppCollaboratorsArgument { + /** @description Grant access for function specified by `name` to app collaborators. */ + appCollaborators: true; +} +interface GrantEveryoneArgument { + /** @description Grant access for function specified by `name` to everyone. */ + everyone: true; +} +interface GrantArgument { + /** @description Grant access for function specified by `name` to users specified by `users`. */ + grant: true; +} +interface RevokeArgument { + /** @description Revoke access for function specified by `name` to users specified by `users`. */ + revoke: true; +} + +/** + * @description Grant/revoke user access arguments. + */ +export type UserAccessChangeArguments = { + /** @description Array of user IDs to grant or revoke access to. */ + users: [string, ...string[]]; +} & (RevokeArgument | GrantArgument); +/** + * @description Grant/revoke user access arguments. + */ +export type ChannelAccessChangeArguments = { + /** @description Array of channel IDs to grant or revoke access to. */ + channels: [string, ...string[]]; +} & (RevokeArgument | GrantArgument); +/** + * @description Grant/revoke organization access arguments. + */ +export type OrganizationAccessChangeArguments = { + /** @description Array of organization IDs to grant or revoke access to. */ + organizations: [string, ...string[]]; +} & (RevokeArgument | GrantArgument); + +export interface InfoArgument { + /** @description Whether to show access information. Supercedes all other arguments. */ + info: true; +} diff --git a/packages/cli-test/src/utils/types.ts b/packages/cli-test/src/types/shell.ts similarity index 62% rename from packages/cli-test/src/utils/types.ts rename to packages/cli-test/src/types/shell.ts index 2deaa14c0..47627aed7 100644 --- a/packages/cli-test/src/utils/types.ts +++ b/packages/cli-test/src/types/shell.ts @@ -1,14 +1,5 @@ import type { ChildProcessWithoutNullStreams } from 'child_process'; -export const SlackProduct = { - FREE: 'FREE', - PRO: 'PRO', - BUSINESS_PLUS: 'PLUS', - ENTERPRISE: 'ENTERPRISE', - ENTERPRISE_SANDBOX: 'ENTERPRISE_SANDBOX', - ENTERPRISE_SELECT: 'ENTERPRISE_SELECT', -}; - export interface ShellProcess { /** * Child process object diff --git a/packages/cli-test/src/utils/custom-errors.ts b/packages/cli-test/src/utils/custom-errors.ts deleted file mode 100644 index 190f7f886..000000000 --- a/packages/cli-test/src/utils/custom-errors.ts +++ /dev/null @@ -1,50 +0,0 @@ -// TODO: refactor this error class: -// - reuse nodejs error `cause` to encode the 'wrapping' behaviour this class intends to implement https://nodejs.org/api/errors.html#errorcause -// - instead of `name`, consider reusing node error `code` https://nodejs.org/api/errors.html#errorcode -// - review how stack traces present themselves and possibly consider using `captureStackTrace` https://nodejs.org/api/errors.html#errorcapturestacktracetargetobject-constructoropt - -/** - * Custom error class for cli methods - */ -export class CustomError extends Error { - public name: string; - - public command: string | undefined; - - public additionalInfo: string | undefined; - - /** - * Inherit and create new instance of default Error class - * @param message - * @param name - * @param stack - * @param options - */ - public constructor( - message: string, - name: string, - stack: string | undefined, - options?: { - /** - * Command used - */ - command?: string; - /** - * Any additional info - */ - additionalInfo?: string; - }, - ) { - super(message); - this.name = name; - this.stack = stack; - this.command = options?.command; - this.additionalInfo = options?.additionalInfo; - - // Set a more readable error message - this.message = `${this.name}: ${this.command}; ${this.additionalInfo}`; - - // Set the prototype explicitly - Object.setPrototypeOf(this, CustomError.prototype); - } -} diff --git a/packages/cli-test/src/utils/test.ts b/packages/cli-test/src/utils/test.ts new file mode 100644 index 000000000..294d37bfc --- /dev/null +++ b/packages/cli-test/src/utils/test.ts @@ -0,0 +1,11 @@ +import child from 'child_process'; +import EventEmitter from 'events'; +import stream from 'stream'; + +export function mockProcess(): child.ChildProcessWithoutNullStreams { + const spawnProcess = new EventEmitter() as child.ChildProcessWithoutNullStreams; + spawnProcess.stdout = new EventEmitter() as stream.Readable; + spawnProcess.stderr = new EventEmitter() as stream.Readable; + spawnProcess.stdin = new stream.Writable(); + return spawnProcess; +}