@thorprovider/adapters 4.3.0 → 5.0.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.
@@ -19,7 +19,6 @@ import type {
19
19
  CreateAddressData,
20
20
  PasswordResetRequest,
21
21
  PasswordResetConfirm,
22
- LinkCustomerToChannelResponse,
23
22
  } from '@thorprovider/types';
24
23
  import { createLogger } from '../../core/logger';
25
24
  import { ProviderAPIError } from '../../core/errors';
@@ -121,18 +120,12 @@ export class MedusaAuthProvider implements AuthProvider {
121
120
  private logger: ReturnType<typeof createLogger>;
122
121
  private authConfig: AuthConfig;
123
122
  private storageManager: AuthStorageManager;
124
- /** Medusa backend base URL — needed for raw fetch calls */
125
- private baseUrl: string;
126
- /** Publishable API key — sent as x-publishable-api-key on store calls */
127
- private publishableKey: string;
128
123
 
129
- constructor(sdk: Medusa, authConfig: AuthConfig, debug = false, baseUrl = '', publishableKey = '') {
124
+ constructor(sdk: Medusa, authConfig: AuthConfig, debug = false) {
130
125
  this.sdk = sdk;
131
126
  this.authConfig = authConfig;
132
127
  this.logger = createLogger('MedusaAuthProvider', debug);
133
128
  this.storageManager = new AuthStorageManager(authConfig);
134
- this.baseUrl = baseUrl;
135
- this.publishableKey = publishableKey;
136
129
 
137
130
  this.logger.info('Medusa Auth Provider initialized', {
138
131
  method: authConfig.method,
@@ -146,35 +139,51 @@ export class MedusaAuthProvider implements AuthProvider {
146
139
 
147
140
  async login(credentials: LoginCredentials): Promise<AuthResponse> {
148
141
  this.logger.debug('login', { email: credentials.email });
149
-
142
+
150
143
  try {
151
144
  // Step 1: Authenticate with Medusa
152
- const token = await this.sdk.auth.login('customer', 'emailpass', {
145
+ const loginResult = await this.sdk.auth.login('customer', 'emailpass', {
153
146
  email: credentials.email,
154
147
  password: credentials.password,
155
148
  });
156
-
157
- // Check if token requires additional actions (OAuth flow)
158
- if (typeof token !== 'string') {
159
- this.logger.warn('Authentication requires additional actions', token);
160
- return {
161
- requiresAction: true,
162
- redirectUrl: token.location,
163
- };
149
+
150
+ // Check if login requires additional actions (OAuth or email verification)
151
+ if (typeof loginResult !== 'string') {
152
+ // v2.16.0: email verification required
153
+ if ('verification_required' in loginResult && loginResult.verification_required) {
154
+ this.logger.warn('Authentication requires email verification', loginResult);
155
+ return {
156
+ requiresAction: true,
157
+ token: loginResult.token,
158
+ };
159
+ }
160
+
161
+ // OAuth redirect flow
162
+ if ('location' in loginResult) {
163
+ this.logger.warn('Authentication requires additional actions', loginResult);
164
+ return {
165
+ requiresAction: true,
166
+ redirectUrl: loginResult.location,
167
+ };
168
+ }
169
+
170
+ throw new Error('Unexpected login result type');
164
171
  }
165
-
172
+
173
+ const token = loginResult;
174
+
166
175
  // Step 2: Store token based on auth method
167
176
  if (this.authConfig.method === 'jwt') {
168
177
  await this.storageManager.setToken(token);
169
178
  }
170
179
  // Note: For session-based auth, the SDK automatically handles cookies
171
180
  // when auth.login() is called with auth.type='session' in SDK config
172
-
181
+
173
182
  // Step 3: Fetch customer data
174
183
  const customer = await this.getCurrentCustomer();
175
-
184
+
176
185
  this.logger.info('Login successful', { customerId: customer?.id });
177
-
186
+
178
187
  return {
179
188
  token: this.authConfig.method === 'jwt' ? token : undefined,
180
189
  customer: customer ?? undefined,
@@ -191,36 +200,64 @@ export class MedusaAuthProvider implements AuthProvider {
191
200
 
192
201
  async register(data: RegisterData): Promise<AuthResponse> {
193
202
  this.logger.debug('register', { email: data.email });
194
-
203
+
195
204
  try {
196
205
  // Step 1: Register auth identity with Medusa
197
- const token = await this.sdk.auth.register('customer', 'emailpass', {
206
+ // v2.16.0: register no longer accepts returnVerification option.
207
+ // The SDK auto-stores the registration JWT token for subsequent requests.
208
+ await this.sdk.auth.register('customer', 'emailpass', {
198
209
  email: data.email,
199
210
  password: data.password,
200
211
  });
201
-
202
- if (typeof token !== 'string') {
203
- throw new Error('Registration returned non-string token');
204
- }
205
-
212
+
206
213
  // Step 2: Create customer profile
207
- // Note: The SDK automatically includes the token in the header after auth.register()
214
+ // The registration token is automatically included in the header by the SDK.
208
215
  const { customer } = await this.sdk.store.customer.create({
209
216
  email: data.email,
210
217
  first_name: data.first_name,
211
218
  last_name: data.last_name,
212
219
  phone: data.phone,
213
220
  });
214
-
215
- // Step 3: Store token based on auth method
221
+
222
+ // Step 3: Login to obtain the actual session/token
223
+ // v2.16.0: verification status is now returned by login, not register.
224
+ const loginResult = await this.sdk.auth.login('customer', 'emailpass', {
225
+ email: data.email,
226
+ password: data.password,
227
+ });
228
+
229
+ if (typeof loginResult !== 'string') {
230
+ // v2.16.0: email verification required before full login
231
+ if ('verification_required' in loginResult && loginResult.verification_required) {
232
+ this.logger.warn('Registration requires email verification', loginResult);
233
+ return {
234
+ requiresAction: true,
235
+ token: loginResult.token,
236
+ };
237
+ }
238
+
239
+ // OAuth redirect flow
240
+ if ('location' in loginResult) {
241
+ this.logger.warn('Registration requires additional actions', loginResult);
242
+ return {
243
+ requiresAction: true,
244
+ redirectUrl: loginResult.location,
245
+ };
246
+ }
247
+
248
+ throw new Error('Unexpected login result after registration');
249
+ }
250
+
251
+ const token = loginResult;
252
+
253
+ // Step 4: Store actual auth token based on auth method
216
254
  if (this.authConfig.method === 'jwt') {
217
255
  await this.storageManager.setToken(token);
218
256
  }
219
257
  // Note: For session-based auth, the SDK automatically handles cookies
220
- // when auth.register() is called with auth.type='session' in SDK config
221
-
258
+
222
259
  this.logger.info('Registration successful', { customerId: customer.id });
223
-
260
+
224
261
  return {
225
262
  token: this.authConfig.method === 'jwt' ? token : undefined,
226
263
  customer: transformCustomer(customer, customer.email),
@@ -500,92 +537,4 @@ export class MedusaAuthProvider implements AuthProvider {
500
537
  undefined
501
538
  );
502
539
  }
503
-
504
- // ========================================
505
- // Multi-Tenant — Channel Link
506
- // ========================================
507
-
508
- /**
509
- * Link the currently authenticated customer to the storefront's sales channel.
510
- *
511
- * Must be called immediately after a successful `login()` so the customer
512
- * appears in the admin panel of the correct storefront channel.
513
- *
514
- * Implements `POST /store/thor/customers/me/channels`.
515
- * Idempotent — returns `{ linked: false, reason: 'already_linked' }` when
516
- * the link already exists.
517
- *
518
- * @throws {ProviderAPIError} When not authenticated (401) or channel cannot
519
- * be resolved from the publishable key (400).
520
- */
521
- async linkCustomerToChannel(): Promise<LinkCustomerToChannelResponse> {
522
- this.logger.debug('linkCustomerToChannel');
523
-
524
- if (!this.baseUrl || !this.publishableKey) {
525
- throw new ProviderAPIError(
526
- 'linkCustomerToChannel requires baseUrl and publishableKey to be set on the auth provider.',
527
- 'CONFIGURATION_ERROR',
528
- undefined
529
- );
530
- }
531
-
532
- // Retrieve the JWT token for the Authorization header.
533
- // Falls back to the SDK-managed token (session or JWT storage).
534
- const token = await this.storageManager.getToken();
535
-
536
- const headers: Record<string, string> = {
537
- 'x-publishable-api-key': this.publishableKey,
538
- 'Content-Type': 'application/json',
539
- };
540
-
541
- if (token) {
542
- headers['Authorization'] = `Bearer ${token}`;
543
- }
544
-
545
- try {
546
- const url = `${this.baseUrl}/store/thor/customers/me/channels`;
547
- const response = await fetch(url, {
548
- method: 'POST',
549
- headers,
550
- });
551
-
552
- if (response.status === 401) {
553
- throw new ProviderAPIError(
554
- 'linkCustomerToChannel: not authenticated. Call login() first.',
555
- 'NOT_AUTHENTICATED',
556
- 401
557
- );
558
- }
559
-
560
- if (response.status === 400) {
561
- const body = await response.json().catch(() => ({}));
562
- throw new ProviderAPIError(
563
- `linkCustomerToChannel: could not resolve sales channel from publishable key. ${body?.message ?? ''}`.trim(),
564
- 'CHANNEL_RESOLUTION_FAILED',
565
- 400
566
- );
567
- }
568
-
569
- if (!response.ok) {
570
- const body = await response.json().catch(() => ({}));
571
- throw new ProviderAPIError(
572
- `linkCustomerToChannel failed with status ${response.status}: ${body?.message ?? 'Unknown error'}`,
573
- 'LINK_CHANNEL_FAILED',
574
- response.status
575
- );
576
- }
577
-
578
- const data = await response.json() as LinkCustomerToChannelResponse;
579
- this.logger.info('Customer linked to channel', { linked: data.linked, reason: data.reason });
580
- return data;
581
- } catch (error: any) {
582
- if (error instanceof ProviderAPIError) throw error;
583
- this.logger.error('linkCustomerToChannel failed', error);
584
- throw new ProviderAPIError(
585
- `linkCustomerToChannel failed: ${error.message}`,
586
- 'LINK_CHANNEL_FAILED',
587
- error
588
- );
589
- }
590
- }
591
540
  }
@@ -0,0 +1,392 @@
1
+ /**
2
+ * Integration tests for MedusaProvider (main storefront provider).
3
+ *
4
+ * Uses msw to intercept HTTP requests — no real backend required.
5
+ * Follows the same pattern as auth.test.ts and admin.test.ts.
6
+ *
7
+ * Coverage:
8
+ * - Products: getProduct, getProducts (success + 404 + 503)
9
+ * - Cart: createCart, getCart (success + 404), addToCart
10
+ * - Shipping: getShippingMethods
11
+ * - Regions: getRegions
12
+ * - Checkout: getPaymentMethods, completeCheckout
13
+ */
14
+ import { describe, expect, it } from 'vitest'
15
+ import { http, HttpResponse } from 'msw'
16
+ import { server } from '../stelorder/client.test-helpers'
17
+ import { MedusaProvider } from './index'
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Test constants
21
+ // ---------------------------------------------------------------------------
22
+ const BASE = 'https://medusa.example.com'
23
+ const PUB_KEY = 'pk_test_abc123'
24
+ const CURRENCY = 'usd'
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Factory
28
+ // ---------------------------------------------------------------------------
29
+ function makeProvider() {
30
+ return new MedusaProvider({
31
+ baseUrl: BASE,
32
+ publishableKey: PUB_KEY,
33
+ currencyCode: CURRENCY,
34
+ debug: false,
35
+ })
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Minimal Medusa response fixtures
40
+ // ---------------------------------------------------------------------------
41
+ const MEDUSA_PRODUCT = {
42
+ id: 'prod_01',
43
+ handle: 'test-product',
44
+ title: 'Test Product',
45
+ subtitle: null,
46
+ description: 'A test product',
47
+ status: 'published',
48
+ thumbnail: 'https://example.com/thumb.jpg',
49
+ images: [{ id: 'img_01', url: 'https://example.com/img.jpg', metadata: null }],
50
+ tags: [],
51
+ collection: null,
52
+ categories: [],
53
+ options: [{ id: 'opt_01', title: 'Size', product_id: 'prod_01', values: [{ id: 'optval_01', value: 'One Size', option_id: 'opt_01' }] }],
54
+ variants: [
55
+ {
56
+ id: 'variant_01',
57
+ title: 'Default',
58
+ product_id: 'prod_01',
59
+ sku: 'SKU-001',
60
+ inventory_quantity: 10,
61
+ prices: [{ id: 'price_01', amount: 2500, currency_code: 'usd' }],
62
+ calculated_price: { calculated_amount: 2500, currency_code: 'usd' },
63
+ options: [{ id: 'optval_01', value: 'One Size' }],
64
+ },
65
+ ],
66
+ metadata: null,
67
+ created_at: '2026-01-01T00:00:00Z',
68
+ updated_at: '2026-01-01T00:00:00Z',
69
+ }
70
+
71
+ const MEDUSA_CART = {
72
+ id: 'cart_01',
73
+ currency_code: 'usd',
74
+ region_id: 'reg_01',
75
+ region: { id: 'reg_01', currency_code: 'usd' },
76
+ items: [],
77
+ total: 0,
78
+ subtotal: 0,
79
+ tax_total: 0,
80
+ shipping_total: 0,
81
+ discount_total: 0,
82
+ shipping_address: null,
83
+ billing_address: null,
84
+ shipping_methods: [],
85
+ payment_collection: null,
86
+ metadata: null,
87
+ created_at: '2026-01-01T00:00:00Z',
88
+ updated_at: '2026-01-01T00:00:00Z',
89
+ }
90
+
91
+ const MEDUSA_REGION = {
92
+ id: 'reg_01',
93
+ name: 'North America',
94
+ currency_code: 'usd',
95
+ countries: [{ iso_2: 'us', display_name: 'United States', name: 'UNITED STATES', iso_3: 'usa', num_code: '840', region_id: 'reg_01', metadata: null, created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z', deleted_at: null }],
96
+ created_at: '2026-01-01T00:00:00Z',
97
+ updated_at: '2026-01-01T00:00:00Z',
98
+ deleted_at: null,
99
+ metadata: null,
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Products
104
+ // ---------------------------------------------------------------------------
105
+ describe('MedusaProvider — Products', () => {
106
+ it('getProduct returns transformed product when found', async () => {
107
+ server.use(
108
+ http.get(`${BASE}/store/products`, () =>
109
+ HttpResponse.json({ products: [MEDUSA_PRODUCT], count: 1, offset: 0, limit: 1 }),
110
+ ),
111
+ )
112
+
113
+ const provider = makeProvider()
114
+ const product = await provider.getProduct('test-product')
115
+
116
+ expect(product).not.toBeUndefined()
117
+ expect(product!.id).toBe('prod_01')
118
+ expect(product!.handle).toBe('test-product')
119
+ expect(product!.title).toBe('Test Product')
120
+ expect(product!.variants).toHaveLength(1)
121
+ })
122
+
123
+ it('getProduct returns undefined when not found', async () => {
124
+ server.use(
125
+ http.get(`${BASE}/store/products`, () =>
126
+ HttpResponse.json({ products: [], count: 0, offset: 0, limit: 1 }),
127
+ ),
128
+ )
129
+
130
+ const provider = makeProvider()
131
+ const product = await provider.getProduct('does-not-exist')
132
+
133
+ expect(product).toBeUndefined()
134
+ })
135
+
136
+ it('getProduct throws ProviderAPIError on 503', async () => {
137
+ server.use(
138
+ http.get(`${BASE}/store/products`, () =>
139
+ HttpResponse.json({ message: 'Service unavailable' }, { status: 503 }),
140
+ ),
141
+ )
142
+
143
+ const provider = makeProvider()
144
+
145
+ await expect(provider.getProduct('any-handle')).rejects.toThrow()
146
+ })
147
+
148
+ it('getProducts returns array of transformed products', async () => {
149
+ server.use(
150
+ http.get(`${BASE}/store/products`, () =>
151
+ HttpResponse.json({ products: [MEDUSA_PRODUCT], count: 1, offset: 0, limit: 100 }),
152
+ ),
153
+ )
154
+
155
+ const provider = makeProvider()
156
+ const products = await provider.getProducts({ first: 10 })
157
+
158
+ expect(Array.isArray(products)).toBe(true)
159
+ expect(products).toHaveLength(1)
160
+ expect(products[0].handle).toBe('test-product')
161
+ })
162
+
163
+ it('getProducts returns empty array when backend returns none', async () => {
164
+ server.use(
165
+ http.get(`${BASE}/store/products`, () =>
166
+ HttpResponse.json({ products: [], count: 0, offset: 0, limit: 100 }),
167
+ ),
168
+ )
169
+
170
+ const provider = makeProvider()
171
+ const products = await provider.getProducts()
172
+
173
+ expect(products).toHaveLength(0)
174
+ })
175
+ })
176
+
177
+ // ---------------------------------------------------------------------------
178
+ // Cart
179
+ // ---------------------------------------------------------------------------
180
+ describe('MedusaProvider — Cart', () => {
181
+ it('createCart returns a cart with an id', async () => {
182
+ server.use(
183
+ http.post(`${BASE}/store/carts`, () =>
184
+ HttpResponse.json({ cart: MEDUSA_CART }),
185
+ ),
186
+ )
187
+
188
+ const provider = makeProvider()
189
+ const cart = await provider.createCart()
190
+
191
+ expect(cart.id).toBe('cart_01')
192
+ })
193
+
194
+ it('createCart with regionId passes region_id in payload', async () => {
195
+ server.use(
196
+ http.post(`${BASE}/store/carts`, async ({ request }) => {
197
+ const body = await request.json() as any
198
+ expect(body.region_id).toBe('reg_test')
199
+ return HttpResponse.json({ cart: { ...MEDUSA_CART, region_id: 'reg_test' } })
200
+ }),
201
+ )
202
+
203
+ const provider = makeProvider()
204
+ const cart = await provider.createCart({ regionId: 'reg_test' })
205
+
206
+ expect(cart.id).toBe('cart_01')
207
+ })
208
+
209
+ it('getCart returns cart when found', async () => {
210
+ server.use(
211
+ http.get(`${BASE}/store/carts/cart_01`, () =>
212
+ HttpResponse.json({ cart: MEDUSA_CART }),
213
+ ),
214
+ )
215
+
216
+ const provider = makeProvider()
217
+ const cart = await provider.getCart('cart_01')
218
+
219
+ expect(cart).not.toBeUndefined()
220
+ expect(cart!.id).toBe('cart_01')
221
+ })
222
+
223
+ it('getCart returns undefined when cart not found (404)', async () => {
224
+ server.use(
225
+ http.get(`${BASE}/store/carts/cart_missing`, () =>
226
+ HttpResponse.json({ message: 'Not found' }, { status: 404 }),
227
+ ),
228
+ )
229
+
230
+ const provider = makeProvider()
231
+ const cart = await provider.getCart('cart_missing')
232
+
233
+ expect(cart).toBeUndefined()
234
+ })
235
+
236
+ it('createCart throws ProviderAPIError on server error', async () => {
237
+ server.use(
238
+ http.post(`${BASE}/store/carts`, () =>
239
+ HttpResponse.json({ message: 'Internal Server Error' }, { status: 500 }),
240
+ ),
241
+ )
242
+
243
+ const provider = makeProvider()
244
+
245
+ await expect(provider.createCart()).rejects.toThrow()
246
+ })
247
+ })
248
+
249
+ // ---------------------------------------------------------------------------
250
+ // Regions
251
+ // ---------------------------------------------------------------------------
252
+ describe('MedusaProvider — Regions', () => {
253
+ it('getRegions returns array of regions', async () => {
254
+ server.use(
255
+ http.get(`${BASE}/store/regions`, () =>
256
+ HttpResponse.json({ regions: [MEDUSA_REGION], count: 1, offset: 0, limit: 100 }),
257
+ ),
258
+ )
259
+
260
+ const provider = makeProvider()
261
+ const regions = await provider.getRegions()
262
+
263
+ expect(Array.isArray(regions)).toBe(true)
264
+ expect(regions).toHaveLength(1)
265
+ expect(regions[0].id).toBe('reg_01')
266
+ expect(regions[0].name).toBe('North America')
267
+ })
268
+
269
+ it('getRegions returns empty array when no regions', async () => {
270
+ server.use(
271
+ http.get(`${BASE}/store/regions`, () =>
272
+ HttpResponse.json({ regions: [], count: 0, offset: 0, limit: 100 }),
273
+ ),
274
+ )
275
+
276
+ const provider = makeProvider()
277
+ const regions = await provider.getRegions()
278
+
279
+ expect(regions).toHaveLength(0)
280
+ })
281
+ })
282
+
283
+ // ---------------------------------------------------------------------------
284
+ // Shipping
285
+ // getShippingMethods first retrieves the cart (to get region_id), then fetches
286
+ // shipping options for that cart via GET /store/shipping-options?cart_id=...
287
+ // ---------------------------------------------------------------------------
288
+ describe('MedusaProvider — Shipping', () => {
289
+ const CART_WITH_REGION = { ...MEDUSA_CART, region_id: 'reg_01' }
290
+
291
+ const MEDUSA_SHIPPING_OPTION = {
292
+ id: 'so_01',
293
+ name: 'Standard Shipping',
294
+ amount: 499,
295
+ price_type: 'flat',
296
+ provider_id: 'manual',
297
+ service_zone: { id: 'sz_01', name: 'Default' },
298
+ }
299
+
300
+ it('getShippingMethods returns available methods', async () => {
301
+ server.use(
302
+ // Step 1: cart retrieve
303
+ http.get(`${BASE}/store/carts/cart_01`, () =>
304
+ HttpResponse.json({ cart: CART_WITH_REGION }),
305
+ ),
306
+ // Step 2: shipping options
307
+ http.get(`${BASE}/store/shipping-options`, () =>
308
+ HttpResponse.json({ shipping_options: [MEDUSA_SHIPPING_OPTION] }),
309
+ ),
310
+ )
311
+
312
+ const provider = makeProvider()
313
+ const methods = await provider.getShippingMethods('cart_01')
314
+
315
+ expect(Array.isArray(methods)).toBe(true)
316
+ expect(methods).toHaveLength(1)
317
+ expect(methods[0].id).toBe('so_01')
318
+ expect(methods[0].name).toBe('Standard Shipping')
319
+ })
320
+
321
+ it('getShippingMethods returns empty array when none available', async () => {
322
+ server.use(
323
+ http.get(`${BASE}/store/carts/cart_01`, () =>
324
+ HttpResponse.json({ cart: CART_WITH_REGION }),
325
+ ),
326
+ http.get(`${BASE}/store/shipping-options`, () =>
327
+ HttpResponse.json({ shipping_options: [] }),
328
+ ),
329
+ )
330
+
331
+ const provider = makeProvider()
332
+ const methods = await provider.getShippingMethods('cart_01')
333
+
334
+ expect(methods).toHaveLength(0)
335
+ })
336
+ })
337
+
338
+ // ---------------------------------------------------------------------------
339
+ // Payment methods
340
+ // getPaymentMethods fetches the cart first (to get region_id), then calls
341
+ // GET /store/payment-providers?region_id=...
342
+ // ---------------------------------------------------------------------------
343
+ describe('MedusaProvider — Payment', () => {
344
+ it('getPaymentMethods returns payment providers', async () => {
345
+ server.use(
346
+ // Step 1: cart retrieve to get region_id
347
+ http.get(`${BASE}/store/carts/cart_01`, () =>
348
+ HttpResponse.json({ cart: { ...MEDUSA_CART, region_id: 'reg_01' } }),
349
+ ),
350
+ // Step 2: payment providers
351
+ http.get(`${BASE}/store/payment-providers`, () =>
352
+ HttpResponse.json({
353
+ payment_providers: [{ id: 'pp_manual', name: 'Manual', is_enabled: true }],
354
+ }),
355
+ ),
356
+ )
357
+
358
+ const provider = makeProvider()
359
+ const methods = await provider.getPaymentMethods({ cartId: 'cart_01' })
360
+
361
+ expect(Array.isArray(methods)).toBe(true)
362
+ })
363
+ })
364
+
365
+ // ---------------------------------------------------------------------------
366
+ // Error propagation
367
+ // ---------------------------------------------------------------------------
368
+ describe('MedusaProvider — Error propagation', () => {
369
+ it('getProducts throws when backend returns 503', async () => {
370
+ server.use(
371
+ http.get(`${BASE}/store/products`, () =>
372
+ HttpResponse.json({ message: 'Service Unavailable' }, { status: 503 }),
373
+ ),
374
+ )
375
+
376
+ const provider = makeProvider()
377
+
378
+ await expect(provider.getProducts()).rejects.toThrow()
379
+ })
380
+
381
+ it('createCart throws when backend returns 500', async () => {
382
+ server.use(
383
+ http.post(`${BASE}/store/carts`, () =>
384
+ HttpResponse.json({ message: 'Internal Server Error' }, { status: 500 }),
385
+ ),
386
+ )
387
+
388
+ const provider = makeProvider()
389
+
390
+ await expect(provider.createCart()).rejects.toThrow()
391
+ })
392
+ })