@zapier/zapier-sdk 0.33.0 → 0.33.2

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.
Files changed (128) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/index.cjs +2 -1
  3. package/dist/index.d.mts +9 -1
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.mjs +2 -1
  7. package/dist/plugins/registry/index.d.ts.map +1 -1
  8. package/dist/plugins/registry/index.js +1 -0
  9. package/dist/types/sdk.d.ts +8 -0
  10. package/dist/types/sdk.d.ts.map +1 -1
  11. package/package.json +2 -2
  12. package/dist/api/auth.test.d.ts +0 -2
  13. package/dist/api/auth.test.d.ts.map +0 -1
  14. package/dist/api/auth.test.js +0 -220
  15. package/dist/api/client.test.d.ts +0 -2
  16. package/dist/api/client.test.d.ts.map +0 -1
  17. package/dist/api/client.test.js +0 -611
  18. package/dist/api/debug.test.d.ts +0 -2
  19. package/dist/api/debug.test.d.ts.map +0 -1
  20. package/dist/api/debug.test.js +0 -59
  21. package/dist/api/polling.test.d.ts +0 -2
  22. package/dist/api/polling.test.d.ts.map +0 -1
  23. package/dist/api/polling.test.js +0 -360
  24. package/dist/auth.test.d.ts +0 -2
  25. package/dist/auth.test.d.ts.map +0 -1
  26. package/dist/auth.test.js +0 -480
  27. package/dist/plugins/eventEmission/builders.test.d.ts +0 -2
  28. package/dist/plugins/eventEmission/builders.test.d.ts.map +0 -1
  29. package/dist/plugins/eventEmission/builders.test.js +0 -138
  30. package/dist/plugins/eventEmission/index.test.d.ts +0 -5
  31. package/dist/plugins/eventEmission/index.test.d.ts.map +0 -1
  32. package/dist/plugins/eventEmission/index.test.js +0 -712
  33. package/dist/plugins/eventEmission/transport.test.d.ts +0 -5
  34. package/dist/plugins/eventEmission/transport.test.d.ts.map +0 -1
  35. package/dist/plugins/eventEmission/transport.test.js +0 -164
  36. package/dist/plugins/fetch/index.test.d.ts +0 -2
  37. package/dist/plugins/fetch/index.test.d.ts.map +0 -1
  38. package/dist/plugins/fetch/index.test.js +0 -428
  39. package/dist/plugins/findFirstConnection/index.test.d.ts +0 -2
  40. package/dist/plugins/findFirstConnection/index.test.d.ts.map +0 -1
  41. package/dist/plugins/findFirstConnection/index.test.js +0 -177
  42. package/dist/plugins/findUniqueConnection/index.test.d.ts +0 -2
  43. package/dist/plugins/findUniqueConnection/index.test.d.ts.map +0 -1
  44. package/dist/plugins/findUniqueConnection/index.test.js +0 -159
  45. package/dist/plugins/getAction/index.test.d.ts +0 -2
  46. package/dist/plugins/getAction/index.test.d.ts.map +0 -1
  47. package/dist/plugins/getAction/index.test.js +0 -211
  48. package/dist/plugins/getApp/index.test.d.ts +0 -2
  49. package/dist/plugins/getApp/index.test.d.ts.map +0 -1
  50. package/dist/plugins/getApp/index.test.js +0 -157
  51. package/dist/plugins/getConnection/index.test.d.ts +0 -2
  52. package/dist/plugins/getConnection/index.test.d.ts.map +0 -1
  53. package/dist/plugins/getConnection/index.test.js +0 -124
  54. package/dist/plugins/getInputFieldsSchema/index.test.d.ts +0 -2
  55. package/dist/plugins/getInputFieldsSchema/index.test.d.ts.map +0 -1
  56. package/dist/plugins/getInputFieldsSchema/index.test.js +0 -291
  57. package/dist/plugins/listActions/index.test.d.ts +0 -2
  58. package/dist/plugins/listActions/index.test.d.ts.map +0 -1
  59. package/dist/plugins/listActions/index.test.js +0 -454
  60. package/dist/plugins/listApps/index.test.d.ts +0 -2
  61. package/dist/plugins/listApps/index.test.d.ts.map +0 -1
  62. package/dist/plugins/listApps/index.test.js +0 -124
  63. package/dist/plugins/listConnections/index.test.d.ts +0 -2
  64. package/dist/plugins/listConnections/index.test.d.ts.map +0 -1
  65. package/dist/plugins/listConnections/index.test.js +0 -920
  66. package/dist/plugins/listInputFieldChoices/index.test.d.ts +0 -2
  67. package/dist/plugins/listInputFieldChoices/index.test.d.ts.map +0 -1
  68. package/dist/plugins/listInputFieldChoices/index.test.js +0 -717
  69. package/dist/plugins/listInputFields/index.test.d.ts +0 -2
  70. package/dist/plugins/listInputFields/index.test.d.ts.map +0 -1
  71. package/dist/plugins/listInputFields/index.test.js +0 -359
  72. package/dist/plugins/manifest/index.test.d.ts +0 -2
  73. package/dist/plugins/manifest/index.test.d.ts.map +0 -1
  74. package/dist/plugins/manifest/index.test.js +0 -1179
  75. package/dist/plugins/request/index.test.d.ts +0 -2
  76. package/dist/plugins/request/index.test.d.ts.map +0 -1
  77. package/dist/plugins/request/index.test.js +0 -458
  78. package/dist/plugins/runAction/index.test.d.ts +0 -2
  79. package/dist/plugins/runAction/index.test.d.ts.map +0 -1
  80. package/dist/plugins/runAction/index.test.js +0 -350
  81. package/dist/resolvers/connectionId.test.d.ts +0 -2
  82. package/dist/resolvers/connectionId.test.d.ts.map +0 -1
  83. package/dist/resolvers/connectionId.test.js +0 -61
  84. package/dist/sdk.test.d.ts +0 -2
  85. package/dist/sdk.test.d.ts.map +0 -1
  86. package/dist/sdk.test.js +0 -260
  87. package/dist/types/domain.test.d.ts +0 -2
  88. package/dist/types/domain.test.d.ts.map +0 -1
  89. package/dist/types/domain.test.js +0 -39
  90. package/dist/utils/array-utils.test.d.ts +0 -2
  91. package/dist/utils/array-utils.test.d.ts.map +0 -1
  92. package/dist/utils/array-utils.test.js +0 -107
  93. package/dist/utils/batch-utils.test.d.ts +0 -2
  94. package/dist/utils/batch-utils.test.d.ts.map +0 -1
  95. package/dist/utils/batch-utils.test.js +0 -476
  96. package/dist/utils/domain-utils.test.d.ts +0 -2
  97. package/dist/utils/domain-utils.test.d.ts.map +0 -1
  98. package/dist/utils/domain-utils.test.js +0 -346
  99. package/dist/utils/file-utils.test.d.ts +0 -2
  100. package/dist/utils/file-utils.test.d.ts.map +0 -1
  101. package/dist/utils/file-utils.test.js +0 -51
  102. package/dist/utils/function-utils.test.d.ts +0 -2
  103. package/dist/utils/function-utils.test.d.ts.map +0 -1
  104. package/dist/utils/function-utils.test.js +0 -188
  105. package/dist/utils/id-utils.test.d.ts +0 -2
  106. package/dist/utils/id-utils.test.d.ts.map +0 -1
  107. package/dist/utils/id-utils.test.js +0 -22
  108. package/dist/utils/pagination-utils.test.d.ts +0 -17
  109. package/dist/utils/pagination-utils.test.d.ts.map +0 -1
  110. package/dist/utils/pagination-utils.test.js +0 -461
  111. package/dist/utils/retry-utils.test.d.ts +0 -2
  112. package/dist/utils/retry-utils.test.d.ts.map +0 -1
  113. package/dist/utils/retry-utils.test.js +0 -90
  114. package/dist/utils/string-utils.test.d.ts +0 -2
  115. package/dist/utils/string-utils.test.d.ts.map +0 -1
  116. package/dist/utils/string-utils.test.js +0 -59
  117. package/dist/utils/telemetry-context.test.d.ts +0 -2
  118. package/dist/utils/telemetry-context.test.d.ts.map +0 -1
  119. package/dist/utils/telemetry-context.test.js +0 -154
  120. package/dist/utils/telemetry-utils.test.d.ts +0 -2
  121. package/dist/utils/telemetry-utils.test.d.ts.map +0 -1
  122. package/dist/utils/telemetry-utils.test.js +0 -155
  123. package/dist/utils/url-utils.test.d.ts +0 -2
  124. package/dist/utils/url-utils.test.d.ts.map +0 -1
  125. package/dist/utils/url-utils.test.js +0 -103
  126. package/dist/utils/validation.test.d.ts +0 -2
  127. package/dist/utils/validation.test.d.ts.map +0 -1
  128. package/dist/utils/validation.test.js +0 -44
@@ -1,611 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { createZapierApi } from "./client";
3
- import * as auth from "../auth";
4
- import { ZapierRateLimitError } from "../types/errors";
5
- vi.mock("../auth");
6
- describe("ApiClient", () => {
7
- const mockResolveAuthToken = vi.mocked(auth.resolveAuthToken);
8
- const mockInvalidateCredentialsToken = vi.mocked(auth.invalidateCredentialsToken);
9
- let mockFetch;
10
- beforeEach(() => {
11
- vi.clearAllMocks();
12
- // Prevent any actual HTTP calls
13
- mockFetch = vi.fn().mockResolvedValue({
14
- ok: true,
15
- status: 200,
16
- text: () => Promise.resolve(JSON.stringify({ data: "test" })),
17
- });
18
- global.fetch = mockFetch;
19
- });
20
- describe("authentication token resolution", () => {
21
- it("should pass credentials to resolveAuthToken when provided", async () => {
22
- mockResolveAuthToken.mockResolvedValue("test-token");
23
- const credentials = {
24
- type: "client_credentials",
25
- clientId: "my-client",
26
- clientSecret: "my-secret",
27
- };
28
- const client = createZapierApi({
29
- baseUrl: "https://api.custom.zapier.dev",
30
- credentials,
31
- debug: false,
32
- });
33
- // Make a request that would trigger token resolution
34
- await client.get("/test", { authRequired: true });
35
- expect(mockResolveAuthToken).toHaveBeenCalledWith({
36
- credentials,
37
- token: undefined,
38
- onEvent: undefined,
39
- fetch: expect.any(Function),
40
- baseUrl: "https://api.custom.zapier.dev",
41
- requiredScopes: undefined,
42
- debug: false,
43
- });
44
- });
45
- it("should pass undefined credentials when not provided", async () => {
46
- mockResolveAuthToken.mockResolvedValue("test-token");
47
- const client = createZapierApi({
48
- baseUrl: "https://api.custom.zapier.dev",
49
- debug: false,
50
- });
51
- // Make a request that would trigger token resolution
52
- await client.get("/test", { authRequired: true });
53
- expect(mockResolveAuthToken).toHaveBeenCalledWith({
54
- credentials: undefined,
55
- token: undefined,
56
- onEvent: undefined,
57
- fetch: expect.any(Function),
58
- baseUrl: "https://api.custom.zapier.dev",
59
- requiredScopes: undefined,
60
- debug: false,
61
- });
62
- });
63
- it("should pass deprecated token option to resolveAuthToken", async () => {
64
- mockResolveAuthToken.mockResolvedValue("direct-token");
65
- const client = createZapierApi({
66
- token: "direct-token",
67
- baseUrl: "https://api.custom.zapier.dev",
68
- debug: false,
69
- });
70
- // Make a request that would use the direct token
71
- await client.get("/test", { authRequired: true });
72
- // resolveAuthToken is still called, but it returns the direct token immediately
73
- expect(mockResolveAuthToken).toHaveBeenCalledWith({
74
- credentials: undefined,
75
- token: "direct-token",
76
- onEvent: undefined,
77
- fetch: expect.any(Function),
78
- baseUrl: "https://api.custom.zapier.dev",
79
- requiredScopes: undefined,
80
- debug: false,
81
- });
82
- });
83
- it("should pass string credentials to resolveAuthToken", async () => {
84
- mockResolveAuthToken.mockResolvedValue("string-token");
85
- const client = createZapierApi({
86
- credentials: "string-token",
87
- baseUrl: "https://api.custom.zapier.dev",
88
- debug: false,
89
- });
90
- await client.get("/test", { authRequired: true });
91
- expect(mockResolveAuthToken).toHaveBeenCalledWith({
92
- credentials: "string-token",
93
- token: undefined,
94
- onEvent: undefined,
95
- fetch: expect.any(Function),
96
- baseUrl: "https://api.custom.zapier.dev",
97
- requiredScopes: undefined,
98
- debug: false,
99
- });
100
- });
101
- it("should pass credentials function to resolveAuthToken", async () => {
102
- mockResolveAuthToken.mockResolvedValue("function-token");
103
- const credentialsFn = () => Promise.resolve("dynamic-token");
104
- const client = createZapierApi({
105
- credentials: credentialsFn,
106
- baseUrl: "https://api.custom.zapier.dev",
107
- debug: false,
108
- });
109
- await client.get("/test", { authRequired: true });
110
- expect(mockResolveAuthToken).toHaveBeenCalledWith({
111
- credentials: credentialsFn,
112
- token: undefined,
113
- onEvent: undefined,
114
- fetch: expect.any(Function),
115
- baseUrl: "https://api.custom.zapier.dev",
116
- requiredScopes: undefined,
117
- debug: false,
118
- });
119
- });
120
- });
121
- describe("401 cache invalidation", () => {
122
- it("should invalidate credentials token on 401 response", async () => {
123
- mockResolveAuthToken.mockResolvedValue("test-token");
124
- mockInvalidateCredentialsToken.mockResolvedValue(undefined);
125
- const clientCredentials = {
126
- type: "client_credentials",
127
- clientId: "my-client-id",
128
- clientSecret: "my-secret",
129
- };
130
- global.fetch = vi.fn().mockResolvedValue({
131
- ok: false,
132
- status: 401,
133
- statusText: "Unauthorized",
134
- text: () => Promise.resolve(JSON.stringify({ message: "Invalid token" })),
135
- });
136
- const client = createZapierApi({
137
- credentials: clientCredentials,
138
- baseUrl: "https://api.custom.zapier.dev",
139
- debug: false,
140
- });
141
- await expect(client.get("/test")).rejects.toThrow();
142
- expect(mockInvalidateCredentialsToken).toHaveBeenCalledWith({
143
- credentials: clientCredentials,
144
- token: undefined,
145
- });
146
- });
147
- it("should not invalidate cache on 403 response", async () => {
148
- mockResolveAuthToken.mockResolvedValue("test-token");
149
- global.fetch = vi.fn().mockResolvedValue({
150
- ok: false,
151
- status: 403,
152
- statusText: "Forbidden",
153
- text: () => Promise.resolve(JSON.stringify({ message: "Access denied" })),
154
- });
155
- const client = createZapierApi({
156
- credentials: "test-token",
157
- baseUrl: "https://api.custom.zapier.dev",
158
- debug: false,
159
- });
160
- await expect(client.get("/test")).rejects.toThrow();
161
- expect(mockInvalidateCredentialsToken).not.toHaveBeenCalled();
162
- });
163
- });
164
- describe("URL path configuration", () => {
165
- beforeEach(() => {
166
- mockResolveAuthToken.mockResolvedValue("test-token");
167
- });
168
- it("should apply pathPrefix for /relay path with Zapier base URL", async () => {
169
- const client = createZapierApi({
170
- baseUrl: "https://zapier.com",
171
- credentials: "test-token",
172
- debug: false,
173
- });
174
- await client.get("/relay/some/path");
175
- expect(mockFetch).toHaveBeenCalledWith("https://sdkapi.zapier.com/api/v0/sdk/relay/some/path", expect.any(Object));
176
- });
177
- it("should apply pathPrefix for /zapier path with Zapier base URL", async () => {
178
- const client = createZapierApi({
179
- baseUrl: "https://zapier.com",
180
- credentials: "test-token",
181
- debug: false,
182
- });
183
- await client.get("/zapier/apps");
184
- expect(mockFetch).toHaveBeenCalledWith("https://sdkapi.zapier.com/api/v0/sdk/zapier/apps", expect.any(Object));
185
- });
186
- it("should apply pathPrefix for /relay path with localhost base URL", async () => {
187
- const client = createZapierApi({
188
- baseUrl: "http://localhost:3000",
189
- credentials: "test-token",
190
- debug: false,
191
- });
192
- await client.get("/relay/some/path");
193
- expect(mockFetch).toHaveBeenCalledWith("http://localhost:3000/api/v0/sdk/relay/some/path", expect.any(Object));
194
- });
195
- it("should apply pathPrefix for /zapier path with localhost base URL", async () => {
196
- const client = createZapierApi({
197
- baseUrl: "http://localhost:3000",
198
- credentials: "test-token",
199
- debug: false,
200
- });
201
- await client.get("/zapier/apps");
202
- expect(mockFetch).toHaveBeenCalledWith("http://localhost:3000/api/v0/sdk/zapier/apps", expect.any(Object));
203
- });
204
- it("should not apply pathPrefix for non-configured paths", async () => {
205
- const client = createZapierApi({
206
- baseUrl: "http://localhost:3000",
207
- credentials: "test-token",
208
- debug: false,
209
- });
210
- await client.get("/other/path");
211
- expect(mockFetch).toHaveBeenCalledWith("http://localhost:3000/other/path", expect.any(Object));
212
- });
213
- it("should handle exact path match for pathPrefix", async () => {
214
- const client = createZapierApi({
215
- baseUrl: "http://localhost:3000",
216
- credentials: "test-token",
217
- debug: false,
218
- });
219
- await client.get("/relay");
220
- expect(mockFetch).toHaveBeenCalledWith("http://localhost:3000/api/v0/sdk/relay/", expect.any(Object));
221
- });
222
- it("should use sdkapi subdomain when Zapier base URL has trailing slash", async () => {
223
- const client = createZapierApi({
224
- baseUrl: "https://zapier-staging.com/",
225
- credentials: "test-token",
226
- debug: false,
227
- });
228
- await client.get("/zapier/api/v4/profile/");
229
- expect(mockFetch).toHaveBeenCalledWith("https://sdkapi.zapier-staging.com/api/v0/sdk/zapier/api/v4/profile/", expect.any(Object));
230
- });
231
- it("should preserve base URL path for localhost with path component", async () => {
232
- const client = createZapierApi({
233
- baseUrl: "http://localhost:3000/a/b/c",
234
- credentials: "test-token",
235
- debug: false,
236
- });
237
- await client.get("/relay/some/path");
238
- expect(mockFetch).toHaveBeenCalledWith("http://localhost:3000/a/b/c/api/v0/sdk/relay/some/path", expect.any(Object));
239
- });
240
- });
241
- describe("fetch body serialization", () => {
242
- beforeEach(() => {
243
- mockResolveAuthToken.mockResolvedValue("test-token");
244
- });
245
- it("should JSON.stringify plain object bodies", async () => {
246
- const client = createZapierApi({
247
- baseUrl: "http://localhost:3000",
248
- credentials: "test-token",
249
- debug: false,
250
- });
251
- const body = { channel: "C123", text: "hello" };
252
- await client.fetch("/relay/api.example.com/data", {
253
- method: "POST",
254
- body: body,
255
- });
256
- const [, options] = mockFetch.mock.calls[0];
257
- expect(options.body).toBe(JSON.stringify(body));
258
- });
259
- it("should JSON.stringify array bodies", async () => {
260
- const client = createZapierApi({
261
- baseUrl: "http://localhost:3000",
262
- credentials: "test-token",
263
- debug: false,
264
- });
265
- const body = [{ id: 1 }, { id: 2 }];
266
- await client.fetch("/relay/api.example.com/data", {
267
- method: "POST",
268
- body: body,
269
- });
270
- const [, options] = mockFetch.mock.calls[0];
271
- expect(options.body).toBe(JSON.stringify(body));
272
- });
273
- it("should not stringify FormData bodies", async () => {
274
- const client = createZapierApi({
275
- baseUrl: "http://localhost:3000",
276
- credentials: "test-token",
277
- debug: false,
278
- });
279
- const body = new FormData();
280
- body.append("file", "contents");
281
- await client.fetch("/relay/api.example.com/upload", {
282
- method: "POST",
283
- body,
284
- });
285
- const [, options] = mockFetch.mock.calls[0];
286
- expect(options.body).toBe(body);
287
- });
288
- it("should not stringify Blob bodies", async () => {
289
- const client = createZapierApi({
290
- baseUrl: "http://localhost:3000",
291
- credentials: "test-token",
292
- debug: false,
293
- });
294
- const body = new Blob(["binary data"], {
295
- type: "application/octet-stream",
296
- });
297
- await client.fetch("/relay/api.example.com/upload", {
298
- method: "POST",
299
- body,
300
- });
301
- const [, options] = mockFetch.mock.calls[0];
302
- expect(options.body).toBe(body);
303
- });
304
- it("should pass string bodies through unchanged", async () => {
305
- const client = createZapierApi({
306
- baseUrl: "http://localhost:3000",
307
- credentials: "test-token",
308
- debug: false,
309
- });
310
- const body = '{"already": "stringified"}';
311
- await client.fetch("/relay/api.example.com/data", {
312
- method: "POST",
313
- body,
314
- });
315
- const [, options] = mockFetch.mock.calls[0];
316
- expect(options.body).toBe(body);
317
- });
318
- });
319
- describe("rate limiting (429) retry behavior", () => {
320
- beforeEach(() => {
321
- mockResolveAuthToken.mockResolvedValue("test-token");
322
- vi.useFakeTimers();
323
- });
324
- afterEach(() => {
325
- vi.useRealTimers();
326
- });
327
- it("should retry on 429 and succeed on subsequent attempt", async () => {
328
- let callCount = 0;
329
- global.fetch = vi.fn().mockImplementation(() => {
330
- callCount++;
331
- if (callCount === 1) {
332
- return Promise.resolve({
333
- ok: false,
334
- status: 429,
335
- statusText: "Too Many Requests",
336
- headers: new Headers({ "retry-after": "1" }),
337
- text: () => Promise.resolve(JSON.stringify({ message: "Rate limited" })),
338
- });
339
- }
340
- return Promise.resolve({
341
- ok: true,
342
- status: 200,
343
- text: () => Promise.resolve(JSON.stringify({ data: "success" })),
344
- });
345
- });
346
- const client = createZapierApi({
347
- baseUrl: "http://localhost:3000",
348
- credentials: "test-token",
349
- debug: false,
350
- });
351
- const resultPromise = client.get("/test");
352
- // Fast-forward through the retry delay
353
- await vi.advanceTimersByTimeAsync(1000);
354
- const result = await resultPromise;
355
- expect(result).toEqual({ data: "success" });
356
- expect(callCount).toBe(2);
357
- });
358
- it("should throw ZapierRateLimitError after max retries exhausted", async () => {
359
- global.fetch = vi.fn().mockResolvedValue({
360
- ok: false,
361
- status: 429,
362
- statusText: "Too Many Requests",
363
- headers: new Headers({
364
- "retry-after": "1",
365
- "x-ratelimit-limit": "100",
366
- "x-ratelimit-remaining": "0",
367
- }),
368
- text: () => Promise.resolve(JSON.stringify({ message: "Rate limited" })),
369
- });
370
- const client = createZapierApi({
371
- baseUrl: "http://localhost:3000",
372
- credentials: "test-token",
373
- debug: false,
374
- maxNetworkRetries: 2,
375
- });
376
- const resultPromise = client.get("/test");
377
- // Attach error handler immediately to prevent unhandled rejection
378
- let caughtError;
379
- resultPromise.catch((err) => {
380
- caughtError = err;
381
- });
382
- // Advance through all retry delays
383
- await vi.advanceTimersByTimeAsync(1000); // 1st retry
384
- await vi.advanceTimersByTimeAsync(1000); // 2nd retry
385
- // Wait for promise to settle
386
- await vi.advanceTimersByTimeAsync(0);
387
- expect(caughtError).toBeInstanceOf(ZapierRateLimitError);
388
- expect(caughtError).toMatchObject({
389
- rateLimit: {
390
- retryAfterMs: 1000,
391
- limit: 100,
392
- remaining: 0,
393
- },
394
- });
395
- // Should have made 3 calls (initial + 2 retries)
396
- expect(global.fetch).toHaveBeenCalledTimes(3);
397
- });
398
- it("should use exponential backoff when no Retry-After header", async () => {
399
- let callCount = 0;
400
- global.fetch = vi.fn().mockImplementation(() => {
401
- callCount++;
402
- if (callCount <= 2) {
403
- return Promise.resolve({
404
- ok: false,
405
- status: 429,
406
- statusText: "Too Many Requests",
407
- headers: new Headers(),
408
- text: () => Promise.resolve(JSON.stringify({ message: "Rate limited" })),
409
- });
410
- }
411
- return Promise.resolve({
412
- ok: true,
413
- status: 200,
414
- text: () => Promise.resolve(JSON.stringify({ data: "success" })),
415
- });
416
- });
417
- const client = createZapierApi({
418
- baseUrl: "http://localhost:3000",
419
- credentials: "test-token",
420
- debug: false,
421
- });
422
- const resultPromise = client.get("/test");
423
- // First retry: ~1000ms base delay
424
- await vi.advanceTimersByTimeAsync(1500);
425
- // Second retry: ~2000ms base delay
426
- await vi.advanceTimersByTimeAsync(3000);
427
- const result = await resultPromise;
428
- expect(result).toEqual({ data: "success" });
429
- expect(callCount).toBe(3);
430
- });
431
- it("should respect X-RateLimit-Reset when no Retry-After header", async () => {
432
- const resetTime = Math.floor(Date.now() / 1000) + 2; // 2 seconds from now
433
- let callCount = 0;
434
- global.fetch = vi.fn().mockImplementation(() => {
435
- callCount++;
436
- if (callCount === 1) {
437
- return Promise.resolve({
438
- ok: false,
439
- status: 429,
440
- statusText: "Too Many Requests",
441
- headers: new Headers({
442
- "x-ratelimit-reset": String(resetTime),
443
- }),
444
- text: () => Promise.resolve(JSON.stringify({ message: "Rate limited" })),
445
- });
446
- }
447
- return Promise.resolve({
448
- ok: true,
449
- status: 200,
450
- text: () => Promise.resolve(JSON.stringify({ data: "success" })),
451
- });
452
- });
453
- const client = createZapierApi({
454
- baseUrl: "http://localhost:3000",
455
- credentials: "test-token",
456
- debug: false,
457
- });
458
- const resultPromise = client.get("/test");
459
- // Advance past the reset time
460
- await vi.advanceTimersByTimeAsync(2500);
461
- const result = await resultPromise;
462
- expect(result).toEqual({ data: "success" });
463
- expect(callCount).toBe(2);
464
- });
465
- it("should emit retry events via onEvent", async () => {
466
- const events = [];
467
- let callCount = 0;
468
- global.fetch = vi.fn().mockImplementation(() => {
469
- callCount++;
470
- if (callCount === 1) {
471
- return Promise.resolve({
472
- ok: false,
473
- status: 429,
474
- statusText: "Too Many Requests",
475
- headers: new Headers({ "retry-after": "1" }),
476
- text: () => Promise.resolve(JSON.stringify({ message: "Rate limited" })),
477
- });
478
- }
479
- return Promise.resolve({
480
- ok: true,
481
- status: 200,
482
- text: () => Promise.resolve(JSON.stringify({ data: "success" })),
483
- });
484
- });
485
- const client = createZapierApi({
486
- baseUrl: "http://localhost:3000",
487
- credentials: "test-token",
488
- debug: false,
489
- onEvent: (event) => events.push(event),
490
- });
491
- const resultPromise = client.get("/test");
492
- await vi.advanceTimersByTimeAsync(1000);
493
- await resultPromise;
494
- expect(events).toContainEqual(expect.objectContaining({
495
- type: "api:rate_limit_retry",
496
- payload: expect.objectContaining({
497
- retry: 1,
498
- maxNetworkRetries: 3,
499
- delayMs: 1000,
500
- }),
501
- }));
502
- });
503
- it("should disable retries when maxNetworkRetries is 0", async () => {
504
- global.fetch = vi.fn().mockResolvedValue({
505
- ok: false,
506
- status: 429,
507
- statusText: "Too Many Requests",
508
- headers: new Headers({ "retry-after": "1" }),
509
- text: () => Promise.resolve(JSON.stringify({ message: "Rate limited" })),
510
- });
511
- const client = createZapierApi({
512
- baseUrl: "http://localhost:3000",
513
- credentials: "test-token",
514
- debug: false,
515
- maxNetworkRetries: 0,
516
- });
517
- await expect(client.get("/test")).rejects.toThrow(ZapierRateLimitError);
518
- // Should only make 1 call (no retries)
519
- expect(global.fetch).toHaveBeenCalledTimes(1);
520
- });
521
- it("should also retry on raw fetch calls", async () => {
522
- let callCount = 0;
523
- global.fetch = vi.fn().mockImplementation(() => {
524
- callCount++;
525
- if (callCount === 1) {
526
- return Promise.resolve({
527
- ok: false,
528
- status: 429,
529
- statusText: "Too Many Requests",
530
- headers: new Headers({ "retry-after": "1" }),
531
- text: () => Promise.resolve("Rate limited"),
532
- });
533
- }
534
- return Promise.resolve({
535
- ok: true,
536
- status: 200,
537
- headers: new Headers(),
538
- text: () => Promise.resolve("success"),
539
- });
540
- });
541
- const client = createZapierApi({
542
- baseUrl: "http://localhost:3000",
543
- credentials: "test-token",
544
- debug: false,
545
- });
546
- const resultPromise = client.fetch("/test");
547
- await vi.advanceTimersByTimeAsync(1000);
548
- const response = await resultPromise;
549
- expect(response.status).toBe(200);
550
- expect(callCount).toBe(2);
551
- });
552
- it("should fail immediately when retry delay exceeds maxNetworkRetryDelayMs", async () => {
553
- global.fetch = vi.fn().mockResolvedValue({
554
- ok: false,
555
- status: 429,
556
- statusText: "Too Many Requests",
557
- headers: new Headers({ "retry-after": "60" }), // Server wants 60 seconds
558
- text: () => Promise.resolve(JSON.stringify({ message: "Rate limited" })),
559
- });
560
- const client = createZapierApi({
561
- baseUrl: "http://localhost:3000",
562
- credentials: "test-token",
563
- debug: false,
564
- maxNetworkRetryDelayMs: 5000, // But we only want to wait up to 5 seconds
565
- });
566
- try {
567
- await client.get("/test");
568
- expect.fail("Should have thrown ZapierRateLimitError");
569
- }
570
- catch (error) {
571
- expect(error).toBeInstanceOf(ZapierRateLimitError);
572
- // Verify retries is 0 (no retries happened), not maxNetworkRetries
573
- expect(error.retries).toBe(0);
574
- }
575
- // Should only make 1 call (no retries because delay exceeds max)
576
- expect(global.fetch).toHaveBeenCalledTimes(1);
577
- });
578
- it("should retry when delay is within maxNetworkRetryDelayMs", async () => {
579
- let callCount = 0;
580
- global.fetch = vi.fn().mockImplementation(() => {
581
- callCount++;
582
- if (callCount === 1) {
583
- return Promise.resolve({
584
- ok: false,
585
- status: 429,
586
- statusText: "Too Many Requests",
587
- headers: new Headers({ "retry-after": "1" }), // Server wants 1 second
588
- text: () => Promise.resolve("Rate limited"),
589
- });
590
- }
591
- return Promise.resolve({
592
- ok: true,
593
- status: 200,
594
- headers: new Headers(),
595
- text: () => Promise.resolve(JSON.stringify({ data: "success" })),
596
- });
597
- });
598
- const client = createZapierApi({
599
- baseUrl: "http://localhost:3000",
600
- credentials: "test-token",
601
- debug: false,
602
- maxNetworkRetryDelayMs: 5000, // 5 seconds is enough for 1 second delay
603
- });
604
- const resultPromise = client.get("/test");
605
- await vi.advanceTimersByTimeAsync(1000);
606
- const result = await resultPromise;
607
- expect(result).toEqual({ data: "success" });
608
- expect(callCount).toBe(2);
609
- });
610
- });
611
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=debug.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"debug.test.d.ts","sourceRoot":"","sources":["../../src/api/debug.test.ts"],"names":[],"mappings":""}
@@ -1,59 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { censorHeaders } from "./debug";
3
- describe("censorHeaders", () => {
4
- it("should return undefined for undefined headers", () => {
5
- expect(censorHeaders(undefined)).toBeUndefined();
6
- });
7
- it("should return headers unchanged for non-auth headers", () => {
8
- const headers = {
9
- "Content-Type": "application/json",
10
- "User-Agent": "test",
11
- };
12
- const result = censorHeaders(headers);
13
- // Headers API normalizes header names to lowercase
14
- expect(result["content-type"]).toBe("application/json");
15
- expect(result["user-agent"]).toBe("test");
16
- });
17
- it("should censor authorization header with Bearer prefix - long token", () => {
18
- const headers = { authorization: "Bearer abcdef1234567890xyz" };
19
- const result = censorHeaders(headers);
20
- expect(result.authorization).toBe("Bearer abcd...0xyz");
21
- });
22
- it("should censor authorization header with Bearer prefix - short token", () => {
23
- const headers = { authorization: "Bearer short123" };
24
- const result = censorHeaders(headers);
25
- expect(result.authorization).toBe("Bearer s...");
26
- });
27
- it("should censor x-api-key header - long token", () => {
28
- const headers = { "x-api-key": "sk-1234567890abcdefghij" };
29
- const result = censorHeaders(headers);
30
- expect(result["x-api-key"]).toBe("sk-1...ghij");
31
- });
32
- it("should censor x-api-key header - short token", () => {
33
- const headers = { "x-api-key": "short" };
34
- const result = censorHeaders(headers);
35
- expect(result["x-api-key"]).toBe("s...");
36
- });
37
- it("should handle Headers object input", () => {
38
- const headers = new Headers();
39
- headers.set("authorization", "Bearer verylongtoken123456789");
40
- const result = censorHeaders(headers);
41
- expect(result.authorization).toBe("Bearer very...6789");
42
- });
43
- it("should handle mixed case authorization headers", () => {
44
- const headers = { Authorization: "Bearer mixedcasetoken123" };
45
- const result = censorHeaders(headers);
46
- expect(result.authorization).toBe("Bearer mixe...n123");
47
- });
48
- it("should preserve non-auth headers while censoring auth headers", () => {
49
- const headers = {
50
- "Content-Type": "application/json",
51
- authorization: "Bearer shouldbecensored123456789",
52
- "User-Agent": "test-agent",
53
- };
54
- const result = censorHeaders(headers);
55
- expect(result["content-type"]).toBe("application/json");
56
- expect(result["user-agent"]).toBe("test-agent");
57
- expect(result.authorization).toBe("Bearer shou...6789");
58
- });
59
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=polling.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"polling.test.d.ts","sourceRoot":"","sources":["../../src/api/polling.test.ts"],"names":[],"mappings":""}