11import { http , HttpResponse } from 'msw' ;
22import { setupServer } from 'msw/node' ;
3- import { BaseClient , RequestMethod } from '../../src/api-client/base-client' ;
3+ import {
4+ BaseClient ,
5+ GENERIC_ERROR_MESSAGE ,
6+ RequestMethod
7+ } from '../../src/api-client/base-client' ;
48import { Routes } from '../../src/types/routes.types' ;
59import { getSdkIdentifier } from '../../src/utils/version' ;
10+ import { GalileoAPIError } from '../../src/types/errors.types' ;
11+ import type { GalileoAPIStandardErrorData } from '../../src/types/errors.types' ;
612
713// Test implementation of BaseClient
814class TestClient extends BaseClient {
@@ -18,28 +24,26 @@ class TestClient extends BaseClient {
1824 }
1925}
2026
21- describe ( 'BaseClient Headers' , ( ) => {
22- let capturedHeaders : Record < string , string > = { } ;
23-
24- const server = setupServer (
25- http . get ( 'http://localhost:8088/healthcheck' , ( { request } ) => {
26- // Capture headers from the request
27- capturedHeaders = { } ;
28- request . headers . forEach ( ( value , key ) => {
29- capturedHeaders [ key ] = value ;
30- } ) ;
31-
32- return HttpResponse . json ( { status : 'ok' } ) ;
33- } )
34- ) ;
35-
36- beforeAll ( ( ) => server . listen ( ) ) ;
37- afterEach ( ( ) => {
38- server . resetHandlers ( ) ;
27+ let capturedHeaders : Record < string , string > = { } ;
28+
29+ const server = setupServer (
30+ http . get ( 'http://localhost:8088/healthcheck' , ( { request } ) => {
3931 capturedHeaders = { } ;
40- } ) ;
41- afterAll ( ( ) => server . close ( ) ) ;
32+ request . headers . forEach ( ( value , key ) => {
33+ capturedHeaders [ key ] = value ;
34+ } ) ;
35+ return HttpResponse . json ( { status : 'ok' } ) ;
36+ } )
37+ ) ;
4238
39+ beforeAll ( ( ) => server . listen ( ) ) ;
40+ afterEach ( ( ) => {
41+ server . resetHandlers ( ) ;
42+ capturedHeaders = { } ;
43+ } ) ;
44+ afterAll ( ( ) => server . close ( ) ) ;
45+
46+ describe ( 'BaseClient Headers' , ( ) => {
4347 it ( 'should include X-Galileo-SDK header with correct value' , async ( ) => {
4448 const client = new TestClient ( ) ;
4549
@@ -62,21 +66,8 @@ describe('BaseClient Headers', () => {
6266 } ) ;
6367
6468 it ( 'should include custom headers when provided' , async ( ) => {
65- const server = setupServer (
66- http . get ( 'http://localhost:8088/healthcheck' , ( { request } ) => {
67- capturedHeaders = { } ;
68- request . headers . forEach ( ( value , key ) => {
69- capturedHeaders [ key ] = value ;
70- } ) ;
71- return HttpResponse . json ( { status : 'ok' } ) ;
72- } )
73- ) ;
74-
75- server . listen ( ) ;
76-
7769 const client = new TestClient ( ) ;
7870
79- // Test with extra headers
8071 await client . makeRequest (
8172 RequestMethod . GET ,
8273 Routes . healthCheck ,
@@ -87,7 +78,253 @@ describe('BaseClient Headers', () => {
8778
8879 expect ( capturedHeaders [ 'custom-header' ] ) . toBe ( 'custom-value' ) ;
8980 expect ( capturedHeaders [ 'x-galileo-sdk' ] ) . toBe ( getSdkIdentifier ( ) ) ;
81+ } ) ;
82+ } ) ;
83+
84+ /**
85+ * Catalog-aligned minimal: Orbit 1006 "Resource not found" required fields only.
86+ */
87+ const VALID_STANDARD_ERROR : GalileoAPIStandardErrorData = {
88+ error_code : 1006 ,
89+ error_type : 'not_found_error' ,
90+ error_group : 'shared' ,
91+ severity : 'medium' ,
92+ message : 'The requested resource could not be found.' ,
93+ retriable : false ,
94+ blocking : false
95+ } ;
96+
97+ /**
98+ * Catalog-aligned full: Orbit 1006 with optional fields (dataset not found variant).
99+ */
100+ const FULL_STANDARD_ERROR : GalileoAPIStandardErrorData = {
101+ error_code : 1006 ,
102+ error_type : 'not_found_error' ,
103+ error_group : 'shared' ,
104+ severity : 'medium' ,
105+ message : 'Dataset with the given id was not found.' ,
106+ user_action : 'Verify the identifier and try again.' ,
107+ documentation_link : null ,
108+ retriable : false ,
109+ blocking : true ,
110+ http_status_code : 404 ,
111+ source_service : 'api' ,
112+ context : { dataset_id : 'ds-123' }
113+ } ;
114+
115+ describe ( 'BaseClient API error handling' , ( ) => {
116+ test ( 'test makeRequest throws GalileoAPIError when response has valid standard_error' , async ( ) => {
117+ server . use (
118+ http . get ( 'http://localhost:8088/healthcheck' , ( ) =>
119+ HttpResponse . json (
120+ { standard_error : VALID_STANDARD_ERROR } ,
121+ { status : 400 }
122+ )
123+ )
124+ ) ;
125+ const client = new TestClient ( ) ;
126+
127+ let err : unknown ;
128+ try {
129+ await client . testRequest ( ) ;
130+ } catch ( e ) {
131+ err = e ;
132+ }
133+ expect ( err ) . toBeInstanceOf ( GalileoAPIError ) ;
134+ const apiErr = err as GalileoAPIError ;
135+ expect ( apiErr . message ) . toBe ( VALID_STANDARD_ERROR . message ) ;
136+ expect ( apiErr . errorCode ) . toBe ( VALID_STANDARD_ERROR . error_code ) ;
137+ expect ( apiErr . retriable ) . toBe ( VALID_STANDARD_ERROR . retriable ) ;
138+ } ) ;
139+
140+ test ( 'test makeRequest throws generic parse error when standard_error is present but invalid' , async ( ) => {
141+ server . use (
142+ http . get ( 'http://localhost:8088/healthcheck' , ( ) =>
143+ HttpResponse . json ( { standard_error : { message : 'x' } } , { status : 400 } )
144+ )
145+ ) ;
146+ const client = new TestClient ( ) ;
147+
148+ await expect ( client . testRequest ( ) ) . rejects . toThrow (
149+ 'The API returned an error, but the details could not be parsed.'
150+ ) ;
151+ } ) ;
152+
153+ test ( 'test makeRequest throws generic parse error when standard_error is null' , async ( ) => {
154+ server . use (
155+ http . get ( 'http://localhost:8088/healthcheck' , ( ) =>
156+ HttpResponse . json ( { standard_error : null } , { status : 400 } )
157+ )
158+ ) ;
159+ const client = new TestClient ( ) ;
160+
161+ await expect ( client . testRequest ( ) ) . rejects . toThrow (
162+ 'The API returned an error, but the details could not be parsed.'
163+ ) ;
164+ } ) ;
165+
166+ test ( 'test makeRequest throws generic parse error when standard_error is non-object' , async ( ) => {
167+ server . use (
168+ http . get ( 'http://localhost:8088/healthcheck' , ( ) =>
169+ HttpResponse . json ( { standard_error : 'invalid' } , { status : 400 } )
170+ )
171+ ) ;
172+ const client = new TestClient ( ) ;
173+
174+ await expect ( client . testRequest ( ) ) . rejects . toThrow (
175+ 'The API returned an error, but the details could not be parsed.'
176+ ) ;
177+ } ) ;
178+
179+ test ( 'test makeRequest throws generic parse error when standard_error is empty object' , async ( ) => {
180+ server . use (
181+ http . get ( 'http://localhost:8088/healthcheck' , ( ) =>
182+ HttpResponse . json ( { standard_error : { } } , { status : 400 } )
183+ )
184+ ) ;
185+ const client = new TestClient ( ) ;
186+
187+ await expect ( client . testRequest ( ) ) . rejects . toThrow (
188+ / T h e A P I r e t u r n e d a n e r r o r , b u t t h e d e t a i l s c o u l d n o t b e p a r s e d \. /
189+ ) ;
190+ } ) ;
191+
192+ test ( 'test makeRequest throws generic parse error when standard_error is array' , async ( ) => {
193+ server . use (
194+ http . get ( 'http://localhost:8088/healthcheck' , ( ) =>
195+ HttpResponse . json ( { standard_error : [ ] } , { status : 400 } )
196+ )
197+ ) ;
198+ const client = new TestClient ( ) ;
199+
200+ await expect ( client . testRequest ( ) ) . rejects . toThrow (
201+ / T h e A P I r e t u r n e d a n e r r o r , b u t t h e d e t a i l s c o u l d n o t b e p a r s e d \. /
202+ ) ;
203+ } ) ;
204+
205+ test ( 'test makeRequest throws generic parse error when standard_error has invalid optional type' , async ( ) => {
206+ server . use (
207+ http . get ( 'http://localhost:8088/healthcheck' , ( ) =>
208+ HttpResponse . json (
209+ {
210+ standard_error : {
211+ ...VALID_STANDARD_ERROR ,
212+ documentation_link : 1
213+ }
214+ } ,
215+ { status : 400 }
216+ )
217+ )
218+ ) ;
219+ const client = new TestClient ( ) ;
220+
221+ await expect ( client . testRequest ( ) ) . rejects . toThrow (
222+ / T h e A P I r e t u r n e d a n e r r o r , b u t t h e d e t a i l s c o u l d n o t b e p a r s e d \. /
223+ ) ;
224+ } ) ;
225+
226+ test ( 'test makeRequest throws GENERIC_ERROR_MESSAGE when error body is null' , async ( ) => {
227+ server . use (
228+ http . get ( 'http://localhost:8088/healthcheck' , ( ) =>
229+ HttpResponse . json ( null , { status : 500 } )
230+ )
231+ ) ;
232+ const client = new TestClient ( ) ;
233+
234+ await expect ( client . testRequest ( ) ) . rejects . toThrow ( GENERIC_ERROR_MESSAGE ) ;
235+ } ) ;
236+
237+ test ( 'test makeRequest throws GENERIC_ERROR_MESSAGE when error body is string' , async ( ) => {
238+ server . use (
239+ http . get ( 'http://localhost:8088/healthcheck' , ( ) =>
240+ HttpResponse . json ( 'error' , { status : 500 } )
241+ )
242+ ) ;
243+ const client = new TestClient ( ) ;
244+
245+ await expect ( client . testRequest ( ) ) . rejects . toThrow ( GENERIC_ERROR_MESSAGE ) ;
246+ } ) ;
247+
248+ test ( 'test makeRequest throws GalileoAPIError with all mapped properties when response has valid standard_error' , async ( ) => {
249+ server . use (
250+ http . get ( 'http://localhost:8088/healthcheck' , ( ) =>
251+ HttpResponse . json (
252+ { standard_error : FULL_STANDARD_ERROR } ,
253+ { status : 400 }
254+ )
255+ )
256+ ) ;
257+ const client = new TestClient ( ) ;
258+
259+ let err : unknown ;
260+ try {
261+ await client . testRequest ( ) ;
262+ } catch ( e ) {
263+ err = e ;
264+ }
265+ expect ( err ) . toBeInstanceOf ( GalileoAPIError ) ;
266+ const apiErr = err as GalileoAPIError ;
267+ expect ( apiErr . message ) . toBe ( FULL_STANDARD_ERROR . message ) ;
268+ expect ( apiErr . errorCode ) . toBe ( FULL_STANDARD_ERROR . error_code ) ;
269+ expect ( apiErr . errorType ) . toBe ( FULL_STANDARD_ERROR . error_type ) ;
270+ expect ( apiErr . errorGroup ) . toBe ( FULL_STANDARD_ERROR . error_group ) ;
271+ expect ( apiErr . severity ) . toBe ( FULL_STANDARD_ERROR . severity ) ;
272+ expect ( apiErr . userAction ) . toBe ( FULL_STANDARD_ERROR . user_action ) ;
273+ expect ( apiErr . documentationLink ) . toBe (
274+ FULL_STANDARD_ERROR . documentation_link
275+ ) ;
276+ expect ( apiErr . retriable ) . toBe ( FULL_STANDARD_ERROR . retriable ) ;
277+ expect ( apiErr . blocking ) . toBe ( FULL_STANDARD_ERROR . blocking ) ;
278+ expect ( apiErr . httpStatusCode ) . toBe ( FULL_STANDARD_ERROR . http_status_code ) ;
279+ expect ( apiErr . sourceService ) . toBe ( FULL_STANDARD_ERROR . source_service ) ;
280+ expect ( apiErr . context ) . toEqual ( FULL_STANDARD_ERROR . context ) ;
281+ } ) ;
282+
283+ test ( 'test makeRequest throws with detail message when detail is string and statusCode present' , async ( ) => {
284+ server . use (
285+ http . get ( 'http://localhost:8088/healthcheck' , ( ) =>
286+ HttpResponse . json ( { detail : 'Validation failed' } , { status : 422 } )
287+ )
288+ ) ;
289+ const client = new TestClient ( ) ;
290+
291+ await expect ( client . testRequest ( ) ) . rejects . toThrow (
292+ / n o n - o k s t a t u s c o d e 4 2 2 w i t h o u t p u t : V a l i d a t i o n f a i l e d /
293+ ) ;
294+ } ) ;
295+
296+ test ( 'test makeRequest throws with detail message when detail is validation array' , async ( ) => {
297+ server . use (
298+ http . get ( 'http://localhost:8088/healthcheck' , ( ) =>
299+ HttpResponse . json (
300+ {
301+ detail : [
302+ {
303+ loc : [ 'body' , 'x' ] ,
304+ msg : 'field required' ,
305+ type : 'value_error'
306+ }
307+ ]
308+ } ,
309+ { status : 422 }
310+ )
311+ )
312+ ) ;
313+ const client = new TestClient ( ) ;
314+
315+ await expect ( client . testRequest ( ) ) . rejects . toThrow (
316+ / n o n - o k s t a t u s c o d e 4 2 2 w i t h o u t p u t : f i e l d r e q u i r e d /
317+ ) ;
318+ } ) ;
319+
320+ test ( 'test makeRequest throws GENERIC_ERROR_MESSAGE when error body has neither standard_error nor detail' , async ( ) => {
321+ server . use (
322+ http . get ( 'http://localhost:8088/healthcheck' , ( ) =>
323+ HttpResponse . json ( { } , { status : 500 } )
324+ )
325+ ) ;
326+ const client = new TestClient ( ) ;
90327
91- server . close ( ) ;
328+ await expect ( client . testRequest ( ) ) . rejects . toThrow ( GENERIC_ERROR_MESSAGE ) ;
92329 } ) ;
93330} ) ;
0 commit comments