@webhooks-cc/sdk 0.5.0 → 1.0.0
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 +316 -140
- package/dist/chunk-7IMPSHQY.mjs +236 -0
- package/dist/diff-Dn4j4B_n.d.mts +701 -0
- package/dist/diff-Dn4j4B_n.d.ts +701 -0
- package/dist/index.d.mts +137 -324
- package/dist/index.d.ts +137 -324
- package/dist/index.js +1846 -163
- package/dist/index.mjs +1731 -286
- package/dist/testing.d.mts +30 -0
- package/dist/testing.d.ts +30 -0
- package/dist/testing.js +360 -0
- package/dist/testing.mjs +124 -0
- package/package.json +9 -4
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { g as DiffResult, e as CreateEndpointOptions, R as Request, a0 as WebhooksCC, E as Endpoint } from './diff-Dn4j4B_n.mjs';
|
|
2
|
+
|
|
3
|
+
type TestingClient = Pick<WebhooksCC, "endpoints" | "requests">;
|
|
4
|
+
interface AssertRequestExpectation {
|
|
5
|
+
method?: string;
|
|
6
|
+
path?: string;
|
|
7
|
+
headers?: Record<string, string>;
|
|
8
|
+
body?: string;
|
|
9
|
+
bodyJson?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
interface AssertRequestOptions {
|
|
12
|
+
ignoreHeaders?: string[];
|
|
13
|
+
throwOnFailure?: boolean;
|
|
14
|
+
}
|
|
15
|
+
interface AssertRequestResult {
|
|
16
|
+
pass: boolean;
|
|
17
|
+
diff: DiffResult;
|
|
18
|
+
}
|
|
19
|
+
interface CaptureDuringOptions extends CreateEndpointOptions {
|
|
20
|
+
timeout?: number | string;
|
|
21
|
+
pollInterval?: number | string;
|
|
22
|
+
count?: number;
|
|
23
|
+
match?: (request: Request) => boolean;
|
|
24
|
+
}
|
|
25
|
+
declare function withEndpoint<T>(client: TestingClient, callback: (endpoint: Endpoint) => Promise<T>, options?: CreateEndpointOptions): Promise<T>;
|
|
26
|
+
declare function withEphemeralEndpoint<T>(client: TestingClient, callback: (endpoint: Endpoint) => Promise<T>, options?: Omit<CreateEndpointOptions, "ephemeral">): Promise<T>;
|
|
27
|
+
declare function captureDuring(client: TestingClient, action: (endpoint: Endpoint) => Promise<unknown>, options?: CaptureDuringOptions): Promise<Request[]>;
|
|
28
|
+
declare function assertRequest(request: Request, expected: AssertRequestExpectation, options?: AssertRequestOptions): AssertRequestResult;
|
|
29
|
+
|
|
30
|
+
export { type AssertRequestExpectation, type AssertRequestOptions, type AssertRequestResult, type CaptureDuringOptions, assertRequest, captureDuring, withEndpoint, withEphemeralEndpoint };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { g as DiffResult, e as CreateEndpointOptions, R as Request, a0 as WebhooksCC, E as Endpoint } from './diff-Dn4j4B_n.js';
|
|
2
|
+
|
|
3
|
+
type TestingClient = Pick<WebhooksCC, "endpoints" | "requests">;
|
|
4
|
+
interface AssertRequestExpectation {
|
|
5
|
+
method?: string;
|
|
6
|
+
path?: string;
|
|
7
|
+
headers?: Record<string, string>;
|
|
8
|
+
body?: string;
|
|
9
|
+
bodyJson?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
interface AssertRequestOptions {
|
|
12
|
+
ignoreHeaders?: string[];
|
|
13
|
+
throwOnFailure?: boolean;
|
|
14
|
+
}
|
|
15
|
+
interface AssertRequestResult {
|
|
16
|
+
pass: boolean;
|
|
17
|
+
diff: DiffResult;
|
|
18
|
+
}
|
|
19
|
+
interface CaptureDuringOptions extends CreateEndpointOptions {
|
|
20
|
+
timeout?: number | string;
|
|
21
|
+
pollInterval?: number | string;
|
|
22
|
+
count?: number;
|
|
23
|
+
match?: (request: Request) => boolean;
|
|
24
|
+
}
|
|
25
|
+
declare function withEndpoint<T>(client: TestingClient, callback: (endpoint: Endpoint) => Promise<T>, options?: CreateEndpointOptions): Promise<T>;
|
|
26
|
+
declare function withEphemeralEndpoint<T>(client: TestingClient, callback: (endpoint: Endpoint) => Promise<T>, options?: Omit<CreateEndpointOptions, "ephemeral">): Promise<T>;
|
|
27
|
+
declare function captureDuring(client: TestingClient, action: (endpoint: Endpoint) => Promise<unknown>, options?: CaptureDuringOptions): Promise<Request[]>;
|
|
28
|
+
declare function assertRequest(request: Request, expected: AssertRequestExpectation, options?: AssertRequestOptions): AssertRequestResult;
|
|
29
|
+
|
|
30
|
+
export { type AssertRequestExpectation, type AssertRequestOptions, type AssertRequestResult, type CaptureDuringOptions, assertRequest, captureDuring, withEndpoint, withEphemeralEndpoint };
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/testing.ts
|
|
21
|
+
var testing_exports = {};
|
|
22
|
+
__export(testing_exports, {
|
|
23
|
+
assertRequest: () => assertRequest,
|
|
24
|
+
captureDuring: () => captureDuring,
|
|
25
|
+
withEndpoint: () => withEndpoint,
|
|
26
|
+
withEphemeralEndpoint: () => withEphemeralEndpoint
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(testing_exports);
|
|
29
|
+
|
|
30
|
+
// src/diff.ts
|
|
31
|
+
function isPlainObject(value) {
|
|
32
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
function isJsonBody(body) {
|
|
35
|
+
if (body.length === 0) {
|
|
36
|
+
return { valid: false };
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
return { valid: true, value: JSON.parse(body) };
|
|
40
|
+
} catch {
|
|
41
|
+
return { valid: false };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function areEqual(left, right) {
|
|
45
|
+
if (left === right) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
49
|
+
return left.length === right.length && left.every((value, index) => areEqual(value, right[index]));
|
|
50
|
+
}
|
|
51
|
+
if (isPlainObject(left) && isPlainObject(right)) {
|
|
52
|
+
const leftKeys = Object.keys(left);
|
|
53
|
+
const rightKeys = Object.keys(right);
|
|
54
|
+
return leftKeys.length === rightKeys.length && leftKeys.every((key) => areEqual(left[key], right[key]));
|
|
55
|
+
}
|
|
56
|
+
return Number.isNaN(left) && Number.isNaN(right);
|
|
57
|
+
}
|
|
58
|
+
function compareJsonValues(left, right, path, changes) {
|
|
59
|
+
if (areEqual(left, right)) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
63
|
+
const maxLength = Math.max(left.length, right.length);
|
|
64
|
+
for (let index = 0; index < maxLength; index++) {
|
|
65
|
+
const nextPath = path ? `${path}.${index}` : String(index);
|
|
66
|
+
compareJsonValues(left[index], right[index], nextPath, changes);
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (isPlainObject(left) && isPlainObject(right)) {
|
|
71
|
+
const keys = [.../* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)])].sort();
|
|
72
|
+
for (const key of keys) {
|
|
73
|
+
const nextPath = path ? `${path}.${key}` : key;
|
|
74
|
+
compareJsonValues(left[key], right[key], nextPath, changes);
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
changes[path || "$"] = { left, right };
|
|
79
|
+
}
|
|
80
|
+
function formatJsonDiff(changes) {
|
|
81
|
+
return Object.entries(changes).map(
|
|
82
|
+
([path, difference]) => `${path}: ${JSON.stringify(difference.left)} -> ${JSON.stringify(difference.right)}`
|
|
83
|
+
).join("\n");
|
|
84
|
+
}
|
|
85
|
+
function formatTextDiff(left, right) {
|
|
86
|
+
const leftLines = left.split("\n");
|
|
87
|
+
const rightLines = right.split("\n");
|
|
88
|
+
const maxLength = Math.max(leftLines.length, rightLines.length);
|
|
89
|
+
const lines = [];
|
|
90
|
+
for (let index = 0; index < maxLength; index++) {
|
|
91
|
+
const leftLine = leftLines[index];
|
|
92
|
+
const rightLine = rightLines[index];
|
|
93
|
+
if (leftLine === rightLine) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (leftLine !== void 0) {
|
|
97
|
+
lines.push(`- ${leftLine}`);
|
|
98
|
+
}
|
|
99
|
+
if (rightLine !== void 0) {
|
|
100
|
+
lines.push(`+ ${rightLine}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return lines.join("\n");
|
|
104
|
+
}
|
|
105
|
+
function normalizeHeaders(headers, ignoredHeaders) {
|
|
106
|
+
const normalized = {};
|
|
107
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
108
|
+
const lowerKey = key.toLowerCase();
|
|
109
|
+
if (!ignoredHeaders.has(lowerKey)) {
|
|
110
|
+
normalized[lowerKey] = value;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return normalized;
|
|
114
|
+
}
|
|
115
|
+
function diffHeaders(leftHeaders, rightHeaders, options) {
|
|
116
|
+
const ignoredHeaders = new Set(
|
|
117
|
+
(options.ignoreHeaders ?? []).map((header) => header.toLowerCase())
|
|
118
|
+
);
|
|
119
|
+
const left = normalizeHeaders(leftHeaders, ignoredHeaders);
|
|
120
|
+
const right = normalizeHeaders(rightHeaders, ignoredHeaders);
|
|
121
|
+
const leftKeys = new Set(Object.keys(left));
|
|
122
|
+
const rightKeys = new Set(Object.keys(right));
|
|
123
|
+
const added = [...rightKeys].filter((key) => !leftKeys.has(key)).sort();
|
|
124
|
+
const removed = [...leftKeys].filter((key) => !rightKeys.has(key)).sort();
|
|
125
|
+
const changed = {};
|
|
126
|
+
for (const key of [...leftKeys].filter((header) => rightKeys.has(header)).sort()) {
|
|
127
|
+
if (left[key] !== right[key]) {
|
|
128
|
+
changed[key] = { left: left[key], right: right[key] };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (added.length === 0 && removed.length === 0 && Object.keys(changed).length === 0) {
|
|
132
|
+
return void 0;
|
|
133
|
+
}
|
|
134
|
+
return { added, removed, changed };
|
|
135
|
+
}
|
|
136
|
+
function diffBodies(leftBody, rightBody) {
|
|
137
|
+
const left = leftBody ?? "";
|
|
138
|
+
const right = rightBody ?? "";
|
|
139
|
+
if (left === right) {
|
|
140
|
+
return void 0;
|
|
141
|
+
}
|
|
142
|
+
const leftJson = isJsonBody(left);
|
|
143
|
+
const rightJson = isJsonBody(right);
|
|
144
|
+
if (leftJson.valid && rightJson.valid) {
|
|
145
|
+
const changed = {};
|
|
146
|
+
compareJsonValues(leftJson.value, rightJson.value, "", changed);
|
|
147
|
+
if (Object.keys(changed).length === 0) {
|
|
148
|
+
return void 0;
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
type: "json",
|
|
152
|
+
changed,
|
|
153
|
+
diff: formatJsonDiff(changed)
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
type: "text",
|
|
158
|
+
diff: formatTextDiff(left, right)
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function diffRequests(left, right, options = {}) {
|
|
162
|
+
const differences = {};
|
|
163
|
+
if (left.method !== right.method) {
|
|
164
|
+
differences.method = { left: left.method, right: right.method };
|
|
165
|
+
}
|
|
166
|
+
if (left.path !== right.path) {
|
|
167
|
+
differences.path = { left: left.path, right: right.path };
|
|
168
|
+
}
|
|
169
|
+
const headerDiff = diffHeaders(left.headers, right.headers, options);
|
|
170
|
+
if (headerDiff) {
|
|
171
|
+
differences.headers = headerDiff;
|
|
172
|
+
}
|
|
173
|
+
const bodyDiff = diffBodies(left.body, right.body);
|
|
174
|
+
if (bodyDiff) {
|
|
175
|
+
differences.body = bodyDiff;
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
matches: Object.keys(differences).length === 0,
|
|
179
|
+
differences
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/errors.ts
|
|
184
|
+
var WebhooksCCError = class extends Error {
|
|
185
|
+
constructor(statusCode, message) {
|
|
186
|
+
super(message);
|
|
187
|
+
this.statusCode = statusCode;
|
|
188
|
+
this.name = "WebhooksCCError";
|
|
189
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
var NotFoundError = class extends WebhooksCCError {
|
|
193
|
+
constructor(message = "Resource not found") {
|
|
194
|
+
super(404, message);
|
|
195
|
+
this.name = "NotFoundError";
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
var TimeoutError = class extends WebhooksCCError {
|
|
199
|
+
constructor(timeoutMs) {
|
|
200
|
+
super(0, `Request timed out after ${timeoutMs}ms`);
|
|
201
|
+
this.name = "TimeoutError";
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// src/utils.ts
|
|
206
|
+
var DURATION_REGEX = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/;
|
|
207
|
+
function parseDuration(input) {
|
|
208
|
+
if (typeof input === "number") {
|
|
209
|
+
if (!Number.isFinite(input) || input < 0) {
|
|
210
|
+
throw new Error(`Invalid duration: must be a finite non-negative number, got ${input}`);
|
|
211
|
+
}
|
|
212
|
+
return input;
|
|
213
|
+
}
|
|
214
|
+
const trimmed = input.trim();
|
|
215
|
+
const asNumber = Number(trimmed);
|
|
216
|
+
if (!isNaN(asNumber) && trimmed.length > 0) {
|
|
217
|
+
if (!Number.isFinite(asNumber) || asNumber < 0) {
|
|
218
|
+
throw new Error(`Invalid duration: must be a finite non-negative number, got "${input}"`);
|
|
219
|
+
}
|
|
220
|
+
return asNumber;
|
|
221
|
+
}
|
|
222
|
+
const match = DURATION_REGEX.exec(trimmed);
|
|
223
|
+
if (!match) {
|
|
224
|
+
throw new Error(`Invalid duration: "${input}"`);
|
|
225
|
+
}
|
|
226
|
+
const value = parseFloat(match[1]);
|
|
227
|
+
switch (match[2]) {
|
|
228
|
+
case "ms":
|
|
229
|
+
return value;
|
|
230
|
+
case "s":
|
|
231
|
+
return value * 1e3;
|
|
232
|
+
case "m":
|
|
233
|
+
return value * 6e4;
|
|
234
|
+
case "h":
|
|
235
|
+
return value * 36e5;
|
|
236
|
+
case "d":
|
|
237
|
+
return value * 864e5;
|
|
238
|
+
default:
|
|
239
|
+
throw new Error(`Invalid duration: "${input}"`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/testing.ts
|
|
244
|
+
var MIN_CAPTURE_POLL_INTERVAL = 10;
|
|
245
|
+
var DEFAULT_CAPTURE_LIMIT = 100;
|
|
246
|
+
function isPlainObject2(value) {
|
|
247
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
248
|
+
}
|
|
249
|
+
function mergeExpectedJson(actual, expected) {
|
|
250
|
+
if (Array.isArray(expected)) {
|
|
251
|
+
if (!Array.isArray(actual)) {
|
|
252
|
+
return expected;
|
|
253
|
+
}
|
|
254
|
+
return expected.map((value, index) => mergeExpectedJson(actual[index], value));
|
|
255
|
+
}
|
|
256
|
+
if (isPlainObject2(expected)) {
|
|
257
|
+
const base = isPlainObject2(actual) ? { ...actual } : {};
|
|
258
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
259
|
+
base[key] = mergeExpectedJson(base[key], value);
|
|
260
|
+
}
|
|
261
|
+
return base;
|
|
262
|
+
}
|
|
263
|
+
return expected;
|
|
264
|
+
}
|
|
265
|
+
function parseRequestJsonBody(body) {
|
|
266
|
+
if (!body) {
|
|
267
|
+
return void 0;
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
return JSON.parse(body);
|
|
271
|
+
} catch {
|
|
272
|
+
return void 0;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function formatAssertionError(diff) {
|
|
276
|
+
return `Request assertion failed:
|
|
277
|
+
${JSON.stringify(diff.differences, null, 2)}`;
|
|
278
|
+
}
|
|
279
|
+
async function cleanupEndpoint(client, slug) {
|
|
280
|
+
try {
|
|
281
|
+
await client.endpoints.delete(slug);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
if (!(error instanceof NotFoundError)) {
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function sleep(ms) {
|
|
289
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
290
|
+
}
|
|
291
|
+
async function withEndpoint(client, callback, options = {}) {
|
|
292
|
+
const endpoint = await client.endpoints.create(options);
|
|
293
|
+
try {
|
|
294
|
+
return await callback(endpoint);
|
|
295
|
+
} finally {
|
|
296
|
+
await cleanupEndpoint(client, endpoint.slug);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
async function withEphemeralEndpoint(client, callback, options = {}) {
|
|
300
|
+
return withEndpoint(client, callback, {
|
|
301
|
+
...options,
|
|
302
|
+
ephemeral: true
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
async function captureDuring(client, action, options = {}) {
|
|
306
|
+
const { timeout = 3e4, pollInterval = 200, count = 1, match, ...createOptions } = options;
|
|
307
|
+
const timeoutMs = parseDuration(timeout);
|
|
308
|
+
const pollIntervalMs = Math.max(MIN_CAPTURE_POLL_INTERVAL, parseDuration(pollInterval));
|
|
309
|
+
const expectedCount = Math.max(1, Math.floor(count));
|
|
310
|
+
const requestLimit = Math.max(DEFAULT_CAPTURE_LIMIT, expectedCount * 5);
|
|
311
|
+
return withEndpoint(
|
|
312
|
+
client,
|
|
313
|
+
async (endpoint) => {
|
|
314
|
+
const startedAt = Date.now();
|
|
315
|
+
await action(endpoint);
|
|
316
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
317
|
+
const requests = await client.requests.list(endpoint.slug, { limit: requestLimit });
|
|
318
|
+
const matched = (match ? requests.filter(match) : requests).slice().sort((left, right) => left.receivedAt - right.receivedAt);
|
|
319
|
+
if (matched.length >= expectedCount) {
|
|
320
|
+
return matched.slice(0, expectedCount);
|
|
321
|
+
}
|
|
322
|
+
await sleep(pollIntervalMs);
|
|
323
|
+
}
|
|
324
|
+
throw new TimeoutError(timeoutMs);
|
|
325
|
+
},
|
|
326
|
+
createOptions
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
function assertRequest(request, expected, options = {}) {
|
|
330
|
+
if (expected.body !== void 0 && expected.bodyJson !== void 0) {
|
|
331
|
+
throw new Error("assertRequest accepts either body or bodyJson, not both");
|
|
332
|
+
}
|
|
333
|
+
const comparable = {
|
|
334
|
+
...request,
|
|
335
|
+
method: expected.method ?? request.method,
|
|
336
|
+
path: expected.path ?? request.path,
|
|
337
|
+
headers: expected.headers ? { ...request.headers, ...expected.headers } : request.headers
|
|
338
|
+
};
|
|
339
|
+
if (expected.body !== void 0) {
|
|
340
|
+
comparable.body = expected.body;
|
|
341
|
+
} else if (expected.bodyJson !== void 0) {
|
|
342
|
+
const actualJson = parseRequestJsonBody(request.body);
|
|
343
|
+
comparable.body = JSON.stringify(mergeExpectedJson(actualJson, expected.bodyJson));
|
|
344
|
+
}
|
|
345
|
+
const diff = diffRequests(request, comparable, {
|
|
346
|
+
ignoreHeaders: options.ignoreHeaders
|
|
347
|
+
});
|
|
348
|
+
const result = { pass: diff.matches, diff };
|
|
349
|
+
if (!result.pass && options.throwOnFailure) {
|
|
350
|
+
throw new Error(formatAssertionError(diff));
|
|
351
|
+
}
|
|
352
|
+
return result;
|
|
353
|
+
}
|
|
354
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
355
|
+
0 && (module.exports = {
|
|
356
|
+
assertRequest,
|
|
357
|
+
captureDuring,
|
|
358
|
+
withEndpoint,
|
|
359
|
+
withEphemeralEndpoint
|
|
360
|
+
});
|
package/dist/testing.mjs
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NotFoundError,
|
|
3
|
+
TimeoutError,
|
|
4
|
+
diffRequests,
|
|
5
|
+
parseDuration
|
|
6
|
+
} from "./chunk-7IMPSHQY.mjs";
|
|
7
|
+
|
|
8
|
+
// src/testing.ts
|
|
9
|
+
var MIN_CAPTURE_POLL_INTERVAL = 10;
|
|
10
|
+
var DEFAULT_CAPTURE_LIMIT = 100;
|
|
11
|
+
function isPlainObject(value) {
|
|
12
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
function mergeExpectedJson(actual, expected) {
|
|
15
|
+
if (Array.isArray(expected)) {
|
|
16
|
+
if (!Array.isArray(actual)) {
|
|
17
|
+
return expected;
|
|
18
|
+
}
|
|
19
|
+
return expected.map((value, index) => mergeExpectedJson(actual[index], value));
|
|
20
|
+
}
|
|
21
|
+
if (isPlainObject(expected)) {
|
|
22
|
+
const base = isPlainObject(actual) ? { ...actual } : {};
|
|
23
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
24
|
+
base[key] = mergeExpectedJson(base[key], value);
|
|
25
|
+
}
|
|
26
|
+
return base;
|
|
27
|
+
}
|
|
28
|
+
return expected;
|
|
29
|
+
}
|
|
30
|
+
function parseRequestJsonBody(body) {
|
|
31
|
+
if (!body) {
|
|
32
|
+
return void 0;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(body);
|
|
36
|
+
} catch {
|
|
37
|
+
return void 0;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function formatAssertionError(diff) {
|
|
41
|
+
return `Request assertion failed:
|
|
42
|
+
${JSON.stringify(diff.differences, null, 2)}`;
|
|
43
|
+
}
|
|
44
|
+
async function cleanupEndpoint(client, slug) {
|
|
45
|
+
try {
|
|
46
|
+
await client.endpoints.delete(slug);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (!(error instanceof NotFoundError)) {
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function sleep(ms) {
|
|
54
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
55
|
+
}
|
|
56
|
+
async function withEndpoint(client, callback, options = {}) {
|
|
57
|
+
const endpoint = await client.endpoints.create(options);
|
|
58
|
+
try {
|
|
59
|
+
return await callback(endpoint);
|
|
60
|
+
} finally {
|
|
61
|
+
await cleanupEndpoint(client, endpoint.slug);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function withEphemeralEndpoint(client, callback, options = {}) {
|
|
65
|
+
return withEndpoint(client, callback, {
|
|
66
|
+
...options,
|
|
67
|
+
ephemeral: true
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
async function captureDuring(client, action, options = {}) {
|
|
71
|
+
const { timeout = 3e4, pollInterval = 200, count = 1, match, ...createOptions } = options;
|
|
72
|
+
const timeoutMs = parseDuration(timeout);
|
|
73
|
+
const pollIntervalMs = Math.max(MIN_CAPTURE_POLL_INTERVAL, parseDuration(pollInterval));
|
|
74
|
+
const expectedCount = Math.max(1, Math.floor(count));
|
|
75
|
+
const requestLimit = Math.max(DEFAULT_CAPTURE_LIMIT, expectedCount * 5);
|
|
76
|
+
return withEndpoint(
|
|
77
|
+
client,
|
|
78
|
+
async (endpoint) => {
|
|
79
|
+
const startedAt = Date.now();
|
|
80
|
+
await action(endpoint);
|
|
81
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
82
|
+
const requests = await client.requests.list(endpoint.slug, { limit: requestLimit });
|
|
83
|
+
const matched = (match ? requests.filter(match) : requests).slice().sort((left, right) => left.receivedAt - right.receivedAt);
|
|
84
|
+
if (matched.length >= expectedCount) {
|
|
85
|
+
return matched.slice(0, expectedCount);
|
|
86
|
+
}
|
|
87
|
+
await sleep(pollIntervalMs);
|
|
88
|
+
}
|
|
89
|
+
throw new TimeoutError(timeoutMs);
|
|
90
|
+
},
|
|
91
|
+
createOptions
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
function assertRequest(request, expected, options = {}) {
|
|
95
|
+
if (expected.body !== void 0 && expected.bodyJson !== void 0) {
|
|
96
|
+
throw new Error("assertRequest accepts either body or bodyJson, not both");
|
|
97
|
+
}
|
|
98
|
+
const comparable = {
|
|
99
|
+
...request,
|
|
100
|
+
method: expected.method ?? request.method,
|
|
101
|
+
path: expected.path ?? request.path,
|
|
102
|
+
headers: expected.headers ? { ...request.headers, ...expected.headers } : request.headers
|
|
103
|
+
};
|
|
104
|
+
if (expected.body !== void 0) {
|
|
105
|
+
comparable.body = expected.body;
|
|
106
|
+
} else if (expected.bodyJson !== void 0) {
|
|
107
|
+
const actualJson = parseRequestJsonBody(request.body);
|
|
108
|
+
comparable.body = JSON.stringify(mergeExpectedJson(actualJson, expected.bodyJson));
|
|
109
|
+
}
|
|
110
|
+
const diff = diffRequests(request, comparable, {
|
|
111
|
+
ignoreHeaders: options.ignoreHeaders
|
|
112
|
+
});
|
|
113
|
+
const result = { pass: diff.matches, diff };
|
|
114
|
+
if (!result.pass && options.throwOnFailure) {
|
|
115
|
+
throw new Error(formatAssertionError(diff));
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
export {
|
|
120
|
+
assertRequest,
|
|
121
|
+
captureDuring,
|
|
122
|
+
withEndpoint,
|
|
123
|
+
withEphemeralEndpoint
|
|
124
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webhooks-cc/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "TypeScript SDK for webhooks.cc — create endpoints, capture requests, assert in tests",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
11
|
"import": "./dist/index.mjs",
|
|
12
12
|
"require": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./testing": {
|
|
15
|
+
"types": "./dist/testing.d.ts",
|
|
16
|
+
"import": "./dist/testing.mjs",
|
|
17
|
+
"require": "./dist/testing.js"
|
|
13
18
|
}
|
|
14
19
|
},
|
|
15
20
|
"files": [
|
|
@@ -34,11 +39,11 @@
|
|
|
34
39
|
"devDependencies": {
|
|
35
40
|
"tsup": "^8.5.1",
|
|
36
41
|
"typescript": "^5.9.3",
|
|
37
|
-
"vitest": "^3.
|
|
42
|
+
"vitest": "^3.2.4"
|
|
38
43
|
},
|
|
39
44
|
"scripts": {
|
|
40
|
-
"build": "tsup
|
|
41
|
-
"dev": "tsup
|
|
45
|
+
"build": "tsup --config tsup.config.ts",
|
|
46
|
+
"dev": "tsup --config tsup.config.ts --watch",
|
|
42
47
|
"typecheck": "tsc --noEmit",
|
|
43
48
|
"test": "vitest run",
|
|
44
49
|
"test:watch": "vitest"
|