邮件与消息

Slack

Slack app installs through OAuth, the Web API with its scope checks, incoming webhooks, and signed events, slash commands and interactions sent to your app.

本页还没有中文版,下面是英文原文。

danwo's Slack emulator answers Slack's API, its incoming webhooks and file uploads. Your app installs through OAuth, calls the Web API with @slack/web-api or Bolt, and receives events, slash commands and button clicks signed with its signing secret.

Set up

Add slackService with your app as its configuration page has it, and install it in advance so the test needn't go through the consent page. Hand the app its token and signing secret:

danwo.config.ts
import { appEnvironment, defineConfig, slackService } from "@danwo/sdk"

const url = "http://localhost:3000"
const signingSecret = "danwo-signing-secret"

export default defineConfig({
  testDir: "tests",
  readiness: { mode: "page+backend", settle: true },
  environment: () =>
    appEnvironment({
      app: {
        command: "npm run dev",
        url,
        env: { SLACK_BOT_TOKEN: "xoxb-danwo", SLACK_SIGNING_SECRET: signingSecret },
      },
      services: [
        slackService({
          seed: {
            users: [{ name: "ada", email: "[email protected]" }],
            channels: [{ name: "deploys", members: ["ada"] }],
            apps: [
              {
                name: "Deploy bot",
                client_id: "1.1",
                client_secret: "danwo-client-secret",
                signing_secret: signingSecret,
                bot_scopes: ["chat:write", "commands"],
                events: { url: `${url}/api/slack/events`, bot_events: ["app_mention"] },
                commands: [{ command: "/deploy", url: `${url}/api/slack/commands` }],
              },
            ],
            installations: [{ app: "Deploy bot", bot_token: "xoxb-danwo", channels: ["deploys"] }],
          },
        }),
      ],
    }),
})

Seed it

FieldWhat it seeds
teamThe workspace's name, id and domain.
usersMembers by handle, with email, real name and admin; team_id for a member of another workspace.
channelsChannels, private or public, with members; #general always exists; is_ext_shared to share one.
appsYour app: credentials, scopes, event subscriptions, slash commands, interactivity URL, token rotation.
installationsApps already installed, with fixed bot and user tokens and the channels the bot is in.

Use it in a test

Run a slash command as a member, then read what the bot posted:

tests/deploy.danwo.ts
import { type SlackFixtures, slackWorkspace, test as base } from "@danwo/sdk"

const test = base.extend<SlackFixtures>()

test("announces a deploy in the channel", async ({ expect, settle, slack }) => {
  const workspace = slackWorkspace(slack)

  workspace.command("ada", "#deploys", "/deploy", "production")
  await settle()

  const [message] = workspace.messages("#deploys")

  expect(message?.text).toContain("Deploying production")
})
slackWorkspace(slack)What it's for
post, react, join, leaveAct as a member in a channel; leave with by removes someone.
commandRun a slash command as a member.
click, submit, closeClick a button, submit a modal, or close it.
convertMake a channel private or public, as an admin.
uninstallRemove your app from the workspace, as an admin.
messages, thread, ephemeralRead what was posted.
views, fileRead the open views, with any errors your app showed, and files.
verifyEventsUrl()Send Slack's challenge to your events URL.

Modals

Slack does what your app answers a submission with. An empty answer closes the modal, and a late or failed one leaves it open, as Slack does after its three seconds:

Your app answersThe modal
Nothing (an empty 200)Closes.
response_action: "errors"Stays open with the messages under blocks.
response_action: "update"Is replaced by the view you sent.
response_action: "push"Gets a new view on top, three at most.
response_action: "clear"Closes, with every view stacked on it.
tests/deploy-form.danwo.ts
import { type SlackFixtures, slackWorkspace, test as base } from "@danwo/sdk"

const test = base.extend<SlackFixtures>()

test("refuses a deploy without an environment", async ({ expect, settle, slack }) => {
  const workspace = slackWorkspace(slack)
  const [modal] = workspace.views("ada")
  const values = { env: { name: { type: "plain_text_input", value: "" } } }

  workspace.submit("ada", String(modal?.id), values)
  await settle()

  expect(workspace.views("ada")[0]?.errors).toEqual({ env: "Pick an environment" })
})

What's covered

AreaCovered
OAuth v2The consent page, token exchange and rotation, testing and revoking tokens.
ChatPost, update, delete and schedule messages, ephemeral messages, threads, blocks.
ConversationsCreate, list, read history and replies, join, invite, leave, kick and open DMs.
AdminMake a channel private or public, for a workspace admin.
Appsapps.uninstall, and removing the app as an admin.
Slack ConnectMembers from another workspace and the channels shared with them.
UsersList users, read one, look one up by email.
Reactions, views, filesReactions, modals and home tabs, and external file uploads.
IncomingIncoming webhooks and response URLs.
Sent to your appEvents API callbacks, slash commands and interactive payloads.
Answered by your appModal submissions: close, errors, update, push and clear.

Every method checks the token's scopes and answers { ok: false, error } as Slack does, such as missing_scope or channel_not_found.

Faults

CallFaults the matrix tries
A Web API write, such as chat.postMessage429, 500, 503, a 200 with ok: false, a 10-second delay, connection reset, answer lost after Slack acted
A Web API readthe same, without the lost answer
Every event, command and interactionarrives twice, arrives 10 minutes late, forged signature, 1 hour stale

Webhooks

What Slack sends your app carries X-Slack-Signature and X-Slack-Request-Timestamp, made with the app's signing_secret, so Bolt's request verification accepts it. A stale timestamp is one your app should refuse.

A failed event is retried after one second, one minute and five minutes, with X-Slack-Retry-Num. A slash command or an interaction is sent once and never retried, as on Slack.

Besides messages, mentions and reactions, your app hears about:

EventWhen
member_joined_channelSomeone joins a channel your bot is in.
member_left_channelSomeone leaves or is removed from it.
channel_left, group_leftYour bot is removed from a public or private channel, and by whom.
channel_convert_to_private/publicAn admin changes a channel's privacy (a message subtype).
app_uninstalled, tokens_revokedYour app is removed; its tokens are already dead when these come.
view_closedA member closes a modal that set notify_on_close.

In a channel shared through Slack Connect, events carry is_ext_shared_channel: true, and messages say which workspace the author is in with user_team and source_team.

Fidelity

Public endpoints only. danwo fidelity slack checks what Slack answers without a workspace: api.test, and the errors for a missing or unknown token. Both match.

Not covered yet

  • Socket Mode
  • Enterprise Grid, Slack Connect invitations and workflow steps
  • Search, pins, bookmarks and reminders

Next

本页内容

在每个 Pull Request 上运行

每个 Pull Request 一个 GitHub Check 和一条评论,并附上完整的运行记录。

配置 CI