@superblocksteam/sabs-client 0.752.0 → 0.753.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/dist/sabs.test.js DELETED
@@ -1,1478 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- const sabs_types_1 = require("@superblocksteam/sabs-types");
37
- const axios_1 = __importStar(require("axios"));
38
- const errors_1 = require("./errors");
39
- const sabs_1 = require("./sabs");
40
- describe('sabs service', () => {
41
- let anyBuildKey;
42
- let anyAccessToken;
43
- beforeEach(() => {
44
- anyBuildKey = 'any-secret-build-key';
45
- anyAccessToken = 'any-access-token';
46
- });
47
- afterEach(() => {
48
- jest.restoreAllMocks();
49
- });
50
- describe('build', () => {
51
- test.each([{ accessToken: 'anyScopedJwt', expectedHeaders: { Authorization: 'Bearer anyScopedJwt' } }])('returns expected response with accessToken=$accessToken', async ({ accessToken, expectedHeaders }) => {
52
- const expectedBuildId = 'expectedBuildId';
53
- const expectedCreated = new Date();
54
- const expectedUpdated = new Date();
55
- const mockAxios = jest.spyOn(axios_1.default, 'request');
56
- mockAxios.mockResolvedValue({
57
- data: {
58
- buildId: expectedBuildId,
59
- created: expectedCreated,
60
- updated: expectedUpdated
61
- }
62
- });
63
- const anyDirectoryHash = 'anyDirectoryHash';
64
- const anyApplicationMetadata = new sabs_types_1.ApplicationMetadata({
65
- id: 'anyApplicationId',
66
- organizationId: 'anyOrganizationId'
67
- });
68
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
69
- const result = await sabs.build({
70
- directoryHash: anyDirectoryHash,
71
- meta: anyApplicationMetadata,
72
- buildKey: anyBuildKey,
73
- accessToken
74
- });
75
- expect(result).toEqual({
76
- buildId: expectedBuildId,
77
- created: expectedCreated,
78
- updated: expectedUpdated
79
- });
80
- expect(mockAxios).toHaveBeenCalledWith({
81
- method: 'POST',
82
- url: 'http://localhost:3000/v1/builds',
83
- headers: expectedHeaders,
84
- data: {
85
- directoryHash: anyDirectoryHash,
86
- applicationMetadata: anyApplicationMetadata,
87
- buildKey: anyBuildKey
88
- }
89
- });
90
- });
91
- test('adds headers from the typed request-header hook', async () => {
92
- const mockAxios = jest.spyOn(axios_1.default, 'request');
93
- mockAxios.mockResolvedValue({
94
- data: {
95
- buildId: 'expected-build-id',
96
- created: new Date(),
97
- updated: new Date()
98
- }
99
- });
100
- const sabs = new sabs_1.SabsClient('http://localhost:3000', {
101
- requestHeaders: () => ({ traceparent: '00-trace-span-01' })
102
- });
103
- await sabs.build({
104
- accessToken: anyAccessToken,
105
- buildKey: anyBuildKey,
106
- directoryHash: 'any-directory-hash',
107
- meta: new sabs_types_1.ApplicationMetadata({
108
- id: 'any-application-id',
109
- organizationId: 'any-organization-id'
110
- })
111
- });
112
- expect(mockAxios).toHaveBeenCalledWith(expect.objectContaining({
113
- headers: {
114
- Authorization: `Bearer ${anyAccessToken}`,
115
- traceparent: '00-trace-span-01'
116
- }
117
- }));
118
- });
119
- test('raises error when request fails', async () => {
120
- const mockAxios = jest.spyOn(axios_1.default, 'request');
121
- mockAxios.mockRejectedValue(new Error('any error'));
122
- const anyDirectoryHash = 'anyDirectoryHash';
123
- const anyApplicationMetadata = new sabs_types_1.ApplicationMetadata({
124
- id: 'anyApplicationId',
125
- organizationId: 'anyOrganizationId'
126
- });
127
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
128
- await expect(sabs.build({ directoryHash: anyDirectoryHash, meta: anyApplicationMetadata, buildKey: anyBuildKey, accessToken: anyAccessToken })).rejects.toThrow();
129
- expect(mockAxios).toHaveBeenCalledWith({
130
- method: 'POST',
131
- url: 'http://localhost:3000/v1/builds',
132
- headers: { Authorization: `Bearer ${anyAccessToken}` },
133
- data: {
134
- directoryHash: anyDirectoryHash,
135
- applicationMetadata: anyApplicationMetadata,
136
- buildKey: anyBuildKey
137
- }
138
- });
139
- });
140
- test('raises error when directory hash is empty', async () => {
141
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
142
- const anyApplicationMetadata = new sabs_types_1.ApplicationMetadata({
143
- id: 'anyApplicationId',
144
- organizationId: 'anyOrganizationId'
145
- });
146
- await expect(sabs.build({ directoryHash: '', meta: anyApplicationMetadata, buildKey: anyBuildKey })).rejects.toThrow();
147
- });
148
- test('raises error when application metadata is empty', async () => {
149
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
150
- await expect(sabs.build({
151
- directoryHash: '',
152
- meta: undefined,
153
- buildKey: anyBuildKey,
154
- accessToken: anyAccessToken
155
- })).rejects.toThrow();
156
- });
157
- test('raises error when build key is empty', async () => {
158
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
159
- const anyApplicationMetadata = new sabs_types_1.ApplicationMetadata({
160
- id: 'anyApplicationId',
161
- organizationId: 'anyOrganizationId'
162
- });
163
- await expect(sabs.build({ directoryHash: 'anyDirectoryHash', meta: anyApplicationMetadata, buildKey: '', accessToken: anyAccessToken })).rejects.toThrow();
164
- });
165
- test('raises error when application id is empty', async () => {
166
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
167
- const anyApplicationMetadata = new sabs_types_1.ApplicationMetadata({
168
- id: '',
169
- organizationId: 'anyOrganizationId'
170
- });
171
- await expect(sabs.build({ directoryHash: 'anyDirectoryHash', meta: anyApplicationMetadata, buildKey: anyBuildKey, accessToken: anyAccessToken })).rejects.toThrow();
172
- });
173
- test('raises error when organization id is empty', async () => {
174
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
175
- const anyApplicationMetadata = new sabs_types_1.ApplicationMetadata({
176
- id: 'anyApplicationId',
177
- organizationId: ''
178
- });
179
- await expect(sabs.build({ directoryHash: 'anyDirectoryHash', meta: anyApplicationMetadata, buildKey: anyBuildKey, accessToken: anyAccessToken })).rejects.toThrow();
180
- });
181
- test('raises error when access token is empty', async () => {
182
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
183
- const anyApplicationMetadata = new sabs_types_1.ApplicationMetadata({
184
- id: 'anyApplicationId',
185
- organizationId: 'anyOrganizationId'
186
- });
187
- await expect(sabs.build({ directoryHash: 'anyDirectoryHash', meta: anyApplicationMetadata, buildKey: anyBuildKey, accessToken: '' })).rejects.toThrow();
188
- });
189
- });
190
- describe('status', () => {
191
- test.each([{ accessToken: 'anyScopedJwt', expectedHeaders: { Authorization: 'Bearer anyScopedJwt' } }])('returns expected response with accessToken=$accessToken', async ({ accessToken, expectedHeaders }) => {
192
- const expectedBuildId = 'expectedBuildId';
193
- const expectedStatus = sabs_types_1.BuildStatus.SUCCESS;
194
- const expectedCreated = new Date();
195
- const expectedUpdated = new Date();
196
- const mockAxios = jest.spyOn(axios_1.default, 'request');
197
- mockAxios.mockResolvedValue({
198
- data: {
199
- buildId: expectedBuildId,
200
- status: expectedStatus,
201
- created: expectedCreated,
202
- updated: expectedUpdated
203
- }
204
- });
205
- const anyBuildId = 'anyBuildId';
206
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
207
- const result = await sabs.status({ buildId: anyBuildId, accessToken });
208
- expect(result).toEqual({
209
- buildId: expectedBuildId,
210
- status: expectedStatus,
211
- created: expectedCreated,
212
- updated: expectedUpdated
213
- });
214
- expect(mockAxios).toHaveBeenCalledWith({
215
- method: 'GET',
216
- url: `http://localhost:3000/v1/builds/${anyBuildId}`,
217
- headers: expectedHeaders
218
- });
219
- });
220
- test('raises error when request fails', async () => {
221
- const mockAxios = jest.spyOn(axios_1.default, 'request');
222
- mockAxios.mockRejectedValue(new Error('any error'));
223
- const anyBuildId = 'anyBuildId';
224
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
225
- await expect(sabs.status({ buildId: anyBuildId, accessToken: anyAccessToken })).rejects.toThrow();
226
- expect(mockAxios).toHaveBeenCalledWith({
227
- headers: { Authorization: `Bearer ${anyAccessToken}` },
228
- method: 'GET',
229
- url: `http://localhost:3000/v1/builds/${anyBuildId}`
230
- });
231
- });
232
- test('raises error when build id is empty', async () => {
233
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
234
- await expect(sabs.status({ buildId: '', accessToken: anyAccessToken })).rejects.toThrow();
235
- });
236
- test('raises error when access token is empty', async () => {
237
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
238
- await expect(sabs.status({ buildId: 'anyBuildId', accessToken: '' })).rejects.toThrow();
239
- });
240
- });
241
- describe('list', () => {
242
- test.each([{ accessToken: 'anyScopedJwt', expectedHeaders: { Authorization: 'Bearer anyScopedJwt' } }])('returns expected response with accessToken=$accessToken', async ({ accessToken, expectedHeaders }) => {
243
- const mockAxios = jest.spyOn(axios_1.default, 'request');
244
- mockAxios.mockResolvedValue({
245
- data: {
246
- builds: [
247
- {
248
- buildId: 'id1',
249
- status: sabs_types_1.BuildStatus.SUCCESS,
250
- created: new Date('2023-01-01'),
251
- updated: new Date('2023-01-02')
252
- },
253
- {
254
- buildId: 'id2',
255
- status: sabs_types_1.BuildStatus.RUNNING,
256
- created: new Date('2023-01-03'),
257
- updated: new Date('2023-01-04')
258
- },
259
- {
260
- buildId: 'id3',
261
- status: sabs_types_1.BuildStatus.FAILED,
262
- error: 'Build failed',
263
- created: new Date('2023-01-05'),
264
- updated: new Date('2023-01-06')
265
- }
266
- ]
267
- }
268
- });
269
- const anyOrganizationId = 'anyOrganizationId';
270
- const anyApplicationId = 'anyApplicationId';
271
- const anyDirectoryHash = 'anyDirectoryHash';
272
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
273
- const result = await sabs.list({
274
- organizationId: anyOrganizationId,
275
- applicationId: anyApplicationId,
276
- directoryHash: anyDirectoryHash,
277
- accessToken
278
- });
279
- expect(result).toEqual({
280
- builds: [
281
- {
282
- buildId: 'id1',
283
- status: sabs_types_1.BuildStatus.SUCCESS,
284
- created: new Date('2023-01-01'),
285
- updated: new Date('2023-01-02')
286
- },
287
- {
288
- buildId: 'id2',
289
- status: sabs_types_1.BuildStatus.RUNNING,
290
- created: new Date('2023-01-03'),
291
- updated: new Date('2023-01-04')
292
- },
293
- {
294
- buildId: 'id3',
295
- status: sabs_types_1.BuildStatus.FAILED,
296
- error: 'Build failed',
297
- created: new Date('2023-01-05'),
298
- updated: new Date('2023-01-06')
299
- }
300
- ]
301
- });
302
- expect(mockAxios).toHaveBeenCalledWith({
303
- method: 'GET',
304
- url: `http://localhost:3000/v1/build`,
305
- headers: expectedHeaders,
306
- params: {
307
- organizationId: anyOrganizationId,
308
- applicationId: anyApplicationId,
309
- directoryHash: anyDirectoryHash
310
- }
311
- });
312
- });
313
- test('raises error when request fails', async () => {
314
- const mockAxios = jest.spyOn(axios_1.default, 'request');
315
- mockAxios.mockRejectedValue(new Error('any error'));
316
- const anyOrganizationId = 'anyOrganizationId';
317
- const anyApplicationId = 'anyApplicationId';
318
- const anyDirectoryHash = 'anyDirectoryHash';
319
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
320
- await expect(sabs.list({
321
- organizationId: anyOrganizationId,
322
- applicationId: anyApplicationId,
323
- directoryHash: anyDirectoryHash,
324
- accessToken: anyAccessToken
325
- })).rejects.toThrow();
326
- expect(mockAxios).toHaveBeenCalledWith({
327
- method: 'GET',
328
- url: `http://localhost:3000/v1/build`,
329
- headers: { Authorization: `Bearer ${anyAccessToken}` },
330
- params: {
331
- organizationId: anyOrganizationId,
332
- applicationId: anyApplicationId,
333
- directoryHash: anyDirectoryHash
334
- }
335
- });
336
- });
337
- test('raises error when organization id is empty', async () => {
338
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
339
- await expect(sabs.list({ organizationId: '', applicationId: 'anyApplicationId', directoryHash: 'anyDirectoryHash', accessToken: anyAccessToken })).rejects.toThrow();
340
- });
341
- test('raises error when application id is empty', async () => {
342
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
343
- await expect(sabs.list({
344
- organizationId: 'anyOrganizationId',
345
- applicationId: '',
346
- directoryHash: 'anyDirectoryHash',
347
- accessToken: anyAccessToken
348
- })).rejects.toThrow();
349
- });
350
- test('raises error when directory hash is empty', async () => {
351
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
352
- await expect(sabs.list({
353
- organizationId: 'anyOrganizationId',
354
- applicationId: 'anyApplicationId',
355
- directoryHash: '',
356
- accessToken: anyAccessToken
357
- })).rejects.toThrow();
358
- });
359
- test('raises error when access token is empty', async () => {
360
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
361
- await expect(sabs.list({
362
- organizationId: 'anyOrganizationId',
363
- applicationId: 'anyApplicationId',
364
- directoryHash: 'anyDirectoryHash',
365
- accessToken: ''
366
- })).rejects.toThrow();
367
- });
368
- });
369
- describe('terminate', () => {
370
- test.each([{ accessToken: 'anyScopedJwt', expectedHeaders: { Authorization: 'Bearer anyScopedJwt' } }])('returns expected response with accessToken=$accessToken', async ({ accessToken, expectedHeaders }) => {
371
- const expectedBuildId = 'expectedBuildId';
372
- const expectedStatus = sabs_types_1.BuildStatus.TIMED_OUT;
373
- const expectedError = 'build timed out';
374
- const expectedCreated = new Date();
375
- const expectedUpdated = new Date();
376
- const mockAxios = jest.spyOn(axios_1.default, 'request');
377
- mockAxios.mockResolvedValue({
378
- data: {
379
- buildId: expectedBuildId,
380
- status: expectedStatus,
381
- error: expectedError,
382
- created: expectedCreated,
383
- updated: expectedUpdated
384
- }
385
- });
386
- const anyBuildId = 'anyBuildId';
387
- const anyStatus = sabs_types_1.BuildStatus.TIMED_OUT;
388
- const anyError = 'build timed out';
389
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
390
- const result = await sabs.terminate({
391
- buildId: anyBuildId,
392
- status: anyStatus,
393
- buildKey: anyBuildKey,
394
- error: anyError,
395
- accessToken
396
- });
397
- expect(result).toEqual({
398
- buildId: expectedBuildId,
399
- status: expectedStatus,
400
- error: expectedError,
401
- created: expectedCreated,
402
- updated: expectedUpdated
403
- });
404
- expect(mockAxios).toHaveBeenCalledWith({
405
- method: 'POST',
406
- url: `http://localhost:3000/v1/builds/${anyBuildId}/terminate`,
407
- headers: expectedHeaders,
408
- data: {
409
- buildId: anyBuildId,
410
- status: anyStatus,
411
- error: anyError,
412
- buildKey: anyBuildKey
413
- }
414
- });
415
- });
416
- test('raises error when request fails', async () => {
417
- const mockAxios = jest.spyOn(axios_1.default, 'request');
418
- mockAxios.mockRejectedValue(new Error('any error'));
419
- const anyBuildId = 'anyBuildId';
420
- const anyStatus = sabs_types_1.BuildStatus.TIMED_OUT;
421
- const anyError = 'build timed out';
422
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
423
- await expect(sabs.terminate({ buildId: anyBuildId, status: anyStatus, buildKey: anyBuildKey, error: anyError, accessToken: anyAccessToken })).rejects.toThrow();
424
- expect(mockAxios).toHaveBeenCalledWith({
425
- method: 'POST',
426
- headers: { Authorization: `Bearer ${anyAccessToken}` },
427
- url: `http://localhost:3000/v1/builds/${anyBuildId}/terminate`,
428
- data: {
429
- buildId: anyBuildId,
430
- status: anyStatus,
431
- error: anyError,
432
- buildKey: anyBuildKey
433
- }
434
- });
435
- });
436
- test('raises error when build id is empty', async () => {
437
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
438
- await expect(sabs.terminate({
439
- buildId: '',
440
- status: sabs_types_1.BuildStatus.TIMED_OUT,
441
- buildKey: anyBuildKey,
442
- error: 'build timed out',
443
- accessToken: anyAccessToken
444
- })).rejects.toThrow();
445
- });
446
- test('raises error when status is empty', async () => {
447
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
448
- await expect(sabs.terminate({
449
- buildId: 'anyBuildId',
450
- status: undefined,
451
- buildKey: anyBuildKey,
452
- error: 'build timed out',
453
- accessToken: anyAccessToken
454
- })).rejects.toThrow();
455
- });
456
- test('raises error when build key is empty', async () => {
457
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
458
- await expect(sabs.terminate({
459
- buildId: 'anyBuildId',
460
- status: sabs_types_1.BuildStatus.TIMED_OUT,
461
- buildKey: '',
462
- error: 'build timed out',
463
- accessToken: anyAccessToken
464
- })).rejects.toThrow();
465
- });
466
- test('raises error when access token is empty', async () => {
467
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
468
- await expect(sabs.terminate({
469
- buildId: 'anyBuildId',
470
- status: sabs_types_1.BuildStatus.TIMED_OUT,
471
- buildKey: anyBuildKey,
472
- error: 'build timed out',
473
- accessToken: ''
474
- })).rejects.toThrow();
475
- });
476
- test('includes stepTimings when provided', async () => {
477
- const mockAxios = jest.spyOn(axios_1.default, 'request');
478
- mockAxios.mockResolvedValue({
479
- data: {
480
- buildId: 'expectedBuildId',
481
- status: sabs_types_1.BuildStatus.SUCCESS,
482
- created: new Date(),
483
- updated: new Date()
484
- }
485
- });
486
- const stepTimings = {
487
- createTempDirMs: 100,
488
- fetchAppBundleMs: 200,
489
- npmInstallMs: 300,
490
- buildMs: 400,
491
- uploadMs: 500
492
- };
493
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
494
- await sabs.terminate({
495
- buildId: 'anyBuildId',
496
- status: sabs_types_1.BuildStatus.SUCCESS,
497
- buildKey: anyBuildKey,
498
- stepTimings,
499
- accessToken: anyAccessToken
500
- });
501
- expect(mockAxios).toHaveBeenCalledWith(expect.objectContaining({
502
- data: expect.objectContaining({
503
- stepTimings: expect.objectContaining({
504
- createTempDirMs: BigInt(100),
505
- fetchAppBundleMs: BigInt(200),
506
- npmInstallMs: BigInt(300),
507
- buildMs: BigInt(400),
508
- uploadMs: BigInt(500)
509
- })
510
- })
511
- }));
512
- });
513
- test('includes sizeMetrics when provided', async () => {
514
- const mockAxios = jest.spyOn(axios_1.default, 'request');
515
- mockAxios.mockResolvedValue({
516
- data: {
517
- buildId: 'expectedBuildId',
518
- status: sabs_types_1.BuildStatus.SUCCESS,
519
- created: new Date(),
520
- updated: new Date()
521
- }
522
- });
523
- const sizeMetrics = {
524
- sourceBundleSizeBytes: 1000000,
525
- nodeModulesSizeBytes: 50000000,
526
- buildOutputSizeBytes: 2000000,
527
- dependencyCount: 150
528
- };
529
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
530
- await sabs.terminate({
531
- buildId: 'anyBuildId',
532
- status: sabs_types_1.BuildStatus.SUCCESS,
533
- buildKey: anyBuildKey,
534
- sizeMetrics,
535
- accessToken: anyAccessToken
536
- });
537
- expect(mockAxios).toHaveBeenCalledWith(expect.objectContaining({
538
- data: expect.objectContaining({
539
- sizeMetrics: expect.objectContaining({
540
- sourceBundleSizeBytes: BigInt(1000000),
541
- nodeModulesSizeBytes: BigInt(50000000),
542
- buildOutputSizeBytes: BigInt(2000000),
543
- dependencyCount: BigInt(150)
544
- })
545
- })
546
- }));
547
- });
548
- test('includes both stepTimings and sizeMetrics when provided', async () => {
549
- const mockAxios = jest.spyOn(axios_1.default, 'request');
550
- mockAxios.mockResolvedValue({
551
- data: {
552
- buildId: 'expectedBuildId',
553
- status: sabs_types_1.BuildStatus.SUCCESS,
554
- created: new Date(),
555
- updated: new Date()
556
- }
557
- });
558
- const stepTimings = {
559
- createTempDirMs: 100,
560
- npmInstallMs: 300
561
- };
562
- const sizeMetrics = {
563
- sourceBundleSizeBytes: 1000000,
564
- dependencyCount: 150
565
- };
566
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
567
- await sabs.terminate({
568
- buildId: 'anyBuildId',
569
- status: sabs_types_1.BuildStatus.SUCCESS,
570
- buildKey: anyBuildKey,
571
- stepTimings,
572
- sizeMetrics,
573
- accessToken: anyAccessToken
574
- });
575
- expect(mockAxios).toHaveBeenCalledWith(expect.objectContaining({
576
- data: expect.objectContaining({
577
- stepTimings: expect.objectContaining({
578
- createTempDirMs: BigInt(100),
579
- npmInstallMs: BigInt(300)
580
- }),
581
- sizeMetrics: expect.objectContaining({
582
- sourceBundleSizeBytes: BigInt(1000000),
583
- dependencyCount: BigInt(150)
584
- })
585
- })
586
- }));
587
- });
588
- test('handles partial sizeMetrics with undefined fields', async () => {
589
- const mockAxios = jest.spyOn(axios_1.default, 'request');
590
- mockAxios.mockResolvedValue({
591
- data: {
592
- buildId: 'expectedBuildId',
593
- status: sabs_types_1.BuildStatus.SUCCESS,
594
- created: new Date(),
595
- updated: new Date()
596
- }
597
- });
598
- const sizeMetrics = {
599
- sourceBundleSizeBytes: 1000000
600
- // Other fields intentionally undefined
601
- };
602
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
603
- await sabs.terminate({
604
- buildId: 'anyBuildId',
605
- status: sabs_types_1.BuildStatus.SUCCESS,
606
- buildKey: anyBuildKey,
607
- sizeMetrics,
608
- accessToken: anyAccessToken
609
- });
610
- expect(mockAxios).toHaveBeenCalledWith(expect.objectContaining({
611
- data: expect.objectContaining({
612
- sizeMetrics: expect.objectContaining({
613
- sourceBundleSizeBytes: BigInt(1000000)
614
- })
615
- })
616
- }));
617
- });
618
- });
619
- describe('bulkStatus', () => {
620
- test('returns expected response', async () => {
621
- const mockAxios = jest.spyOn(axios_1.default, 'request');
622
- mockAxios.mockResolvedValue({
623
- data: {
624
- builds: [
625
- {
626
- buildId: 'build1',
627
- status: sabs_types_1.BuildStatus.SUCCESS,
628
- created: new Date('2023-01-01'),
629
- updated: new Date('2023-01-02')
630
- },
631
- {
632
- buildId: 'build2',
633
- status: sabs_types_1.BuildStatus.RUNNING,
634
- created: new Date('2023-01-03'),
635
- updated: new Date('2023-01-04')
636
- }
637
- ]
638
- }
639
- });
640
- const anyOrganizationId = 'anyOrganizationId';
641
- const anyApplicationId = 'anyApplicationId';
642
- const anyDirectoryHashes = ['hash1', 'hash2'];
643
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
644
- const result = await sabs.bulkStatus({
645
- organizationId: anyOrganizationId,
646
- applicationId: anyApplicationId,
647
- directoryHashes: anyDirectoryHashes,
648
- accessToken: anyAccessToken
649
- });
650
- expect(result).toEqual({
651
- builds: [
652
- {
653
- buildId: 'build1',
654
- status: sabs_types_1.BuildStatus.SUCCESS,
655
- created: new Date('2023-01-01'),
656
- updated: new Date('2023-01-02')
657
- },
658
- {
659
- buildId: 'build2',
660
- status: sabs_types_1.BuildStatus.RUNNING,
661
- created: new Date('2023-01-03'),
662
- updated: new Date('2023-01-04')
663
- }
664
- ]
665
- });
666
- expect(mockAxios).toHaveBeenCalledWith({
667
- method: 'POST',
668
- headers: { Authorization: `Bearer ${anyAccessToken}` },
669
- url: `http://localhost:3000/v1/builds/${anyOrganizationId}/${anyApplicationId}/bulk-status`,
670
- data: {
671
- organizationId: anyOrganizationId,
672
- applicationId: anyApplicationId,
673
- directoryHashes: anyDirectoryHashes
674
- }
675
- });
676
- });
677
- test('raises error when access token is empty', async () => {
678
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
679
- await expect(sabs.bulkStatus({
680
- organizationId: 'anyOrganizationId',
681
- applicationId: 'anyApplicationId',
682
- directoryHashes: ['anyDirectoryHash'],
683
- accessToken: ''
684
- })).rejects.toThrow();
685
- });
686
- test('raises error when organization id is empty', async () => {
687
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
688
- await expect(sabs.bulkStatus({
689
- organizationId: '',
690
- applicationId: 'anyApplicationId',
691
- directoryHashes: ['anyDirectoryHash'],
692
- accessToken: anyAccessToken
693
- })).rejects.toThrow();
694
- });
695
- test('raises error when application id is empty', async () => {
696
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
697
- await expect(sabs.bulkStatus({
698
- organizationId: 'anyOrganizationId',
699
- applicationId: '',
700
- directoryHashes: ['anyDirectoryHash'],
701
- accessToken: anyAccessToken
702
- })).rejects.toThrow();
703
- });
704
- test('raises error when directory hashes is empty', async () => {
705
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
706
- await expect(sabs.bulkStatus({
707
- organizationId: 'anyOrganizationId',
708
- applicationId: 'anyApplicationId',
709
- directoryHashes: [],
710
- accessToken: anyAccessToken
711
- })).rejects.toThrow();
712
- });
713
- });
714
- describe('liveEdit', () => {
715
- const applicationId = 'anyApplicationId';
716
- const organizationId = 'anyOrganizationId';
717
- const branch = 'anyBranch';
718
- test('ensureLiveEdit enqueues an idempotent live edit', async () => {
719
- const mockAxios = jest.spyOn(axios_1.default, 'request');
720
- const response = { liveEdit: { liveEditId: 'liveEditId', status: sabs_types_1.LiveEditStatus.STARTING }, wasCreated: true };
721
- mockAxios.mockResolvedValue({ data: response });
722
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
723
- await expect(sabs.ensureLiveEdit({
724
- applicationId,
725
- organizationId,
726
- branch,
727
- expiresIn: 1000,
728
- accessToken: anyAccessToken
729
- })).resolves.toEqual(response);
730
- expect(mockAxios).toHaveBeenCalledWith({
731
- method: 'POST',
732
- url: 'http://localhost:3000/v2/live-edit/ensure',
733
- data: {
734
- sessionJwt: anyAccessToken,
735
- spec: {
736
- application: { applicationId, organizationId, branch },
737
- clientKey: '',
738
- expiresIn: BigInt(1000)
739
- }
740
- },
741
- headers: { Authorization: `Bearer ${anyAccessToken}` }
742
- });
743
- });
744
- test('getLiveEdit reads by live edit id', async () => {
745
- const mockAxios = jest.spyOn(axios_1.default, 'request');
746
- const response = { liveEdit: { liveEditId: 'liveEditId', status: sabs_types_1.LiveEditStatus.ACTIVE } };
747
- mockAxios.mockResolvedValue({ data: response });
748
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
749
- await expect(sabs.getLiveEdit({ liveEditId: 'liveEditId', accessToken: anyAccessToken })).resolves.toEqual(response);
750
- expect(mockAxios).toHaveBeenCalledWith({
751
- method: 'GET',
752
- url: 'http://localhost:3000/v2/live-edit/liveEditId',
753
- headers: { Authorization: `Bearer ${anyAccessToken}` }
754
- });
755
- });
756
- test('raises error when application id is empty', async () => {
757
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
758
- await expect(sabs.createLiveEdit({
759
- applicationId: '',
760
- organizationId: 'anyOrganizationId',
761
- branch: 'anyBranch',
762
- expiresIn: 1000,
763
- accessToken: anyAccessToken
764
- })).rejects.toThrow();
765
- });
766
- test('raises error when organization id is empty', async () => {
767
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
768
- await expect(sabs.createLiveEdit({
769
- applicationId: 'anyApplicationId',
770
- organizationId: '',
771
- branch: 'anyBranch',
772
- expiresIn: 1000,
773
- accessToken: anyAccessToken
774
- })).rejects.toThrow();
775
- });
776
- test('raises error when branch is empty', async () => {
777
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
778
- await expect(sabs.createLiveEdit({
779
- applicationId: 'anyApplicationId',
780
- organizationId: 'anyOrganizationId',
781
- branch: '',
782
- expiresIn: 1000,
783
- accessToken: anyAccessToken
784
- })).rejects.toThrow();
785
- });
786
- test('raises error when access token is empty', async () => {
787
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
788
- await expect(sabs.createLiveEdit({
789
- applicationId: 'anyApplicationId',
790
- organizationId: 'anyOrganizationId',
791
- branch: 'anyBranch',
792
- expiresIn: 1000,
793
- accessToken: ''
794
- })).rejects.toThrow();
795
- });
796
- test('createLiveEdit', async () => {
797
- const mockAxios = jest.spyOn(axios_1.default, 'request');
798
- const expiresInSeconds = 1000;
799
- const now = Date.now();
800
- const expectedRequest = {
801
- application: {
802
- applicationId,
803
- organizationId,
804
- branch
805
- },
806
- expiresIn: BigInt(expiresInSeconds),
807
- sessionJwt: anyAccessToken,
808
- clientKey: ''
809
- };
810
- const newLiveEditResponse = new sabs_types_1.CreateLiveEditResponse({
811
- liveEditId: 'liveEditId',
812
- liveEditUrl: 'http://localhost:3000/live-edit/liveEditId',
813
- application: {
814
- applicationId,
815
- organizationId,
816
- branch
817
- },
818
- expiresAt: BigInt(now + expiresInSeconds * 1000)
819
- });
820
- mockAxios.mockResolvedValue({ data: newLiveEditResponse });
821
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
822
- const result = await sabs.createLiveEdit({
823
- applicationId,
824
- organizationId,
825
- branch,
826
- expiresIn: expiresInSeconds,
827
- accessToken: anyAccessToken
828
- });
829
- expect(result).toEqual(newLiveEditResponse);
830
- expect(mockAxios).toHaveBeenCalledWith({
831
- method: 'POST',
832
- url: 'http://localhost:3000/v1/live-edit',
833
- data: expectedRequest,
834
- headers: {
835
- Authorization: `Bearer ${anyAccessToken}`
836
- }
837
- });
838
- });
839
- test('createLiveEdit with clientKey', async () => {
840
- const mockAxios = jest.spyOn(axios_1.default, 'request');
841
- const expiresInSeconds = 1000;
842
- const now = Date.now();
843
- const clientKey = 'user-123';
844
- const expectedRequest = {
845
- application: {
846
- applicationId,
847
- organizationId,
848
- branch
849
- },
850
- expiresIn: BigInt(expiresInSeconds),
851
- sessionJwt: anyAccessToken,
852
- clientKey: clientKey
853
- };
854
- const newLiveEditResponse = new sabs_types_1.CreateLiveEditResponse({
855
- liveEditId: 'liveEditId',
856
- liveEditUrl: 'http://localhost:3000/live-edit/liveEditId',
857
- application: {
858
- applicationId,
859
- organizationId,
860
- branch
861
- },
862
- expiresAt: BigInt(now + expiresInSeconds * 1000)
863
- });
864
- mockAxios.mockResolvedValue({ data: newLiveEditResponse });
865
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
866
- const result = await sabs.createLiveEdit({
867
- applicationId,
868
- organizationId,
869
- branch,
870
- expiresIn: expiresInSeconds,
871
- accessToken: anyAccessToken,
872
- clientKey: clientKey
873
- });
874
- expect(result).toEqual(newLiveEditResponse);
875
- expect(mockAxios).toHaveBeenCalledWith({
876
- method: 'POST',
877
- url: 'http://localhost:3000/v1/live-edit',
878
- data: expectedRequest,
879
- headers: {
880
- Authorization: `Bearer ${anyAccessToken}`
881
- }
882
- });
883
- });
884
- test('terminateLiveEdit', async () => {
885
- const mockAxios = jest.spyOn(axios_1.default, 'request');
886
- const liveEditId = 'liveEditId';
887
- const expectedRequest = { liveEditId };
888
- const expectedCreated = new Date();
889
- const expectedUpdated = new Date();
890
- const expectedResponse = {
891
- liveEditId,
892
- status: sabs_types_1.LiveEditStatus.TERMINATED,
893
- application: {
894
- applicationId,
895
- organizationId,
896
- branch
897
- },
898
- created: expectedCreated,
899
- updated: expectedUpdated
900
- };
901
- mockAxios.mockResolvedValue({ data: expectedResponse });
902
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
903
- const result = await sabs.terminateLiveEdit({ liveEditId, accessToken: anyAccessToken });
904
- expect(result).toEqual(expectedResponse);
905
- expect(mockAxios).toHaveBeenCalledWith({
906
- method: 'POST',
907
- url: `http://localhost:3000/v1/live-edit/${liveEditId}/terminate`,
908
- data: expectedRequest,
909
- headers: {
910
- Authorization: `Bearer ${anyAccessToken}`
911
- }
912
- });
913
- });
914
- test('raises error when live edit id is empty', async () => {
915
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
916
- await expect(sabs.terminateLiveEdit({ liveEditId: '', accessToken: anyAccessToken })).rejects.toThrow();
917
- });
918
- test('raises error when access token is empty', async () => {
919
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
920
- await expect(sabs.terminateLiveEdit({ liveEditId: 'liveEditId', accessToken: '' })).rejects.toThrow();
921
- });
922
- test('terminateLiveEdit raises error when request fails', async () => {
923
- const mockAxios = jest.spyOn(axios_1.default, 'request');
924
- mockAxios.mockRejectedValue(new Error('any error'));
925
- const liveEditId = 'liveEditId';
926
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
927
- await expect(sabs.terminateLiveEdit({ liveEditId, accessToken: anyAccessToken })).rejects.toThrow();
928
- expect(mockAxios).toHaveBeenCalledWith({
929
- method: 'POST',
930
- url: `http://localhost:3000/v1/live-edit/${liveEditId}/terminate`,
931
- data: {
932
- liveEditId
933
- },
934
- headers: {
935
- Authorization: `Bearer ${anyAccessToken}`
936
- }
937
- });
938
- });
939
- });
940
- describe('retry', () => {
941
- test('retries live-edit creation when the session being reused terminates', async () => {
942
- const replacement = { liveEditId: 'replacement-session', liveEditUrl: 'https://replacement.example.com', wasCreated: true };
943
- const mockAxios = jest.spyOn(axios_1.default, 'request');
944
- mockAxios
945
- .mockRejectedValueOnce(new axios_1.AxiosError('Service Unavailable', undefined, undefined, undefined, {
946
- config: { headers: new axios_1.AxiosHeaders() },
947
- data: { code: 14, message: 'live edit became terminated while polling for URL' },
948
- headers: {},
949
- status: 503,
950
- statusText: 'Service Unavailable'
951
- }))
952
- .mockResolvedValueOnce({ data: replacement });
953
- const sleep = jest.fn().mockResolvedValue(undefined);
954
- const sabs = new sabs_1.SabsClient('http://localhost:3000', { sleep });
955
- const result = await sabs.createLiveEdit({
956
- applicationId: 'app',
957
- organizationId: 'org',
958
- branch: 'main',
959
- expiresIn: 3600,
960
- accessToken: anyAccessToken,
961
- clientKey: 'same-client'
962
- });
963
- expect(result).toEqual(replacement);
964
- expect(mockAxios).toHaveBeenCalledTimes(2);
965
- expect(mockAxios.mock.calls[1][0]).toEqual(mockAxios.mock.calls[0][0]);
966
- expect(sleep).toHaveBeenCalledTimes(1);
967
- });
968
- test('retries on retriable error (5xx) and succeeds on subsequent attempt', async () => {
969
- const mockAxios = jest.spyOn(axios_1.default, 'request');
970
- const sleepCalls = [];
971
- mockAxios
972
- .mockRejectedValueOnce(new axios_1.AxiosError('Service Unavailable', 'ERR_NETWORK', undefined, undefined, {
973
- config: { headers: new axios_1.AxiosHeaders() },
974
- data: null,
975
- headers: {},
976
- status: 503,
977
- statusText: 'Service Unavailable'
978
- }))
979
- .mockResolvedValueOnce({
980
- data: { buildId: 'build-1', status: sabs_types_1.BuildStatus.SUCCESS, created: new Date(), updated: new Date() }
981
- });
982
- const sabs = new sabs_1.SabsClient('http://localhost:3000', {
983
- maxRetries: 2,
984
- sleep: (ms) => {
985
- sleepCalls.push(ms);
986
- return Promise.resolve();
987
- }
988
- });
989
- const result = await sabs.status({ buildId: 'anyBuildId', accessToken: anyAccessToken });
990
- expect(result).toEqual({
991
- buildId: 'build-1',
992
- status: sabs_types_1.BuildStatus.SUCCESS,
993
- created: expect.any(Date),
994
- updated: expect.any(Date)
995
- });
996
- expect(mockAxios).toHaveBeenCalledTimes(2);
997
- expect(sleepCalls).toHaveLength(1);
998
- expect(sleepCalls[0]).toBeGreaterThanOrEqual(500);
999
- expect(sleepCalls[0]).toBeLessThanOrEqual(1000);
1000
- });
1001
- test('refreshes request headers for every retry attempt', async () => {
1002
- const mockAxios = jest.spyOn(axios_1.default, 'request');
1003
- mockAxios
1004
- .mockRejectedValueOnce(new axios_1.AxiosError('Service Unavailable', 'ERR_NETWORK', undefined, undefined, {
1005
- config: { headers: new axios_1.AxiosHeaders() },
1006
- data: null,
1007
- headers: {},
1008
- status: 503,
1009
- statusText: 'Service Unavailable'
1010
- }))
1011
- .mockResolvedValueOnce({
1012
- data: { buildId: 'build-1', status: sabs_types_1.BuildStatus.RUNNING, created: new Date(), updated: new Date() }
1013
- });
1014
- let headerAttempt = 0;
1015
- const sabs = new sabs_1.SabsClient('http://localhost:3000', {
1016
- maxRetries: 1,
1017
- requestHeaders: () => ({ traceparent: `00-trace-span-${++headerAttempt}` }),
1018
- sleep: () => Promise.resolve()
1019
- });
1020
- await sabs.status({ buildId: 'anyBuildId', accessToken: anyAccessToken });
1021
- expect(mockAxios).toHaveBeenNthCalledWith(1, expect.objectContaining({
1022
- headers: expect.objectContaining({ traceparent: '00-trace-span-1' })
1023
- }));
1024
- expect(mockAxios).toHaveBeenNthCalledWith(2, expect.objectContaining({
1025
- headers: expect.objectContaining({ traceparent: '00-trace-span-2' })
1026
- }));
1027
- });
1028
- test('retries on network error (no response) and succeeds on second attempt', async () => {
1029
- const mockAxios = jest.spyOn(axios_1.default, 'request');
1030
- mockAxios
1031
- .mockRejectedValueOnce(new axios_1.AxiosError('ECONNREFUSED', 'ECONNREFUSED', undefined, undefined, undefined))
1032
- .mockResolvedValueOnce({
1033
- data: { buildId: 'build-1', status: sabs_types_1.BuildStatus.RUNNING, created: new Date(), updated: new Date() }
1034
- });
1035
- const sabs = new sabs_1.SabsClient('http://localhost:3000', {
1036
- maxRetries: 2,
1037
- sleep: () => Promise.resolve()
1038
- });
1039
- const result = await sabs.status({ buildId: 'anyBuildId', accessToken: anyAccessToken });
1040
- expect(result.buildId).toBe('build-1');
1041
- expect(mockAxios).toHaveBeenCalledTimes(2);
1042
- });
1043
- test('exhausts retries then throws on persistent retriable error', async () => {
1044
- const mockAxios = jest.spyOn(axios_1.default, 'request');
1045
- mockAxios.mockRejectedValue(new axios_1.AxiosError('Gateway Timeout', 'ETIMEDOUT', undefined, undefined, {
1046
- config: { headers: new axios_1.AxiosHeaders() },
1047
- data: null,
1048
- headers: {},
1049
- status: 504,
1050
- statusText: 'Gateway Timeout'
1051
- }));
1052
- const sabs = new sabs_1.SabsClient('http://localhost:3000', {
1053
- maxRetries: 2,
1054
- sleep: () => Promise.resolve()
1055
- });
1056
- await expect(sabs.status({ buildId: 'anyBuildId', accessToken: anyAccessToken })).rejects.toThrow();
1057
- expect(mockAxios).toHaveBeenCalledTimes(3);
1058
- });
1059
- test('does not retry on non-retriable error (4xx)', async () => {
1060
- const mockAxios = jest.spyOn(axios_1.default, 'request');
1061
- mockAxios.mockRejectedValue(new axios_1.AxiosError('Bad Request', 'BAD_REQUEST', undefined, undefined, {
1062
- config: { headers: new axios_1.AxiosHeaders() },
1063
- data: { message: 'Invalid build id' },
1064
- headers: {},
1065
- status: 400,
1066
- statusText: 'Bad Request'
1067
- }));
1068
- const sabs = new sabs_1.SabsClient('http://localhost:3000', {
1069
- maxRetries: 2,
1070
- sleep: () => Promise.resolve()
1071
- });
1072
- await expect(sabs.status({ buildId: 'anyBuildId', accessToken: anyAccessToken })).rejects.toThrow();
1073
- expect(mockAxios).toHaveBeenCalledTimes(1);
1074
- });
1075
- test('respects maxRetries: 0 (no retries)', async () => {
1076
- const mockAxios = jest.spyOn(axios_1.default, 'request');
1077
- mockAxios.mockRejectedValue(new axios_1.AxiosError('Service Unavailable', 'ERR_NETWORK', undefined, undefined, {
1078
- config: { headers: new axios_1.AxiosHeaders() },
1079
- data: null,
1080
- headers: {},
1081
- status: 503,
1082
- statusText: 'Service Unavailable'
1083
- }));
1084
- const sabs = new sabs_1.SabsClient('http://localhost:3000', { maxRetries: 0 });
1085
- await expect(sabs.status({ buildId: 'anyBuildId', accessToken: anyAccessToken })).rejects.toThrow();
1086
- expect(mockAxios).toHaveBeenCalledTimes(1);
1087
- });
1088
- test('respects maxDelayMs cap', async () => {
1089
- const mockAxios = jest.spyOn(axios_1.default, 'request');
1090
- const sleepCalls = [];
1091
- mockAxios
1092
- .mockRejectedValueOnce(new axios_1.AxiosError('Service Unavailable', 'ERR_NETWORK', undefined, undefined, {
1093
- config: { headers: new axios_1.AxiosHeaders() },
1094
- data: null,
1095
- headers: {},
1096
- status: 503,
1097
- statusText: 'Service Unavailable'
1098
- }))
1099
- .mockRejectedValueOnce(new axios_1.AxiosError('Service Unavailable', 'ERR_NETWORK', undefined, undefined, {
1100
- config: { headers: new axios_1.AxiosHeaders() },
1101
- data: null,
1102
- headers: {},
1103
- status: 503,
1104
- statusText: 'Service Unavailable'
1105
- }))
1106
- .mockResolvedValueOnce({
1107
- data: { buildId: 'build-1', status: sabs_types_1.BuildStatus.SUCCESS, created: new Date(), updated: new Date() }
1108
- });
1109
- const sabs = new sabs_1.SabsClient('http://localhost:3000', {
1110
- maxRetries: 3,
1111
- initialDelayMs: 1000,
1112
- maxDelayMs: 1500,
1113
- sleep: (ms) => {
1114
- sleepCalls.push(ms);
1115
- return Promise.resolve();
1116
- }
1117
- });
1118
- await sabs.status({ buildId: 'anyBuildId', accessToken: anyAccessToken });
1119
- // attempt 1: 1000 * 2 * jitter(0.5–1.0) = 1000–2000, capped at 1500
1120
- expect(sleepCalls).toHaveLength(2);
1121
- expect(sleepCalls[1]).toBeLessThanOrEqual(1500);
1122
- });
1123
- test('does not retry CreateBuild POST (non-idempotent)', async () => {
1124
- const mockAxios = jest.spyOn(axios_1.default, 'request');
1125
- mockAxios.mockRejectedValue(new axios_1.AxiosError('Service Unavailable', 'ERR_NETWORK', undefined, undefined, {
1126
- config: { headers: new axios_1.AxiosHeaders() },
1127
- data: null,
1128
- headers: {},
1129
- status: 503,
1130
- statusText: 'Service Unavailable'
1131
- }));
1132
- const sabs = new sabs_1.SabsClient('http://localhost:3000', {
1133
- maxRetries: 2,
1134
- sleep: () => Promise.resolve()
1135
- });
1136
- await expect(sabs.build({
1137
- directoryHash: 'anyHash',
1138
- meta: new sabs_types_1.ApplicationMetadata({ id: 'anyAppId', organizationId: 'anyOrgId' }),
1139
- buildKey: anyBuildKey,
1140
- accessToken: anyAccessToken
1141
- })).rejects.toThrow();
1142
- expect(mockAxios).toHaveBeenCalledTimes(1);
1143
- });
1144
- test('retries terminate POST on network error (idempotent)', async () => {
1145
- const mockAxios = jest.spyOn(axios_1.default, 'request');
1146
- mockAxios
1147
- .mockRejectedValueOnce(new axios_1.AxiosError('ECONNREFUSED', 'ECONNREFUSED', undefined, undefined, undefined))
1148
- .mockResolvedValueOnce({ data: { buildId: 'build-1', status: sabs_types_1.BuildStatus.SUCCESS } });
1149
- const sabs = new sabs_1.SabsClient('http://localhost:3000', {
1150
- maxRetries: 2,
1151
- sleep: () => Promise.resolve()
1152
- });
1153
- const result = await sabs.terminate({
1154
- buildId: 'build-1',
1155
- status: sabs_types_1.BuildStatus.SUCCESS,
1156
- buildKey: anyBuildKey,
1157
- accessToken: anyAccessToken
1158
- });
1159
- expect(result.buildId).toBe('build-1');
1160
- expect(mockAxios).toHaveBeenCalledTimes(2);
1161
- });
1162
- test('retries terminate POST on 5xx (idempotent)', async () => {
1163
- const mockAxios = jest.spyOn(axios_1.default, 'request');
1164
- mockAxios
1165
- .mockRejectedValueOnce(new axios_1.AxiosError('Service Unavailable', 'ERR_NETWORK', undefined, undefined, {
1166
- config: { headers: new axios_1.AxiosHeaders() },
1167
- data: null,
1168
- headers: {},
1169
- status: 503,
1170
- statusText: 'Service Unavailable'
1171
- }))
1172
- .mockResolvedValueOnce({ data: { buildId: 'build-1', status: sabs_types_1.BuildStatus.SUCCESS } });
1173
- const sabs = new sabs_1.SabsClient('http://localhost:3000', {
1174
- maxRetries: 2,
1175
- sleep: () => Promise.resolve()
1176
- });
1177
- const result = await sabs.terminate({
1178
- buildId: 'build-1',
1179
- status: sabs_types_1.BuildStatus.SUCCESS,
1180
- buildKey: anyBuildKey,
1181
- accessToken: anyAccessToken
1182
- });
1183
- expect(result.buildId).toBe('build-1');
1184
- expect(mockAxios).toHaveBeenCalledTimes(2);
1185
- });
1186
- });
1187
- describe('executeRequest', () => {
1188
- test('normalizes request-header hook errors', async () => {
1189
- const mockAxios = jest.spyOn(axios_1.default, 'request');
1190
- const sabs = new sabs_1.SabsClient('http://localhost:3000', {
1191
- requestHeaders: () => {
1192
- throw new Error('Trace context unavailable');
1193
- }
1194
- });
1195
- await expect(sabs.status({ buildId: 'anyBuildId', accessToken: anyAccessToken })).rejects.toBeInstanceOf(errors_1.BadRequestError);
1196
- await expect(sabs.status({ buildId: 'anyBuildId', accessToken: anyAccessToken })).rejects.toThrow('SABS Client Error: Request header hook failed: Trace context unavailable');
1197
- expect(mockAxios).not.toHaveBeenCalled();
1198
- });
1199
- test('raies expected error when error is axios error', async () => {
1200
- const mockAxios = jest.spyOn(axios_1.default, 'request');
1201
- mockAxios.mockRejectedValue(new axios_1.AxiosError('request failed', 'internal', undefined, undefined, {
1202
- headers: {},
1203
- config: {
1204
- headers: new axios_1.AxiosHeaders()
1205
- },
1206
- status: 500,
1207
- statusText: 'internal server error',
1208
- data: 'failed to process request'
1209
- }));
1210
- const anyBuildId = 'anyBuildId';
1211
- const sabs = new sabs_1.SabsClient('http://localhost:3000', { maxRetries: 0 });
1212
- await expect(sabs.status({ buildId: anyBuildId, accessToken: anyAccessToken })).rejects.toThrow();
1213
- });
1214
- test('re-raises error when error is not axios error', async () => {
1215
- const mockAxios = jest.spyOn(axios_1.default, 'request');
1216
- mockAxios.mockRejectedValue(new Error('unexpected error'));
1217
- const anyBuildId = 'anyBuildId';
1218
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
1219
- await expect(sabs.status({ buildId: anyBuildId, accessToken: anyAccessToken })).rejects.toThrow();
1220
- });
1221
- test('uses custom logger for network errors', async () => {
1222
- const mockAxios = jest.spyOn(axios_1.default, 'request');
1223
- mockAxios.mockRejectedValue(new axios_1.AxiosError('ECONNREFUSED', 'ECONNREFUSED', undefined, undefined, undefined));
1224
- const loggedArgs = [];
1225
- const sabs = new sabs_1.SabsClient('http://localhost:3000', {
1226
- maxRetries: 0,
1227
- logger: { error: (...args) => loggedArgs.push(args) }
1228
- });
1229
- await expect(sabs.status({ buildId: 'anyBuildId', accessToken: anyAccessToken })).rejects.toThrow();
1230
- expect(loggedArgs).toHaveLength(1);
1231
- expect(loggedArgs[0][0]).toBe('SABS client: network error (no HTTP response)');
1232
- });
1233
- });
1234
- describe('backward compatibility', () => {
1235
- test('errors can be caught as generic Error instances (backward compatibility)', async () => {
1236
- const mockAxios = jest.spyOn(axios_1.default, 'request');
1237
- mockAxios.mockRejectedValue(new axios_1.AxiosError('Service unavailable', 'ECONNREFUSED', undefined, undefined, {
1238
- headers: {},
1239
- config: {
1240
- headers: new axios_1.AxiosHeaders()
1241
- },
1242
- status: 503,
1243
- statusText: 'Service Unavailable',
1244
- data: { message: 'Server temporarily unavailable' }
1245
- }));
1246
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
1247
- const applicationId = 'test-app';
1248
- const organizationId = 'test-org';
1249
- const directoryHash = 'abc123';
1250
- // This simulates how existing applications might handle errors
1251
- try {
1252
- const result = await sabs.build({
1253
- directoryHash,
1254
- meta: new sabs_types_1.ApplicationMetadata({
1255
- id: applicationId,
1256
- organizationId
1257
- }),
1258
- buildKey: anyBuildKey,
1259
- accessToken: anyAccessToken
1260
- });
1261
- // Should not reach here
1262
- expect(result).toBeUndefined();
1263
- }
1264
- catch (error) {
1265
- // Legacy error handling - should still work with new error types
1266
- expect(error).toBeInstanceOf(Error);
1267
- expect(error.message).toContain('SABS API Error (503)');
1268
- // Verify we can still access basic Error properties
1269
- expect(typeof error.message).toBe('string');
1270
- expect(error.name).toBeDefined();
1271
- // This is how existing code might log and re-throw
1272
- const loggedError = {
1273
- error,
1274
- applicationId,
1275
- organizationId,
1276
- directoryHash,
1277
- message: error.message
1278
- };
1279
- expect(loggedError.error).toBe(error);
1280
- expect(loggedError.message).toBe(error.message);
1281
- // Re-throwing as generic Error should work
1282
- expect(() => {
1283
- throw new Error('Unable to launch build');
1284
- }).toThrow('Unable to launch build');
1285
- }
1286
- });
1287
- test('new error types provide additional functionality when accessed', async () => {
1288
- const mockAxios = jest.spyOn(axios_1.default, 'request');
1289
- mockAxios.mockRejectedValue(new axios_1.AxiosError('Unauthorized', 'UNAUTHORIZED', undefined, undefined, {
1290
- headers: {},
1291
- config: {
1292
- headers: new axios_1.AxiosHeaders()
1293
- },
1294
- status: 401,
1295
- statusText: 'Unauthorized',
1296
- data: { message: 'Invalid token' }
1297
- }));
1298
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
1299
- try {
1300
- await sabs.status({ buildId: 'test-build', accessToken: 'invalid-token' });
1301
- }
1302
- catch (error) {
1303
- // Backward compatible - can still catch as Error
1304
- expect(error).toBeInstanceOf(Error);
1305
- // But applications can now also check for specific error types
1306
- const { UnauthorizedError } = await Promise.resolve().then(() => __importStar(require('./errors')));
1307
- expect(error).toBeInstanceOf(UnauthorizedError);
1308
- // And access the status code if needed
1309
- if ('status' in error) {
1310
- expect(error.status).toBe(401);
1311
- }
1312
- }
1313
- });
1314
- describe('response data handling', () => {
1315
- test('handles responseData with message property', async () => {
1316
- const mockAxios = jest.spyOn(axios_1.default, 'request');
1317
- mockAxios.mockRejectedValue(new axios_1.AxiosError('Bad Request', 'BAD_REQUEST', undefined, undefined, {
1318
- headers: {},
1319
- config: {
1320
- headers: new axios_1.AxiosHeaders()
1321
- },
1322
- status: 400,
1323
- statusText: 'Bad Request',
1324
- data: { message: 'Directory hash is required' }
1325
- }));
1326
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
1327
- try {
1328
- await sabs.build({
1329
- directoryHash: 'test', // Use non-empty value to avoid client-side validation
1330
- meta: new sabs_types_1.ApplicationMetadata({ id: 'test', organizationId: 'test' }),
1331
- buildKey: 'test',
1332
- accessToken: 'test'
1333
- });
1334
- }
1335
- catch (error) {
1336
- expect(error).toBeInstanceOf(Error);
1337
- expect(error.message).toBe('SABS API Error (400): Directory hash is required');
1338
- }
1339
- });
1340
- test('handles responseData as string', async () => {
1341
- const mockAxios = jest.spyOn(axios_1.default, 'request');
1342
- mockAxios.mockRejectedValue(new axios_1.AxiosError('Internal Server Error', 'INTERNAL_SERVER_ERROR', undefined, undefined, {
1343
- headers: {},
1344
- config: {
1345
- headers: new axios_1.AxiosHeaders()
1346
- },
1347
- status: 500,
1348
- statusText: 'Internal Server Error',
1349
- data: 'Database connection failed'
1350
- }));
1351
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
1352
- try {
1353
- await sabs.build({
1354
- directoryHash: 'test',
1355
- meta: new sabs_types_1.ApplicationMetadata({ id: 'test', organizationId: 'test' }),
1356
- buildKey: 'test',
1357
- accessToken: 'test'
1358
- });
1359
- }
1360
- catch (error) {
1361
- expect(error).toBeInstanceOf(Error);
1362
- expect(error.message).toBe('SABS API Error (500): Database connection failed');
1363
- }
1364
- });
1365
- test('handles null responseData', async () => {
1366
- const mockAxios = jest.spyOn(axios_1.default, 'request');
1367
- mockAxios.mockRejectedValue(new axios_1.AxiosError('Service Unavailable', 'SERVICE_UNAVAILABLE', undefined, undefined, {
1368
- headers: {},
1369
- config: {
1370
- headers: new axios_1.AxiosHeaders()
1371
- },
1372
- status: 503,
1373
- statusText: 'Service Unavailable',
1374
- data: null
1375
- }));
1376
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
1377
- try {
1378
- await sabs.build({
1379
- directoryHash: 'test',
1380
- meta: new sabs_types_1.ApplicationMetadata({ id: 'test', organizationId: 'test' }),
1381
- buildKey: 'test',
1382
- accessToken: 'test'
1383
- });
1384
- }
1385
- catch (error) {
1386
- expect(error).toBeInstanceOf(Error);
1387
- expect(error.message).toBe('SABS API Error (503): Service Unavailable (503)');
1388
- }
1389
- });
1390
- test('handles undefined responseData', async () => {
1391
- const mockAxios = jest.spyOn(axios_1.default, 'request');
1392
- mockAxios.mockRejectedValue(new axios_1.AxiosError('Gateway Timeout', 'GATEWAY_TIMEOUT', undefined, undefined, {
1393
- headers: {},
1394
- config: {
1395
- headers: new axios_1.AxiosHeaders()
1396
- },
1397
- status: 504,
1398
- statusText: 'Gateway Timeout',
1399
- data: undefined
1400
- }));
1401
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
1402
- try {
1403
- await sabs.build({
1404
- directoryHash: 'test',
1405
- meta: new sabs_types_1.ApplicationMetadata({ id: 'test', organizationId: 'test' }),
1406
- buildKey: 'test',
1407
- accessToken: 'test'
1408
- });
1409
- }
1410
- catch (error) {
1411
- expect(error).toBeInstanceOf(Error);
1412
- expect(error.message).toBe('SABS API Error (504): Gateway Timeout (504)');
1413
- }
1414
- });
1415
- test('handles responseData object without message property', async () => {
1416
- const mockAxios = jest.spyOn(axios_1.default, 'request');
1417
- mockAxios.mockRejectedValue(new axios_1.AxiosError('Bad Request', 'BAD_REQUEST', undefined, undefined, {
1418
- headers: {},
1419
- config: {
1420
- headers: new axios_1.AxiosHeaders()
1421
- },
1422
- status: 400,
1423
- statusText: 'Bad Request',
1424
- data: { error: 'validation_failed', details: ['field is required'] }
1425
- }));
1426
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
1427
- try {
1428
- await sabs.build({
1429
- directoryHash: 'test',
1430
- meta: new sabs_types_1.ApplicationMetadata({ id: 'test', organizationId: 'test' }),
1431
- buildKey: 'test',
1432
- accessToken: 'test'
1433
- });
1434
- }
1435
- catch (error) {
1436
- expect(error).toBeInstanceOf(Error);
1437
- expect(error.message).toBe('SABS API Error (400): Bad Request (400)');
1438
- }
1439
- });
1440
- test('all errors have consistent HttpError interface', async () => {
1441
- const responseData = { code: 'TIMEOUT', retryAfter: 30 };
1442
- const mockAxios = jest.spyOn(axios_1.default, 'request');
1443
- mockAxios.mockRejectedValue(new axios_1.AxiosError('Request timeout', 'TIMEOUT', undefined, undefined, {
1444
- headers: {},
1445
- config: {
1446
- headers: new axios_1.AxiosHeaders()
1447
- },
1448
- status: 500,
1449
- statusText: 'Internal Server Error',
1450
- data: responseData
1451
- }));
1452
- const sabs = new sabs_1.SabsClient('http://localhost:3000');
1453
- try {
1454
- await sabs.build({
1455
- directoryHash: 'test',
1456
- meta: new sabs_types_1.ApplicationMetadata({ id: 'test', organizationId: 'test' }),
1457
- buildKey: 'test',
1458
- accessToken: 'test'
1459
- });
1460
- }
1461
- catch (error) {
1462
- expect(error).toBeInstanceOf(Error);
1463
- expect(error.message).toBe('SABS API Error (500): Internal Server Error (500)');
1464
- // All error types now have consistent HttpError interface
1465
- expect('status' in error).toBe(true);
1466
- expect('title' in error).toBe(true);
1467
- if ('status' in error) {
1468
- expect(error.status).toBe(500);
1469
- }
1470
- if ('title' in error) {
1471
- expect(error.title).toBe('Internal server error');
1472
- }
1473
- }
1474
- });
1475
- });
1476
- });
1477
- });
1478
- //# sourceMappingURL=sabs.test.js.map