@thorprovider/adapters 5.0.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.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +6 -0
- package/README.md +53 -631
- package/dist/index.d.mts +406 -194
- package/dist/index.d.ts +406 -194
- package/dist/index.js +3092 -604
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3081 -602
- package/dist/index.mjs.map +1 -1
- package/docs/RETRY_SOLUTION_SUMMARY.md +1 -1
- package/docs/authentication.md +0 -26
- package/docs/capabilities-system.md +66 -36
- package/docs/getting-started.md +1 -1
- package/docs/providers-overview.md +16 -13
- package/docs/update-documentation.md +10 -8
- package/package.json +6 -5
- package/quick-reference.md +0 -33
- package/src/backend/factory.ts +11 -1
- package/src/factory/index.ts +52 -74
- package/src/index.ts +17 -3
- package/src/providers/bigcommerce/bigcommerce.ts +6 -0
- package/src/providers/bigcommerce/index.ts +315 -0
- package/src/providers/bigcommerce/transforms.ts +227 -0
- package/src/providers/index.ts +16 -1
- package/src/providers/magento/index.ts +387 -0
- package/src/providers/magento/magento.ts +5 -0
- package/src/providers/magento/queries.ts +267 -0
- package/src/providers/magento/transforms.ts +249 -0
- package/src/providers/medusa/auth.test.ts +94 -92
- package/src/providers/medusa/auth.ts +72 -123
- package/src/providers/medusa/index.test.ts +392 -0
- package/src/providers/medusa/index.ts +1 -21
- package/src/providers/medusa/live-smoke.test.ts +412 -0
- package/src/providers/shopify/auth.ts +402 -0
- package/src/providers/shopify/index.ts +444 -0
- package/src/providers/shopify/mutations.ts +247 -0
- package/src/providers/shopify/queries.ts +324 -0
- package/src/providers/shopify/shopify.ts +7 -0
- package/src/providers/shopify/transforms.ts +275 -0
- package/src/providers/spree/index.ts +344 -0
- package/src/providers/spree/spree.ts +5 -0
- package/src/providers/spree/transforms.ts +254 -0
- package/src/providers/woocommerce/index.ts +310 -0
- package/src/providers/woocommerce/transforms.ts +254 -0
- package/src/providers/woocommerce/woocommerce.ts +5 -0
- package/src/types/capabilities.types.ts +250 -0
- package/vitest.live.config.ts +18 -0
|
@@ -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
|
|
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
|
|
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
|
|
158
|
-
if (typeof
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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:
|
|
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
|
-
|
|
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
|
+
})
|