@kombo-api/sdk 0.2.0 → 0.2.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/esm/funcs/generalSendPassthroughRequest.d.ts +1 -0
- package/esm/funcs/generalSendPassthroughRequest.d.ts.map +1 -1
- package/esm/funcs/generalSendPassthroughRequest.js +1 -0
- package/esm/funcs/generalSendPassthroughRequest.js.map +1 -1
- package/esm/lib/config.d.ts +3 -3
- package/esm/lib/config.js +3 -3
- package/esm/models/deletehrisabsencesabsenceidpositiveresponse.d.ts.map +1 -1
- package/esm/models/deletehrisabsencesabsenceidpositiveresponse.js +2 -6
- package/esm/models/deletehrisabsencesabsenceidpositiveresponse.js.map +1 -1
- package/esm/models/getatsjobspositiveresponse.d.ts.map +1 -1
- package/esm/models/getatsjobspositiveresponse.js +6 -26
- package/esm/models/getatsjobspositiveresponse.js.map +1 -1
- package/esm/models/gethrisabsencespositiveresponse.d.ts.map +1 -1
- package/esm/models/gethrisabsencespositiveresponse.js +2 -6
- package/esm/models/gethrisabsencespositiveresponse.js.map +1 -1
- package/esm/models/gethrisemployeespositiveresponse.d.ts +4 -0
- package/esm/models/gethrisemployeespositiveresponse.d.ts.map +1 -1
- package/esm/models/gethrisemployeespositiveresponse.js +11 -46
- package/esm/models/gethrisemployeespositiveresponse.js.map +1 -1
- package/esm/models/gethrisemploymentspositiveresponse.d.ts.map +1 -1
- package/esm/models/gethrisemploymentspositiveresponse.js +4 -16
- package/esm/models/gethrisemploymentspositiveresponse.js.map +1 -1
- package/esm/models/postconnectcreatelinkrequestbody.d.ts +2 -0
- package/esm/models/postconnectcreatelinkrequestbody.d.ts.map +1 -1
- package/esm/models/postconnectcreatelinkrequestbody.js +2 -0
- package/esm/models/postconnectcreatelinkrequestbody.js.map +1 -1
- package/esm/models/posthrisabsencespositiveresponse.d.ts.map +1 -1
- package/esm/models/posthrisabsencespositiveresponse.js +2 -6
- package/esm/models/posthrisabsencespositiveresponse.js.map +1 -1
- package/esm/sdk/general.d.ts +1 -0
- package/esm/sdk/general.d.ts.map +1 -1
- package/esm/sdk/general.js +1 -0
- package/esm/sdk/general.js.map +1 -1
- package/esm/types/enums.d.ts +10 -6
- package/esm/types/enums.d.ts.map +1 -1
- package/esm/types/enums.js +38 -1
- package/esm/types/enums.js.map +1 -1
- package/esm/types/index.d.ts +0 -1
- package/esm/types/index.d.ts.map +1 -1
- package/esm/types/index.js +0 -1
- package/esm/types/index.js.map +1 -1
- package/examples/package-lock.json +4 -2
- package/jsr.json +1 -1
- package/package.json +6 -3
- package/src/funcs/generalSendPassthroughRequest.ts +1 -0
- package/src/lib/config.ts +3 -3
- package/src/models/deletehrisabsencesabsenceidpositiveresponse.ts +5 -6
- package/src/models/getatsjobspositiveresponse.ts +7 -26
- package/src/models/gethrisabsencespositiveresponse.ts +3 -6
- package/src/models/gethrisemployeespositiveresponse.ts +16 -46
- package/src/models/gethrisemploymentspositiveresponse.ts +5 -16
- package/src/models/postconnectcreatelinkrequestbody.ts +2 -0
- package/src/models/posthrisabsencespositiveresponse.ts +3 -6
- package/src/sdk/general.ts +1 -0
- package/src/types/enums.ts +55 -6
- package/src/types/index.ts +0 -1
- package/tests/basic-behavior.spec.ts +162 -0
- package/tests/error-handling.spec.ts +604 -0
- package/tests/helpers/test-context.ts +147 -0
- package/tests/job-board.spec.ts +32 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
import { it, expect, expectTypeOf, describe } from "vitest";
|
|
2
|
+
import { TestContext, describeSdkSuite } from "./helpers/test-context";
|
|
3
|
+
import {
|
|
4
|
+
KomboAtsError,
|
|
5
|
+
KomboHrisError,
|
|
6
|
+
KomboGeneralError,
|
|
7
|
+
KomboDefaultError,
|
|
8
|
+
ResponseValidationError,
|
|
9
|
+
} from "../src/models/errors";
|
|
10
|
+
import { RequestTimeoutError } from "../src/models/errors/httpclienterrors";
|
|
11
|
+
import {
|
|
12
|
+
KomboAtsErrorCode,
|
|
13
|
+
KomboHrisErrorCode,
|
|
14
|
+
KomboGeneralErrorCode,
|
|
15
|
+
} from "../src/models";
|
|
16
|
+
|
|
17
|
+
// Test that the SDK properly throws structured KomboAtsError errors
|
|
18
|
+
// when the API returns error responses with the correct format.
|
|
19
|
+
|
|
20
|
+
describeSdkSuite("Error Handling", () => {
|
|
21
|
+
describe("ATS endpoints", () => {
|
|
22
|
+
it("returns KomboAtsError for platform rate limit errors", async () => {
|
|
23
|
+
const ctx = new TestContext();
|
|
24
|
+
|
|
25
|
+
ctx.mockEndpoint({
|
|
26
|
+
method: "GET",
|
|
27
|
+
path: "/v1/ats/jobs",
|
|
28
|
+
response: {
|
|
29
|
+
statusCode: 429,
|
|
30
|
+
body: {
|
|
31
|
+
status: "error",
|
|
32
|
+
error: {
|
|
33
|
+
code: "PLATFORM.RATE_LIMIT_EXCEEDED",
|
|
34
|
+
title: "Rate limit exceeded",
|
|
35
|
+
message:
|
|
36
|
+
"You have exceeded the rate limit. Please try again later.",
|
|
37
|
+
log_url: "https://app.kombo.dev/logs/abc123",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
for await (const _page of await ctx.kombo.ats.getJobs({})) {
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
expect.fail("Expected error to be thrown");
|
|
48
|
+
} catch (error) {
|
|
49
|
+
expect(error).toMatchInlineSnapshot(
|
|
50
|
+
`[KomboAtsError: You have exceeded the rate limit. Please try again later.]`
|
|
51
|
+
);
|
|
52
|
+
if (!(error instanceof KomboAtsError)) {
|
|
53
|
+
expect.fail("Expected error to be a KomboAtsError");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
expect(error.data$).toMatchObject({
|
|
57
|
+
error: {
|
|
58
|
+
code: "PLATFORM.RATE_LIMIT_EXCEEDED",
|
|
59
|
+
title: "Rate limit exceeded",
|
|
60
|
+
message:
|
|
61
|
+
"You have exceeded the rate limit. Please try again later.",
|
|
62
|
+
log_url: "https://app.kombo.dev/logs/abc123",
|
|
63
|
+
},
|
|
64
|
+
status: "error",
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expectTypeOf(error.data$).toMatchObjectType<{
|
|
68
|
+
status: "error";
|
|
69
|
+
error: {
|
|
70
|
+
code: KomboAtsErrorCode | null;
|
|
71
|
+
title: string | null;
|
|
72
|
+
message: string;
|
|
73
|
+
log_url: string | null;
|
|
74
|
+
};
|
|
75
|
+
}>();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("returns KomboAtsError for ATS-specific job closed errors", async () => {
|
|
80
|
+
const ctx = new TestContext();
|
|
81
|
+
|
|
82
|
+
ctx.mockEndpoint({
|
|
83
|
+
method: "POST",
|
|
84
|
+
path: "/v1/ats/jobs/test-job-id/applications",
|
|
85
|
+
response: {
|
|
86
|
+
statusCode: 400,
|
|
87
|
+
body: {
|
|
88
|
+
status: "error",
|
|
89
|
+
error: {
|
|
90
|
+
code: "ATS.JOB_CLOSED",
|
|
91
|
+
title: "Job is closed",
|
|
92
|
+
message:
|
|
93
|
+
"Cannot create application for a closed job. The job must be in an open state.",
|
|
94
|
+
log_url: "https://app.kombo.dev/logs/ghi789",
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
await ctx.kombo.ats.createApplication({
|
|
102
|
+
job_id: "test-job-id",
|
|
103
|
+
body: {
|
|
104
|
+
candidate: {
|
|
105
|
+
first_name: "John",
|
|
106
|
+
last_name: "Doe",
|
|
107
|
+
email_address: "john.doe@example.com",
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
expect.fail("Expected error to be thrown");
|
|
112
|
+
} catch (error) {
|
|
113
|
+
expect(error).toMatchInlineSnapshot(
|
|
114
|
+
`[KomboAtsError: Cannot create application for a closed job. The job must be in an open state.]`
|
|
115
|
+
);
|
|
116
|
+
if (!(error instanceof KomboAtsError)) {
|
|
117
|
+
expect.fail("Expected error to be an KomboAtsError");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
expect(error.data$).toMatchObject({
|
|
121
|
+
error: {
|
|
122
|
+
code: "ATS.JOB_CLOSED",
|
|
123
|
+
title: "Job is closed",
|
|
124
|
+
message:
|
|
125
|
+
"Cannot create application for a closed job. The job must be in an open state.",
|
|
126
|
+
log_url: "https://app.kombo.dev/logs/ghi789",
|
|
127
|
+
},
|
|
128
|
+
status: "error",
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expectTypeOf(error.data$).toMatchObjectType<{
|
|
132
|
+
status: "error";
|
|
133
|
+
error: {
|
|
134
|
+
code: KomboAtsErrorCode | null;
|
|
135
|
+
title: string | null;
|
|
136
|
+
message: string;
|
|
137
|
+
log_url: string | null;
|
|
138
|
+
};
|
|
139
|
+
}>();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("HRIS endpoints", () => {
|
|
145
|
+
it("returns KomboHrisError for integration permission errors", async () => {
|
|
146
|
+
const ctx = new TestContext();
|
|
147
|
+
|
|
148
|
+
ctx.mockEndpoint({
|
|
149
|
+
method: "GET",
|
|
150
|
+
path: "/v1/hris/employees",
|
|
151
|
+
response: {
|
|
152
|
+
statusCode: 403,
|
|
153
|
+
body: {
|
|
154
|
+
status: "error",
|
|
155
|
+
error: {
|
|
156
|
+
code: "INTEGRATION.PERMISSION_MISSING",
|
|
157
|
+
title: "Permission missing",
|
|
158
|
+
message:
|
|
159
|
+
"The integration is missing required permissions to access this resource.",
|
|
160
|
+
log_url: "https://app.kombo.dev/logs/hris-def456",
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
for await (const _page of await ctx.kombo.hris.getEmployees({})) {
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
expect.fail("Expected error to be thrown");
|
|
171
|
+
} catch (error) {
|
|
172
|
+
expect(error).toMatchInlineSnapshot(
|
|
173
|
+
`[KomboHrisError: The integration is missing required permissions to access this resource.]`
|
|
174
|
+
);
|
|
175
|
+
if (!(error instanceof KomboHrisError)) {
|
|
176
|
+
expect.fail("Expected error to be an KomboHrisError");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
expect(error.data$).toMatchObject({
|
|
180
|
+
error: {
|
|
181
|
+
code: "INTEGRATION.PERMISSION_MISSING",
|
|
182
|
+
title: "Permission missing",
|
|
183
|
+
message:
|
|
184
|
+
"The integration is missing required permissions to access this resource.",
|
|
185
|
+
log_url: "https://app.kombo.dev/logs/hris-def456",
|
|
186
|
+
},
|
|
187
|
+
status: "error",
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
expectTypeOf(error.data$).toMatchObjectType<{
|
|
191
|
+
status: "error";
|
|
192
|
+
error: {
|
|
193
|
+
code: KomboHrisErrorCode | null;
|
|
194
|
+
title: string | null;
|
|
195
|
+
message: string;
|
|
196
|
+
log_url: string | null;
|
|
197
|
+
};
|
|
198
|
+
}>();
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe("Assessment endpoints", () => {
|
|
204
|
+
it("returns KomboAtsError for platform input validation errors", async () => {
|
|
205
|
+
const ctx = new TestContext();
|
|
206
|
+
|
|
207
|
+
ctx.mockEndpoint({
|
|
208
|
+
method: "GET",
|
|
209
|
+
path: "/v1/assessment/orders/open",
|
|
210
|
+
response: {
|
|
211
|
+
statusCode: 400,
|
|
212
|
+
body: {
|
|
213
|
+
status: "error",
|
|
214
|
+
error: {
|
|
215
|
+
code: "PLATFORM.INPUT_INVALID",
|
|
216
|
+
title: "Input invalid",
|
|
217
|
+
message: "The provided input is invalid or malformed.",
|
|
218
|
+
log_url: "https://app.kombo.dev/logs/assessment-xyz",
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
for await (const _page of await ctx.kombo.assessment.getOpenOrders(
|
|
226
|
+
{}
|
|
227
|
+
)) {
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
expect.fail("Expected error to be thrown");
|
|
231
|
+
} catch (error) {
|
|
232
|
+
// Assessment uses KomboAtsError for errors
|
|
233
|
+
expect(error).toMatchInlineSnapshot(
|
|
234
|
+
`[KomboAtsError: The provided input is invalid or malformed.]`
|
|
235
|
+
);
|
|
236
|
+
if (!(error instanceof KomboAtsError)) {
|
|
237
|
+
expect.fail("Expected error to be an KomboAtsError");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
expect(error.data$).toMatchObject({
|
|
241
|
+
error: {
|
|
242
|
+
code: "PLATFORM.INPUT_INVALID",
|
|
243
|
+
title: "Input invalid",
|
|
244
|
+
message: "The provided input is invalid or malformed.",
|
|
245
|
+
log_url: "https://app.kombo.dev/logs/assessment-xyz",
|
|
246
|
+
},
|
|
247
|
+
status: "error",
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
expectTypeOf(error.data$).toMatchObjectType<{
|
|
251
|
+
status: "error";
|
|
252
|
+
error: {
|
|
253
|
+
code: KomboAtsErrorCode | null;
|
|
254
|
+
title: string | null;
|
|
255
|
+
message: string;
|
|
256
|
+
log_url: string | null;
|
|
257
|
+
};
|
|
258
|
+
}>();
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe("General endpoints", () => {
|
|
264
|
+
it("returns KomboGeneralError for authentication errors", async () => {
|
|
265
|
+
const ctx = new TestContext();
|
|
266
|
+
|
|
267
|
+
ctx.mockEndpoint({
|
|
268
|
+
method: "GET",
|
|
269
|
+
path: "/v1/check-api-key",
|
|
270
|
+
response: {
|
|
271
|
+
statusCode: 401,
|
|
272
|
+
body: {
|
|
273
|
+
status: "error",
|
|
274
|
+
error: {
|
|
275
|
+
code: "PLATFORM.AUTHENTICATION_INVALID",
|
|
276
|
+
title: "Authentication invalid",
|
|
277
|
+
message: "The provided API key is invalid or expired.",
|
|
278
|
+
log_url: "https://app.kombo.dev/logs/general-auth-123",
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
await ctx.kombo.general.checkApiKey();
|
|
286
|
+
expect.fail("Expected error to be thrown");
|
|
287
|
+
} catch (error) {
|
|
288
|
+
// General endpoints use KomboGeneralError for errors
|
|
289
|
+
expect(error).toMatchInlineSnapshot(
|
|
290
|
+
`[KomboGeneralError: The provided API key is invalid or expired.]`
|
|
291
|
+
);
|
|
292
|
+
if (!(error instanceof KomboGeneralError)) {
|
|
293
|
+
expect.fail("Expected error to be an KomboGeneralError");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
expect(error.data$).toMatchObject({
|
|
297
|
+
error: {
|
|
298
|
+
code: "PLATFORM.AUTHENTICATION_INVALID",
|
|
299
|
+
title: "Authentication invalid",
|
|
300
|
+
message: "The provided API key is invalid or expired.",
|
|
301
|
+
log_url: "https://app.kombo.dev/logs/general-auth-123",
|
|
302
|
+
},
|
|
303
|
+
status: "error",
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
expectTypeOf(error.data$).toMatchObjectType<{
|
|
307
|
+
status: "error";
|
|
308
|
+
error: {
|
|
309
|
+
code: KomboGeneralErrorCode | null;
|
|
310
|
+
title: string | null;
|
|
311
|
+
message: string;
|
|
312
|
+
log_url: string | null;
|
|
313
|
+
};
|
|
314
|
+
}>();
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
describe("Unexpected response formats", () => {
|
|
319
|
+
describe("KomboDefaultError thrown for non-JSON responses", () => {
|
|
320
|
+
it("handles plain text 500 error from load balancer", async () => {
|
|
321
|
+
const ctx = new TestContext();
|
|
322
|
+
|
|
323
|
+
ctx.mockEndpoint({
|
|
324
|
+
method: "GET",
|
|
325
|
+
path: "/v1/ats/jobs",
|
|
326
|
+
response: {
|
|
327
|
+
statusCode: 500,
|
|
328
|
+
body: "500 Internal Server Error",
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
for await (const _page of await ctx.kombo.ats.getJobs({})) {
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
expect.fail("Expected error to be thrown");
|
|
337
|
+
} catch (error) {
|
|
338
|
+
if (!(error instanceof KomboDefaultError)) {
|
|
339
|
+
expect.fail(
|
|
340
|
+
`Expected error to be a KomboDefaultError, got ${error?.constructor?.name}`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
expect(error).toMatchInlineSnapshot(
|
|
345
|
+
`[KomboDefaultError: Unexpected Status or Content-Type: Status 500 Content-Type "". Body: 500 Internal Server Error]`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("handles plain text 502 bad gateway error", async () => {
|
|
351
|
+
const ctx = new TestContext();
|
|
352
|
+
|
|
353
|
+
ctx.mockEndpoint({
|
|
354
|
+
method: "GET",
|
|
355
|
+
path: "/v1/hris/employees",
|
|
356
|
+
response: {
|
|
357
|
+
statusCode: 502,
|
|
358
|
+
body: "502 Bad Gateway",
|
|
359
|
+
headers: {
|
|
360
|
+
"Content-Type": "text/plain",
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
for await (const _page of await ctx.kombo.hris.getEmployees({})) {
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
expect.fail("Expected error to be thrown");
|
|
370
|
+
} catch (error) {
|
|
371
|
+
if (!(error instanceof KomboDefaultError)) {
|
|
372
|
+
expect.fail(
|
|
373
|
+
`Expected error to be a KomboDefaultError, got ${error?.constructor?.name}`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
expect(error).toMatchInlineSnapshot(
|
|
378
|
+
`[KomboDefaultError: Unexpected Status or Content-Type: Status 502 Content-Type text/plain. Body: 502 Bad Gateway]`
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("handles HTML error page from nginx", async () => {
|
|
384
|
+
const ctx = new TestContext();
|
|
385
|
+
|
|
386
|
+
const htmlErrorPage = `<!DOCTYPE html>
|
|
387
|
+
<html>
|
|
388
|
+
<head><title>503 Service Temporarily Unavailable</title></head>
|
|
389
|
+
<body>
|
|
390
|
+
<center><h1>503 Service Temporarily Unavailable</h1></center>
|
|
391
|
+
<hr><center>nginx/1.18.0</center>
|
|
392
|
+
</body>
|
|
393
|
+
</html>`;
|
|
394
|
+
|
|
395
|
+
ctx.mockEndpoint({
|
|
396
|
+
method: "POST",
|
|
397
|
+
path: "/v1/ats/jobs/test-job-id/applications",
|
|
398
|
+
response: {
|
|
399
|
+
statusCode: 503,
|
|
400
|
+
body: htmlErrorPage,
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
await ctx.kombo.ats.createApplication({
|
|
406
|
+
job_id: "test-job-id",
|
|
407
|
+
body: {
|
|
408
|
+
candidate: {
|
|
409
|
+
first_name: "John",
|
|
410
|
+
last_name: "Doe",
|
|
411
|
+
email_address: "john.doe@example.com",
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
expect.fail("Expected error to be thrown");
|
|
416
|
+
} catch (error) {
|
|
417
|
+
if (!(error instanceof KomboDefaultError)) {
|
|
418
|
+
expect.fail(
|
|
419
|
+
`Expected error to be a KomboDefaultError, got ${error?.constructor?.name}`
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
expect(error).toMatchInlineSnapshot(`
|
|
424
|
+
[KomboDefaultError: Unexpected Status or Content-Type: Status 503 Content-Type ""
|
|
425
|
+
Body: <!DOCTYPE html>
|
|
426
|
+
<html>
|
|
427
|
+
<head><title>503 Service Temporarily Unavailable</title></head>
|
|
428
|
+
<body>
|
|
429
|
+
<center><h1>503 Service Temporarily Unavailable</h1></center>
|
|
430
|
+
<hr><center>nginx/1.18.0</center>
|
|
431
|
+
</body>
|
|
432
|
+
</html>]
|
|
433
|
+
`);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it("handles empty response body with error status code", async () => {
|
|
438
|
+
const ctx = new TestContext();
|
|
439
|
+
|
|
440
|
+
ctx.mockEndpoint({
|
|
441
|
+
method: "GET",
|
|
442
|
+
path: "/v1/check-api-key",
|
|
443
|
+
response: {
|
|
444
|
+
statusCode: 500,
|
|
445
|
+
body: "",
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
await ctx.kombo.general.checkApiKey();
|
|
451
|
+
expect.fail("Expected error to be thrown");
|
|
452
|
+
} catch (error) {
|
|
453
|
+
if (!(error instanceof KomboDefaultError)) {
|
|
454
|
+
expect.fail(
|
|
455
|
+
`Expected error to be a KomboDefaultError, got ${error?.constructor?.name}`
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
expect(error).toMatchInlineSnapshot(
|
|
460
|
+
`[KomboDefaultError: Unexpected Status or Content-Type: Status 500 Content-Type "". Body: ""]`
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it("handles unexpected Content-Type header", async () => {
|
|
466
|
+
const ctx = new TestContext();
|
|
467
|
+
|
|
468
|
+
// Response with unexpected Content-Type
|
|
469
|
+
ctx.mockEndpoint({
|
|
470
|
+
method: "GET",
|
|
471
|
+
path: "/v1/ats/applications",
|
|
472
|
+
response: {
|
|
473
|
+
statusCode: 500,
|
|
474
|
+
body: "Server error occurred",
|
|
475
|
+
headers: {
|
|
476
|
+
"Content-Type": "text/xml",
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
try {
|
|
482
|
+
for await (const _page of await ctx.kombo.ats.getApplications({})) {
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
expect.fail("Expected error to be thrown");
|
|
486
|
+
} catch (error) {
|
|
487
|
+
if (!(error instanceof KomboDefaultError)) {
|
|
488
|
+
expect.fail(
|
|
489
|
+
`Expected error to be a KomboDefaultError, got ${error?.constructor?.name}`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
expect(error).toMatchInlineSnapshot(
|
|
494
|
+
`[KomboDefaultError: Unexpected Status or Content-Type: Status 500 Content-Type text/xml. Body: Server error occurred]`
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// NOTE: Currently disabled because it seems like Speakeasy doesn't
|
|
501
|
+
// correctly catch this kind of error (e.g., it doesn't bubble up and the
|
|
502
|
+
// promise is never rejected)
|
|
503
|
+
it.skip("handles malformed JSON response with proper Content-Type header (SyntaxError)", async () => {
|
|
504
|
+
const ctx = new TestContext();
|
|
505
|
+
|
|
506
|
+
const malformedJson = '{"status": "error", "error": {"code": "UNKNOWN"';
|
|
507
|
+
|
|
508
|
+
ctx.mockEndpoint({
|
|
509
|
+
method: "GET",
|
|
510
|
+
path: "/v1/ats/applications",
|
|
511
|
+
response: {
|
|
512
|
+
statusCode: 500,
|
|
513
|
+
body: malformedJson,
|
|
514
|
+
headers: {
|
|
515
|
+
"Content-Type": "application/json",
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
await expect(async () => {
|
|
521
|
+
for await (const _page of await ctx.kombo.ats.getApplications({})) {
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
}).rejects.toThrow(SyntaxError);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it("handles unexpected JSON structure in error response (ResponseValidationError)", async () => {
|
|
528
|
+
const ctx = new TestContext();
|
|
529
|
+
|
|
530
|
+
// Valid JSON but unexpected structure (not matching Kombo error format)
|
|
531
|
+
const unexpectedJson = {
|
|
532
|
+
errorCode: "500",
|
|
533
|
+
errorMessage: "Internal server error",
|
|
534
|
+
timestamp: "2024-01-01T00:00:00Z",
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
ctx.mockEndpoint({
|
|
538
|
+
method: "GET",
|
|
539
|
+
path: "/v1/ats/jobs",
|
|
540
|
+
response: {
|
|
541
|
+
statusCode: 500,
|
|
542
|
+
body: unexpectedJson,
|
|
543
|
+
},
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
try {
|
|
547
|
+
for await (const _page of await ctx.kombo.ats.getJobs({})) {
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
expect.fail("Expected error to be thrown");
|
|
551
|
+
} catch (error) {
|
|
552
|
+
// Valid JSON but unexpected structure triggers ResponseValidationError
|
|
553
|
+
if (!(error instanceof ResponseValidationError)) {
|
|
554
|
+
expect.fail(
|
|
555
|
+
`Expected error to be a ResponseValidationError, got ${error?.constructor?.name}`
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
expect(error).toMatchInlineSnapshot(
|
|
560
|
+
`[ResponseValidationError: Response validation failed]`
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
describe("HTTP client errors", () => {
|
|
567
|
+
it("handles request timeout", async () => {
|
|
568
|
+
const ctx = new TestContext();
|
|
569
|
+
|
|
570
|
+
// Mock endpoint but delay the connection to exceed SDK timeout
|
|
571
|
+
ctx.mockEndpoint({
|
|
572
|
+
method: "GET",
|
|
573
|
+
path: "/v1/ats/jobs",
|
|
574
|
+
response: {
|
|
575
|
+
statusCode: 200,
|
|
576
|
+
body: { data: [] },
|
|
577
|
+
},
|
|
578
|
+
delayResponseMs: 500,
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
try {
|
|
582
|
+
// Use a short timeout for this test
|
|
583
|
+
for await (const _page of await ctx.kombo.ats.getJobs(
|
|
584
|
+
{},
|
|
585
|
+
{ timeout_ms: 100 }
|
|
586
|
+
)) {
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
expect.fail("Expected error to be thrown");
|
|
590
|
+
} catch (error) {
|
|
591
|
+
// With real timers and delayed connection, we should get RequestTimeoutError
|
|
592
|
+
if (!(error instanceof RequestTimeoutError)) {
|
|
593
|
+
expect.fail(
|
|
594
|
+
`Expected error to be a RequestTimeoutError, got ${error?.constructor?.name}`
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
expect(error).toMatchInlineSnapshot(
|
|
599
|
+
`[RequestTimeoutError: Request timed out: TimeoutError: The operation was aborted due to timeout]`
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
});
|