File size: 7,094 Bytes
95b671e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | import { strict as assert } from "node:assert";
import { test } from "node:test";
// slack.ts transitively imports the db client, which throws at import time
// without a connection string. Set a dummy URL before the dynamic import (the
// porsager client connects lazily, so these pure-function tests never open a
// socket). Same dynamic-import pattern as detail.test.ts / loops.test.ts.
process.env.DATABASE_URL ??= "postgres://localhost:5434/superlog";
process.env.BETTER_AUTH_SECRET ??= "test-better-auth-secret-with-enough-length";
const { preferPinnedInstallation } = await import("./slack.js");
type FakeResponse = { ok: boolean; channels?: unknown[]; error?: string; cursor?: string };
function fakeFetch(pages: FakeResponse[]) {
const calls: URL[] = [];
let i = 0;
const fetchImpl: typeof fetch = async (input) => {
calls.push(input as URL);
const page = pages[i++] ?? { ok: true, channels: [] };
const body: Record<string, unknown> = { ok: page.ok };
if (page.error) body.error = page.error;
if (page.channels) body.channels = page.channels;
if (page.cursor) body.response_metadata = { next_cursor: page.cursor };
return { json: async () => body } as unknown as Response;
};
return { fetchImpl, calls };
}
// Regression guard: a workspace installed into multiple Superlog projects owns
// several non-revoked `slack_installations` rows (upsertInstallation keys by
// project). Slack only keeps the most-recently-refreshed bot token live, so an
// unordered team-wide lookup can hand back a stale row whose token fails every
// API call with `invalid_auth` — which is exactly what broke the incident
// feedback modal (views.open -> invalid_auth). Incidents/proposals pin the
// exact installation that posted their thread, so that pin must win.
test("prefers the installation pinned to the incident over a team-wide match", () => {
const pinned = { id: "pinned", botAccessToken: "live" };
const teamFallback = { id: "other", botAccessToken: "stale" };
assert.equal(preferPinnedInstallation(pinned, teamFallback), pinned);
});
test("falls back to the team match when the incident has no pinned installation", () => {
const teamFallback = { id: "other", botAccessToken: "stale" };
assert.equal(preferPinnedInstallation(null, teamFallback), teamFallback);
});
test("returns null when neither a pin nor a team match resolves", () => {
assert.equal(preferPinnedInstallation(null, null), null);
});
test("Slack resolve clicks on noise incidents refresh resolved-side effects", async () => {
const { resolveSlackResolveClickDisposition } = await import("./slack.js");
assert.equal(resolveSlackResolveClickDisposition("autoresolved_noise"), "refresh_side_effects");
});
test("Slack resolve clicks on open incidents perform a fresh resolve", async () => {
const { resolveSlackResolveClickDisposition } = await import("./slack.js");
assert.equal(resolveSlackResolveClickDisposition("open"), "resolve");
});
test("listSlackChannels requests both public and private channels", async () => {
const { listSlackChannels } = await import("./slack.js");
const { fetchImpl, calls } = fakeFetch([{ ok: true, channels: [] }]);
await listSlackChannels("xoxb-token", fetchImpl);
assert.equal(calls.length, 1);
assert.equal(calls[0]?.searchParams.get("types"), "public_channel,private_channel");
assert.equal(calls[0]?.searchParams.get("exclude_archived"), "true");
});
test("listSlackChannels follows cursor pagination and aggregates all channels", async () => {
const { listSlackChannels } = await import("./slack.js");
const { fetchImpl, calls } = fakeFetch([
{
ok: true,
channels: [{ id: "C1", name: "general", is_private: false }],
cursor: "page2",
},
{
ok: true,
channels: [{ id: "G1", name: "secret-room", is_private: true }],
},
]);
const result = await listSlackChannels("xoxb-token", fetchImpl);
assert.equal(result.ok, true);
assert.equal(calls.length, 2);
assert.equal(calls[1]?.searchParams.get("cursor"), "page2");
assert.ok(result.ok);
assert.deepEqual(result.channels, [
{ id: "C1", name: "general", isPrivate: false },
{ id: "G1", name: "secret-room", isPrivate: true },
]);
// the private channel must survive into the final list
assert.ok(result.channels.some((c) => c.isPrivate && c.name === "secret-room"));
});
test("listSlackChannels returns the Slack error without paginating further", async () => {
const { listSlackChannels } = await import("./slack.js");
const { fetchImpl, calls } = fakeFetch([{ ok: false, error: "token_revoked" }]);
const result = await listSlackChannels("xoxb-token", fetchImpl);
assert.equal(result.ok, false);
assert.equal(calls.length, 1);
assert.ok(!result.ok);
assert.equal(result.error, "token_revoked");
});
// Regression guard: clicking Send on the Slack incident feedback modal kept
// surfacing "We had some trouble connecting. Try again?". Slack's
// view_submission ack contract requires an EMPTY 200 body to close the modal;
// our route was returning `{"ok":true}`, which Slack treats as an invalid
// response and refuses to close. The ack body must be empty.
test("view_submission ack is an empty 200 body (closes the Slack modal)", async () => {
const { Hono } = await import("hono");
const { mountSlackPublic } = await import("./slack.js");
const secret = "test-slack-signing-secret";
process.env.SLACK_SIGNING_SECRET = secret;
const app = new Hono();
mountSlackPublic(app);
// Empty feedback value → handler returns before any DB access, isolating the
// ack-body behavior we care about.
const payload = {
type: "view_submission",
view: {
callback_id: "feedback_modal:incident-123",
state: { values: { feedback_body: { value: { value: "" } } } },
},
user: { id: "U1" },
team: { id: "T1" },
};
const rawBody = `payload=${encodeURIComponent(JSON.stringify(payload))}`;
const ts = Math.floor(Date.now() / 1000).toString();
const crypto = await import("node:crypto");
const sig = `v0=${crypto
.createHmac("sha256", secret)
.update(`v0:${ts}:${rawBody}`)
.digest("hex")}`;
const res = await app.request("/slack/interactivity", {
method: "POST",
headers: {
"content-type": "application/x-www-form-urlencoded",
"x-slack-signature": sig,
"x-slack-request-timestamp": ts,
},
body: rawBody,
});
assert.equal(res.status, 200);
assert.equal(await res.text(), "");
});
test("listSlackChannels returns an error when the page cap is exhausted", async () => {
const { listSlackChannels } = await import("./slack.js");
const { fetchImpl, calls } = fakeFetch(
Array.from({ length: 50 }, (_, i) => ({
ok: true,
channels: [{ id: `C${i}`, name: `channel-${i}` }],
cursor: `page-${i + 1}`,
})),
);
const result = await listSlackChannels("xoxb-token", fetchImpl);
assert.equal(calls.length, 50);
assert.equal(result.ok, false);
assert.ok(!result.ok);
assert.equal(result.error, "pagination_limit_exceeded");
});
|