@superblocksteam/sdk-api 2.0.105 → 2.0.106-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +439 -89
- package/dist/api/definition.d.ts +11 -6
- package/dist/api/definition.d.ts.map +1 -1
- package/dist/api/definition.js +19 -12
- package/dist/api/definition.js.map +1 -1
- package/dist/api/definition.test.js +39 -15
- package/dist/api/definition.test.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -1
- package/dist/index.d.ts +10 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -5
- package/dist/index.js.map +1 -1
- package/dist/integrations/base/index.d.ts +2 -1
- package/dist/integrations/base/index.d.ts.map +1 -1
- package/dist/integrations/base/index.js +1 -0
- package/dist/integrations/base/index.js.map +1 -1
- package/dist/integrations/base/rest-api-client-base.d.ts +48 -0
- package/dist/integrations/base/rest-api-client-base.d.ts.map +1 -0
- package/dist/integrations/base/rest-api-client-base.js +98 -0
- package/dist/integrations/base/rest-api-client-base.js.map +1 -0
- package/dist/integrations/base/rest-api-integration-client.d.ts +10 -20
- package/dist/integrations/base/rest-api-integration-client.d.ts.map +1 -1
- package/dist/integrations/base/rest-api-integration-client.js +10 -65
- package/dist/integrations/base/rest-api-integration-client.js.map +1 -1
- package/dist/integrations/box/types.d.ts +1 -1
- package/dist/integrations/declarations.d.ts +5 -73
- package/dist/integrations/declarations.d.ts.map +1 -1
- package/dist/integrations/declarations.js +5 -68
- package/dist/integrations/declarations.js.map +1 -1
- package/dist/integrations/documentation.test.js +0 -2
- package/dist/integrations/documentation.test.js.map +1 -1
- package/dist/integrations/googledrive/types.d.ts +1 -1
- package/dist/integrations/index.d.ts +1 -11
- package/dist/integrations/index.d.ts.map +1 -1
- package/dist/integrations/index.js +1 -7
- package/dist/integrations/index.js.map +1 -1
- package/dist/integrations/registry.d.ts +1 -11
- package/dist/integrations/registry.d.ts.map +1 -1
- package/dist/integrations/registry.js +0 -29
- package/dist/integrations/registry.js.map +1 -1
- package/dist/integrations/slack/client.d.ts +13 -9
- package/dist/integrations/slack/client.d.ts.map +1 -1
- package/dist/integrations/slack/client.js +60 -8
- package/dist/integrations/slack/client.js.map +1 -1
- package/dist/integrations/slack/client.test.d.ts +11 -0
- package/dist/integrations/slack/client.test.d.ts.map +1 -0
- package/dist/integrations/slack/client.test.js +368 -0
- package/dist/integrations/slack/client.test.js.map +1 -0
- package/dist/integrations/slack/index.d.ts +2 -1
- package/dist/integrations/slack/index.d.ts.map +1 -1
- package/dist/integrations/slack/index.js +1 -0
- package/dist/integrations/slack/index.js.map +1 -1
- package/dist/integrations/slack/types.d.ts +127 -28
- package/dist/integrations/slack/types.d.ts.map +1 -1
- package/dist/integrations/slack/types.js +27 -1
- package/dist/integrations/slack/types.js.map +1 -1
- package/dist/integrations/snowflake/client.d.ts +2 -2
- package/dist/integrations/snowflake/client.js +2 -2
- package/dist/runtime/context.d.ts +1 -1
- package/dist/runtime/executor.d.ts +2 -2
- package/dist/types.d.ts +15 -6
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/api/definition.test.ts +40 -15
- package/src/api/definition.ts +19 -12
- package/src/errors.ts +1 -1
- package/src/index.ts +13 -33
- package/src/integrations/asana/README.md +12 -12
- package/src/integrations/base/index.ts +2 -1
- package/src/integrations/base/rest-api-client-base.ts +134 -0
- package/src/integrations/base/rest-api-integration-client.ts +12 -89
- package/src/integrations/bitbucket/README.md +19 -19
- package/src/integrations/box/README.md +24 -24
- package/src/integrations/box/types.ts +1 -1
- package/src/integrations/circleci/README.md +18 -18
- package/src/integrations/declarations.ts +5 -105
- package/src/integrations/documentation.test.ts +0 -2
- package/src/integrations/googledrive/README.md +25 -22
- package/src/integrations/googledrive/types.ts +1 -1
- package/src/integrations/graphql/README.md +2 -2
- package/src/integrations/groq/README.md +8 -8
- package/src/integrations/index.ts +0 -51
- package/src/integrations/mongodb/README.md +65 -12
- package/src/integrations/perplexity/README.md +39 -48
- package/src/integrations/registry.ts +1 -39
- package/src/integrations/salesforce/README.md +11 -9
- package/src/integrations/slack/README.md +62 -19
- package/src/integrations/slack/client.test.ts +553 -0
- package/src/integrations/slack/client.ts +92 -12
- package/src/integrations/slack/index.ts +6 -1
- package/src/integrations/slack/types.ts +142 -29
- package/src/integrations/snowflake/client.ts +2 -2
- package/src/integrations/zoom/README.md +15 -15
- package/src/runtime/context.ts +1 -1
- package/src/runtime/executor.ts +2 -2
- package/src/types.ts +15 -6
- package/dist/integrations/couchbase/client.d.ts +0 -36
- package/dist/integrations/couchbase/client.d.ts.map +0 -1
- package/dist/integrations/couchbase/client.js +0 -148
- package/dist/integrations/couchbase/client.js.map +0 -1
- package/dist/integrations/couchbase/index.d.ts +0 -8
- package/dist/integrations/couchbase/index.d.ts.map +0 -1
- package/dist/integrations/couchbase/index.js +0 -7
- package/dist/integrations/couchbase/index.js.map +0 -1
- package/dist/integrations/couchbase/types.d.ts +0 -100
- package/dist/integrations/couchbase/types.d.ts.map +0 -1
- package/dist/integrations/couchbase/types.js +0 -5
- package/dist/integrations/couchbase/types.js.map +0 -1
- package/dist/integrations/kafka/client.d.ts +0 -25
- package/dist/integrations/kafka/client.d.ts.map +0 -1
- package/dist/integrations/kafka/client.js +0 -124
- package/dist/integrations/kafka/client.js.map +0 -1
- package/dist/integrations/kafka/index.d.ts +0 -8
- package/dist/integrations/kafka/index.d.ts.map +0 -1
- package/dist/integrations/kafka/index.js +0 -7
- package/dist/integrations/kafka/index.js.map +0 -1
- package/dist/integrations/kafka/types.d.ts +0 -113
- package/dist/integrations/kafka/types.d.ts.map +0 -1
- package/dist/integrations/kafka/types.js +0 -5
- package/dist/integrations/kafka/types.js.map +0 -1
- package/dist/integrations/kinesis/client.d.ts +0 -31
- package/dist/integrations/kinesis/client.d.ts.map +0 -1
- package/dist/integrations/kinesis/client.js +0 -101
- package/dist/integrations/kinesis/client.js.map +0 -1
- package/dist/integrations/kinesis/index.d.ts +0 -8
- package/dist/integrations/kinesis/index.d.ts.map +0 -1
- package/dist/integrations/kinesis/index.js +0 -7
- package/dist/integrations/kinesis/index.js.map +0 -1
- package/dist/integrations/kinesis/types.d.ts +0 -97
- package/dist/integrations/kinesis/types.d.ts.map +0 -1
- package/dist/integrations/kinesis/types.js +0 -7
- package/dist/integrations/kinesis/types.js.map +0 -1
- package/dist/integrations/python/client.d.ts +0 -42
- package/dist/integrations/python/client.d.ts.map +0 -1
- package/dist/integrations/python/client.js +0 -89
- package/dist/integrations/python/client.js.map +0 -1
- package/dist/integrations/python/client.test.d.ts +0 -5
- package/dist/integrations/python/client.test.d.ts.map +0 -1
- package/dist/integrations/python/client.test.js +0 -214
- package/dist/integrations/python/client.test.js.map +0 -1
- package/dist/integrations/python/index.d.ts +0 -6
- package/dist/integrations/python/index.d.ts.map +0 -1
- package/dist/integrations/python/index.js +0 -5
- package/dist/integrations/python/index.js.map +0 -1
- package/dist/integrations/python/types.d.ts +0 -85
- package/dist/integrations/python/types.d.ts.map +0 -1
- package/dist/integrations/python/types.js +0 -5
- package/dist/integrations/python/types.js.map +0 -1
- package/dist/integrations/redis/client.d.ts +0 -43
- package/dist/integrations/redis/client.d.ts.map +0 -1
- package/dist/integrations/redis/client.js +0 -142
- package/dist/integrations/redis/client.js.map +0 -1
- package/dist/integrations/redis/index.d.ts +0 -8
- package/dist/integrations/redis/index.d.ts.map +0 -1
- package/dist/integrations/redis/index.js +0 -7
- package/dist/integrations/redis/index.js.map +0 -1
- package/dist/integrations/redis/types.d.ts +0 -137
- package/dist/integrations/redis/types.d.ts.map +0 -1
- package/dist/integrations/redis/types.js +0 -5
- package/dist/integrations/redis/types.js.map +0 -1
- package/src/integrations/couchbase/README.md +0 -138
- package/src/integrations/couchbase/client.ts +0 -225
- package/src/integrations/couchbase/index.ts +0 -8
- package/src/integrations/couchbase/types.ts +0 -126
- package/src/integrations/kafka/README.md +0 -144
- package/src/integrations/kafka/client.ts +0 -216
- package/src/integrations/kafka/index.ts +0 -14
- package/src/integrations/kafka/types.ts +0 -128
- package/src/integrations/kinesis/README.md +0 -153
- package/src/integrations/kinesis/client.ts +0 -146
- package/src/integrations/kinesis/index.ts +0 -14
- package/src/integrations/kinesis/types.ts +0 -114
- package/src/integrations/python/README.md +0 -566
- package/src/integrations/python/client.test.ts +0 -341
- package/src/integrations/python/client.ts +0 -136
- package/src/integrations/python/index.ts +0 -6
- package/src/integrations/python/types.ts +0 -92
- package/src/integrations/redis/README.md +0 -200
- package/src/integrations/redis/client.ts +0 -208
- package/src/integrations/redis/index.ts +0 -8
- package/src/integrations/redis/types.ts +0 -167
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for SlackClientImpl.
|
|
3
|
+
*
|
|
4
|
+
* Validates the SlackResponse<T> discriminated union contract:
|
|
5
|
+
* - ok:false from Slack → SlackErrorResponse (no throw)
|
|
6
|
+
* - ok:true from Slack → Zod-validated success data
|
|
7
|
+
* - ok:true but schema mismatch → RestApiValidationError (thrown)
|
|
8
|
+
* - Body validation failures still throw RestApiValidationError
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, vi } from "vitest";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
|
|
14
|
+
import { RestApiValidationError } from "../../errors.js";
|
|
15
|
+
import type { IntegrationConfig } from "../types.js";
|
|
16
|
+
import { SlackClientImpl } from "./client.js";
|
|
17
|
+
import type { SlackErrorResponse } from "./types.js";
|
|
18
|
+
|
|
19
|
+
const TEST_CONFIG: IntegrationConfig = {
|
|
20
|
+
id: "slack-test-id",
|
|
21
|
+
name: "Test Slack",
|
|
22
|
+
pluginId: "slack",
|
|
23
|
+
configuration: {},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function createClient(mockResult: unknown) {
|
|
27
|
+
const executeQuery = vi.fn().mockResolvedValue(mockResult);
|
|
28
|
+
const client = new SlackClientImpl(TEST_CONFIG, executeQuery);
|
|
29
|
+
return { client, executeQuery };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const ChannelsSchema = z.object({
|
|
33
|
+
channels: z.array(z.object({ id: z.string(), name: z.string() })),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const PostMessageSchema = z.object({
|
|
37
|
+
channel: z.string(),
|
|
38
|
+
ts: z.string(),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("SlackClientImpl", () => {
|
|
42
|
+
// ── Success responses ──────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
describe("success responses (ok: true)", () => {
|
|
45
|
+
it("returns validated data with ok: true", async () => {
|
|
46
|
+
const { client } = createClient({
|
|
47
|
+
ok: true,
|
|
48
|
+
channels: [{ id: "C123", name: "general" }],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const result = await client.apiRequest(
|
|
52
|
+
{ method: "GET", path: "/conversations.list" },
|
|
53
|
+
{ response: ChannelsSchema },
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
expect(result.ok).toBe(true);
|
|
57
|
+
if (result.ok) {
|
|
58
|
+
expect(result.channels).toHaveLength(1);
|
|
59
|
+
expect(result.channels[0]).toEqual({ id: "C123", name: "general" });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("strips unknown keys via Zod strip mode", async () => {
|
|
64
|
+
const { client } = createClient({
|
|
65
|
+
ok: true,
|
|
66
|
+
channels: [{ id: "C1", name: "test", extra: true }],
|
|
67
|
+
response_metadata: { next_cursor: "" },
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const result = await client.apiRequest(
|
|
71
|
+
{ method: "GET", path: "/conversations.list" },
|
|
72
|
+
{ response: ChannelsSchema },
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
expect(result.ok).toBe(true);
|
|
76
|
+
if (result.ok) {
|
|
77
|
+
expect(result).not.toHaveProperty("response_metadata");
|
|
78
|
+
expect(result.channels[0]).not.toHaveProperty("extra");
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("validates a post message success response", async () => {
|
|
83
|
+
const { client } = createClient({
|
|
84
|
+
ok: true,
|
|
85
|
+
channel: "C123",
|
|
86
|
+
ts: "1234567890.123456",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const result = await client.apiRequest(
|
|
90
|
+
{
|
|
91
|
+
method: "POST",
|
|
92
|
+
path: "/chat.postMessage",
|
|
93
|
+
body: { channel: "C123", text: "hello" },
|
|
94
|
+
},
|
|
95
|
+
{ response: PostMessageSchema },
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
expect(result.ok).toBe(true);
|
|
99
|
+
if (result.ok) {
|
|
100
|
+
expect(result.ts).toBe("1234567890.123456");
|
|
101
|
+
expect(result.channel).toBe("C123");
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ── Error responses (ok: false) ────────────────────────────────
|
|
107
|
+
|
|
108
|
+
describe("error responses (ok: false)", () => {
|
|
109
|
+
it("returns SlackErrorResponse instead of throwing", async () => {
|
|
110
|
+
const { client } = createClient({
|
|
111
|
+
ok: false,
|
|
112
|
+
error: "channel_not_found",
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const result = await client.apiRequest(
|
|
116
|
+
{
|
|
117
|
+
method: "POST",
|
|
118
|
+
path: "/chat.postMessage",
|
|
119
|
+
body: { channel: "C000", text: "hi" },
|
|
120
|
+
},
|
|
121
|
+
{ response: PostMessageSchema },
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
expect(result.ok).toBe(false);
|
|
125
|
+
if (!result.ok) {
|
|
126
|
+
expect(result.error).toBe("channel_not_found");
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("includes needed scopes on missing_scope errors", async () => {
|
|
131
|
+
const { client } = createClient({
|
|
132
|
+
ok: false,
|
|
133
|
+
error: "missing_scope",
|
|
134
|
+
needed: "channels:read",
|
|
135
|
+
provided: "identify,bot",
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const result = await client.apiRequest(
|
|
139
|
+
{ method: "GET", path: "/conversations.list" },
|
|
140
|
+
{ response: ChannelsSchema },
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
expect(result.ok).toBe(false);
|
|
144
|
+
if (!result.ok) {
|
|
145
|
+
expect(result.error).toBe("missing_scope");
|
|
146
|
+
expect(result.needed).toBe("channels:read");
|
|
147
|
+
expect(result.provided).toBe("identify,bot");
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("throws when ok: false but no error string (invalid Slack error)", async () => {
|
|
152
|
+
const { client } = createClient({ ok: false });
|
|
153
|
+
|
|
154
|
+
// { ok: false } without an error string doesn't match SlackErrorSchema,
|
|
155
|
+
// and also doesn't match the success schema → RestApiValidationError.
|
|
156
|
+
await expect(
|
|
157
|
+
client.apiRequest(
|
|
158
|
+
{ method: "GET", path: "/conversations.list" },
|
|
159
|
+
{ response: ChannelsSchema },
|
|
160
|
+
),
|
|
161
|
+
).rejects.toThrow(RestApiValidationError);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it.each(["not_authed", "invalid_auth", "ratelimited"])(
|
|
165
|
+
"handles %s error",
|
|
166
|
+
async (errorCode) => {
|
|
167
|
+
const { client } = createClient({ ok: false, error: errorCode });
|
|
168
|
+
|
|
169
|
+
const result = await client.apiRequest(
|
|
170
|
+
{ method: "GET", path: "/auth.test" },
|
|
171
|
+
{ response: z.object({}) },
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
expect(result.ok).toBe(false);
|
|
175
|
+
if (!result.ok) {
|
|
176
|
+
expect(result.error).toBe(errorCode);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
it("omits needed/provided when not present in Slack response", async () => {
|
|
182
|
+
const { client } = createClient({
|
|
183
|
+
ok: false,
|
|
184
|
+
error: "channel_not_found",
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const result = await client.apiRequest(
|
|
188
|
+
{ method: "POST", path: "/chat.postMessage" },
|
|
189
|
+
{ response: PostMessageSchema },
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
expect(result.ok).toBe(false);
|
|
193
|
+
if (!result.ok) {
|
|
194
|
+
expect(result).not.toHaveProperty("needed");
|
|
195
|
+
expect(result).not.toHaveProperty("provided");
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("throws when needed/provided are non-string (invalid Slack error)", async () => {
|
|
200
|
+
const { client } = createClient({
|
|
201
|
+
ok: false,
|
|
202
|
+
error: "missing_scope",
|
|
203
|
+
needed: 123,
|
|
204
|
+
provided: true,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Zod rejects non-string needed/provided. Since raw response has ok:false,
|
|
208
|
+
// SlackClient rejects it before success payload validation.
|
|
209
|
+
await expect(
|
|
210
|
+
client.apiRequest(
|
|
211
|
+
{ method: "GET", path: "/conversations.list" },
|
|
212
|
+
{ response: ChannelsSchema },
|
|
213
|
+
),
|
|
214
|
+
).rejects.toThrow(RestApiValidationError);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// ── Validation errors (still thrown) ───────────────────────────
|
|
219
|
+
|
|
220
|
+
describe("validation errors", () => {
|
|
221
|
+
it("throws RestApiValidationError when ok:true but schema does not match", async () => {
|
|
222
|
+
// Slack says ok:true but response is missing required fields
|
|
223
|
+
const { client } = createClient({
|
|
224
|
+
ok: true,
|
|
225
|
+
// Missing 'channels' field
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
await expect(
|
|
229
|
+
client.apiRequest(
|
|
230
|
+
{ method: "GET", path: "/conversations.list" },
|
|
231
|
+
{ response: ChannelsSchema },
|
|
232
|
+
),
|
|
233
|
+
).rejects.toThrow(RestApiValidationError);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("includes raw data and Zod error in validation error details", async () => {
|
|
237
|
+
const badResponse = { ok: true };
|
|
238
|
+
const { client } = createClient(badResponse);
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
await client.apiRequest(
|
|
242
|
+
{ method: "GET", path: "/conversations.list" },
|
|
243
|
+
{ response: ChannelsSchema },
|
|
244
|
+
);
|
|
245
|
+
expect.fail("Expected RestApiValidationError");
|
|
246
|
+
} catch (e) {
|
|
247
|
+
expect(e).toBeInstanceOf(RestApiValidationError);
|
|
248
|
+
const err = e as RestApiValidationError;
|
|
249
|
+
expect(err.details.data).toEqual(badResponse);
|
|
250
|
+
expect(err.details.zodError).toBeDefined();
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("throws RestApiValidationError when request body fails validation", async () => {
|
|
255
|
+
const { client } = createClient({ ok: true });
|
|
256
|
+
const BodySchema = z.object({
|
|
257
|
+
channel: z.string(),
|
|
258
|
+
text: z.string(),
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
await expect(
|
|
262
|
+
client.apiRequest(
|
|
263
|
+
{
|
|
264
|
+
method: "POST",
|
|
265
|
+
path: "/chat.postMessage",
|
|
266
|
+
body: { channel: 123 as unknown as string, text: "hello" },
|
|
267
|
+
},
|
|
268
|
+
{ body: BodySchema, response: PostMessageSchema },
|
|
269
|
+
),
|
|
270
|
+
).rejects.toThrow(RestApiValidationError);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// ── Request building ───────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
describe("request building", () => {
|
|
277
|
+
it("sends correct proto structure to executeQuery", async () => {
|
|
278
|
+
const { client, executeQuery } = createClient({
|
|
279
|
+
ok: true,
|
|
280
|
+
channel: "C123",
|
|
281
|
+
ts: "1234567890.123456",
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
await client.apiRequest(
|
|
285
|
+
{
|
|
286
|
+
method: "POST",
|
|
287
|
+
path: "/chat.postMessage",
|
|
288
|
+
body: { channel: "#general", text: "hello" },
|
|
289
|
+
headers: { "X-Custom": "value" },
|
|
290
|
+
params: { unfurl_links: false },
|
|
291
|
+
},
|
|
292
|
+
{ response: PostMessageSchema },
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
expect(executeQuery).toHaveBeenCalledOnce();
|
|
296
|
+
const request = executeQuery.mock.calls[0][0];
|
|
297
|
+
expect(request.openApiAction).toBe("genericHttpRequest");
|
|
298
|
+
expect(request.httpMethod).toBe("POST");
|
|
299
|
+
expect(request.urlPath).toBe("/chat.postMessage");
|
|
300
|
+
expect(request.responseType).toBe("json");
|
|
301
|
+
expect(request.body).toBe(
|
|
302
|
+
JSON.stringify({ channel: "#general", text: "hello" }),
|
|
303
|
+
);
|
|
304
|
+
expect(request.bodyType).toBe("jsonBody");
|
|
305
|
+
expect(request.headers).toEqual([{ key: "X-Custom", value: "value" }]);
|
|
306
|
+
expect(request.params).toEqual([{ key: "unfurl_links", value: "false" }]);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("passes trace metadata to executeQuery", async () => {
|
|
310
|
+
const { client, executeQuery } = createClient({
|
|
311
|
+
ok: true,
|
|
312
|
+
channel: "C1",
|
|
313
|
+
ts: "1.1",
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
await client.apiRequest(
|
|
317
|
+
{ method: "POST", path: "/chat.postMessage" },
|
|
318
|
+
{ response: PostMessageSchema },
|
|
319
|
+
{ label: "slack.postMessage", description: "Post a message" },
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
expect(executeQuery).toHaveBeenCalledWith(expect.any(Object), undefined, {
|
|
323
|
+
label: "slack.postMessage",
|
|
324
|
+
description: "Post a message",
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("propagates executeQuery rejections as transport errors", async () => {
|
|
329
|
+
const executeQuery = vi
|
|
330
|
+
.fn()
|
|
331
|
+
.mockRejectedValue(new Error("network timeout"));
|
|
332
|
+
const client = new SlackClientImpl(TEST_CONFIG, executeQuery);
|
|
333
|
+
|
|
334
|
+
await expect(
|
|
335
|
+
client.apiRequest(
|
|
336
|
+
{ method: "GET", path: "/auth.test" },
|
|
337
|
+
{ response: z.object({}) },
|
|
338
|
+
),
|
|
339
|
+
).rejects.toThrow("network timeout");
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// ── Edge cases ─────────────────────────────────────────────────
|
|
344
|
+
|
|
345
|
+
describe("edge cases", () => {
|
|
346
|
+
it("falls through to schema validation when response has no ok field", async () => {
|
|
347
|
+
const { client } = createClient({ data: [1, 2, 3] });
|
|
348
|
+
|
|
349
|
+
// No ok field → not Slack error, and missing success envelope.
|
|
350
|
+
await expect(
|
|
351
|
+
client.apiRequest(
|
|
352
|
+
{ method: "GET", path: "/some.endpoint" },
|
|
353
|
+
{ response: ChannelsSchema },
|
|
354
|
+
),
|
|
355
|
+
).rejects.toThrow(RestApiValidationError);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("falls through to schema validation when ok is not a boolean", async () => {
|
|
359
|
+
const { client } = createClient({ ok: "false", error: "nope" });
|
|
360
|
+
|
|
361
|
+
// ok is a string, so it is neither a valid Slack error envelope
|
|
362
|
+
// nor a valid success envelope.
|
|
363
|
+
await expect(
|
|
364
|
+
client.apiRequest(
|
|
365
|
+
{ method: "GET", path: "/some.endpoint" },
|
|
366
|
+
{ response: ChannelsSchema },
|
|
367
|
+
),
|
|
368
|
+
).rejects.toThrow(RestApiValidationError);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("handles response surviving JSON round-trip (protobuf simulation)", async () => {
|
|
372
|
+
const original = {
|
|
373
|
+
ok: true,
|
|
374
|
+
channels: [{ id: "C123", name: "general" }],
|
|
375
|
+
nested: { count: 42 },
|
|
376
|
+
};
|
|
377
|
+
const roundTripped = JSON.parse(JSON.stringify(original));
|
|
378
|
+
const { client } = createClient(roundTripped);
|
|
379
|
+
|
|
380
|
+
const result = await client.apiRequest(
|
|
381
|
+
{ method: "GET", path: "/conversations.list" },
|
|
382
|
+
{ response: ChannelsSchema },
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
expect(result.ok).toBe(true);
|
|
386
|
+
if (result.ok) {
|
|
387
|
+
expect(result.channels[0].id).toBe("C123");
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("injects ok: true when schema omits the ok field", async () => {
|
|
392
|
+
const NoOkSchema = z.object({
|
|
393
|
+
channels: z.array(z.object({ id: z.string(), name: z.string() })),
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
const { client } = createClient({
|
|
397
|
+
ok: true,
|
|
398
|
+
channels: [{ id: "C1", name: "test" }],
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
const result = await client.apiRequest(
|
|
402
|
+
{ method: "GET", path: "/conversations.list" },
|
|
403
|
+
{ response: NoOkSchema },
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
expect(result.ok).toBe(true);
|
|
407
|
+
if (result.ok) {
|
|
408
|
+
expect(result.channels).toHaveLength(1);
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("validates payload schema after removing envelope field for strict schemas", async () => {
|
|
413
|
+
const StrictSchema = z
|
|
414
|
+
.object({
|
|
415
|
+
channels: z.array(z.object({ id: z.string(), name: z.string() })),
|
|
416
|
+
})
|
|
417
|
+
.strict();
|
|
418
|
+
|
|
419
|
+
const { client } = createClient({
|
|
420
|
+
ok: true,
|
|
421
|
+
channels: [{ id: "C1", name: "test" }],
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
const result = await client.apiRequest(
|
|
425
|
+
{ method: "GET", path: "/conversations.list" },
|
|
426
|
+
{ response: StrictSchema },
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
expect(result.ok).toBe(true);
|
|
430
|
+
if (result.ok) {
|
|
431
|
+
expect(result.channels).toHaveLength(1);
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it("supports passthrough schemas and still enforces literal ok discriminant", async () => {
|
|
436
|
+
const PassthroughSchema = z
|
|
437
|
+
.object({
|
|
438
|
+
channels: z.array(z.object({ id: z.string(), name: z.string() })),
|
|
439
|
+
})
|
|
440
|
+
.passthrough();
|
|
441
|
+
|
|
442
|
+
const { client } = createClient({
|
|
443
|
+
ok: true,
|
|
444
|
+
channels: [{ id: "C1", name: "test" }],
|
|
445
|
+
response_metadata: { next_cursor: "abc" },
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
const result = await client.apiRequest(
|
|
449
|
+
{ method: "GET", path: "/conversations.list" },
|
|
450
|
+
{ response: PassthroughSchema },
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
expect(result.ok).toBe(true);
|
|
454
|
+
if (result.ok) {
|
|
455
|
+
expect(result.channels).toHaveLength(1);
|
|
456
|
+
expect(result).toHaveProperty("response_metadata");
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("throws when ok:false and loose payload schema would otherwise accept", async () => {
|
|
461
|
+
const LooseSchema = z.object({});
|
|
462
|
+
const { client } = createClient({ ok: false });
|
|
463
|
+
|
|
464
|
+
await expect(
|
|
465
|
+
client.apiRequest(
|
|
466
|
+
{ method: "GET", path: "/auth.test" },
|
|
467
|
+
{ response: LooseSchema },
|
|
468
|
+
),
|
|
469
|
+
).rejects.toThrow(RestApiValidationError);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it("routes to error branch when response matches both schemas", async () => {
|
|
473
|
+
// Response matches SlackErrorSchema AND a loose success schema.
|
|
474
|
+
// Error branch must win because it's checked first.
|
|
475
|
+
const FlexibleSchema = z.object({
|
|
476
|
+
error: z.string().optional(),
|
|
477
|
+
});
|
|
478
|
+
const { client } = createClient({
|
|
479
|
+
ok: false,
|
|
480
|
+
error: "channel_not_found",
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
const result = await client.apiRequest(
|
|
484
|
+
{ method: "POST", path: "/chat.postMessage" },
|
|
485
|
+
{ response: FlexibleSchema },
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
expect(result.ok).toBe(false);
|
|
489
|
+
if (!result.ok) {
|
|
490
|
+
expect(result.error).toBe("channel_not_found");
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it("does not treat ok: 0 as a Slack error (only strict false)", async () => {
|
|
495
|
+
// ok: 0 is not === false, so it is NOT detected as a Slack error.
|
|
496
|
+
// It fails success envelope validation because ok must be literal true.
|
|
497
|
+
const { client } = createClient({
|
|
498
|
+
ok: 0,
|
|
499
|
+
channels: [{ id: "C1", name: "test" }],
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
await expect(
|
|
503
|
+
client.apiRequest(
|
|
504
|
+
{ method: "GET", path: "/conversations.list" },
|
|
505
|
+
{ response: ChannelsSchema },
|
|
506
|
+
),
|
|
507
|
+
).rejects.toThrow(RestApiValidationError);
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// ── Type narrowing (compile-time verification) ─────────────────
|
|
512
|
+
|
|
513
|
+
describe("type narrowing", () => {
|
|
514
|
+
it("narrows to error response fields when ok is false", async () => {
|
|
515
|
+
const { client } = createClient({
|
|
516
|
+
ok: false,
|
|
517
|
+
error: "missing_scope",
|
|
518
|
+
needed: "channels:read",
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
const result = await client.apiRequest(
|
|
522
|
+
{ method: "GET", path: "/conversations.list" },
|
|
523
|
+
{ response: ChannelsSchema },
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
// This block exercises the SlackErrorResponse branch
|
|
527
|
+
if (!result.ok) {
|
|
528
|
+
const errorResult: SlackErrorResponse = result;
|
|
529
|
+
expect(errorResult.error).toBe("missing_scope");
|
|
530
|
+
expect(errorResult.needed).toBe("channels:read");
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it("narrows to success response fields when ok is true", async () => {
|
|
535
|
+
const { client } = createClient({
|
|
536
|
+
ok: true,
|
|
537
|
+
channels: [{ id: "C1", name: "general" }],
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
const result = await client.apiRequest(
|
|
541
|
+
{ method: "GET", path: "/conversations.list" },
|
|
542
|
+
{ response: ChannelsSchema },
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
// This block exercises the T & { ok: true } branch
|
|
546
|
+
if (result.ok) {
|
|
547
|
+
// TypeScript narrows: result.channels is accessible
|
|
548
|
+
expect(result.channels).toHaveLength(1);
|
|
549
|
+
expect(result.channels[0].name).toBe("general");
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
});
|
|
@@ -1,23 +1,103 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Slack client implementation.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Extends RestApiClientBase to inherit shared request infrastructure,
|
|
5
|
+
* then provides a Slack-specific apiRequest() that returns a
|
|
6
|
+
* SlackResponse<T> discriminated union instead of throwing on ok:false.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
|
-
import {
|
|
9
|
-
|
|
9
|
+
import type { z } from "zod";
|
|
10
|
+
|
|
11
|
+
import { RestApiValidationError } from "../../errors.js";
|
|
12
|
+
import { RestApiClientBase } from "../base/rest-api-client-base.js";
|
|
13
|
+
import type { ApiRequestOptions } from "../base/types.js";
|
|
14
|
+
import type { TraceMetadata } from "../registry.js";
|
|
15
|
+
import {
|
|
16
|
+
SlackErrorSchema,
|
|
17
|
+
SlackSuccessEnvelopeSchema,
|
|
18
|
+
type SlackApiRequestSchema,
|
|
19
|
+
type SlackClient,
|
|
20
|
+
type SlackResponse,
|
|
21
|
+
} from "./types.js";
|
|
10
22
|
|
|
11
23
|
/**
|
|
12
24
|
* Internal implementation of SlackClient.
|
|
13
25
|
*
|
|
14
|
-
* Extends
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* and API request execution.
|
|
26
|
+
* Extends RestApiClientBase for request building and execution.
|
|
27
|
+
* Uses {@link SlackErrorSchema} to parse Slack error responses via Zod,
|
|
28
|
+
* then falls back to the caller's schema for success responses.
|
|
18
29
|
*/
|
|
19
|
-
export class SlackClientImpl
|
|
20
|
-
extends
|
|
21
|
-
|
|
22
|
-
|
|
30
|
+
export class SlackClientImpl extends RestApiClientBase implements SlackClient {
|
|
31
|
+
async apiRequest<TBody, TResponseSchema extends z.AnyZodObject>(
|
|
32
|
+
options: ApiRequestOptions<TBody>,
|
|
33
|
+
schema: SlackApiRequestSchema<TBody, TResponseSchema>,
|
|
34
|
+
metadata?: TraceMetadata,
|
|
35
|
+
): Promise<SlackResponse<z.output<TResponseSchema>>> {
|
|
36
|
+
const result = await this.executeApiRequest(options, schema.body, metadata);
|
|
37
|
+
|
|
38
|
+
// Slack returns errors as HTTP 200 with { ok: false, error: "..." }.
|
|
39
|
+
// Let Zod parse the error shape — if it matches, return the error branch.
|
|
40
|
+
const errorResult = SlackErrorSchema.safeParse(result);
|
|
41
|
+
if (errorResult.success) {
|
|
42
|
+
return errorResult.data;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Guard: if the raw response has ok: false but didn't match SlackErrorSchema
|
|
46
|
+
// (e.g. missing error string), don't let it fall through to the success
|
|
47
|
+
// schema which might accept it and get stamped with ok: true.
|
|
48
|
+
if (
|
|
49
|
+
typeof result === "object" &&
|
|
50
|
+
result !== null &&
|
|
51
|
+
"ok" in result &&
|
|
52
|
+
(result as Record<string, unknown>).ok === false
|
|
53
|
+
) {
|
|
54
|
+
throw new RestApiValidationError(
|
|
55
|
+
`Slack returned ok: false but the response did not match the expected error shape: ${errorResult.error.message}`,
|
|
56
|
+
{
|
|
57
|
+
zodError: errorResult.error,
|
|
58
|
+
data: result,
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Success responses must carry the Slack envelope discriminant.
|
|
64
|
+
const successEnvelopeResult = SlackSuccessEnvelopeSchema.safeParse(result);
|
|
65
|
+
if (!successEnvelopeResult.success) {
|
|
66
|
+
throw new RestApiValidationError(
|
|
67
|
+
`Slack success response is missing the expected ok: true envelope: ${successEnvelopeResult.error.message}`,
|
|
68
|
+
{
|
|
69
|
+
zodError: successEnvelopeResult.error,
|
|
70
|
+
data: result,
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Validate caller-provided payload shape (without the `ok` field).
|
|
76
|
+
// This keeps strict payload schemas compatible with Slack's envelope.
|
|
77
|
+
const payloadCandidate: Record<string, unknown> = {
|
|
78
|
+
...(result as Record<string, unknown>),
|
|
79
|
+
};
|
|
80
|
+
delete payloadCandidate.ok;
|
|
81
|
+
|
|
82
|
+
const payloadResult = schema.response.safeParse(payloadCandidate);
|
|
83
|
+
if (!payloadResult.success) {
|
|
84
|
+
throw new RestApiValidationError(
|
|
85
|
+
`Response validation failed: ${payloadResult.error.message}`,
|
|
86
|
+
{
|
|
87
|
+
zodError: payloadResult.error,
|
|
88
|
+
data: result,
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Defense against `.passthrough()` schemas: strip any leaked `ok` from
|
|
94
|
+
// the parsed payload before re-injecting the literal discriminant.
|
|
95
|
+
const payloadWithoutOk: Record<string, unknown> = { ...payloadResult.data };
|
|
96
|
+
delete payloadWithoutOk.ok;
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
...payloadWithoutOk,
|
|
100
|
+
ok: true as const,
|
|
101
|
+
} as z.output<TResponseSchema> & { readonly ok: true };
|
|
102
|
+
}
|
|
23
103
|
}
|
|
@@ -2,5 +2,10 @@
|
|
|
2
2
|
* Slack integration module.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export type {
|
|
5
|
+
export type {
|
|
6
|
+
SlackClient,
|
|
7
|
+
SlackResponse,
|
|
8
|
+
SlackErrorResponse,
|
|
9
|
+
} from "./types.js";
|
|
10
|
+
export { SlackErrorSchema } from "./types.js";
|
|
6
11
|
export { SlackClientImpl } from "./client.js";
|