@thorprovider/medusa-extended 1.0.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.
@@ -0,0 +1,404 @@
1
+ /**
2
+ * @fileoverview Tests for MedusaAdminClient
3
+ *
4
+ * Uses msw to intercept fetch calls and verify that each method:
5
+ * - Hits the correct URL and HTTP method
6
+ * - Sends `x-medusa-access-token`
7
+ * - Forwards query-string options correctly
8
+ * - Returns the parsed response body
9
+ * - Throws ProviderAPIError on non-2xx responses
10
+ */
11
+
12
+ import { describe, expect, it } from 'vitest'
13
+ import { http, HttpResponse } from 'msw'
14
+ import { server } from '../stelorder/client.test-helpers'
15
+ import { MedusaAdminClient } from './admin'
16
+
17
+ const BASE = 'https://medusa.example.com'
18
+ const API_KEY = 'test-admin-key'
19
+
20
+ function client() {
21
+ return new MedusaAdminClient({ baseUrl: BASE, apiKey: API_KEY })
22
+ }
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Helpers
26
+ // ---------------------------------------------------------------------------
27
+
28
+ function requireAdminKey(request: Request) {
29
+ expect(request.headers.get('x-medusa-access-token')).toBe(API_KEY)
30
+ }
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Sales Channel — Customers
34
+ // ---------------------------------------------------------------------------
35
+
36
+ describe('MedusaAdminClient.getChannelCustomers', () => {
37
+ it('GET /admin/thor/sales-channels/:id/customers with defaults', async () => {
38
+ server.use(
39
+ http.get(`${BASE}/admin/thor/sales-channels/sc_01/customers`, ({ request }) => {
40
+ requireAdminKey(request)
41
+ const url = new URL(request.url)
42
+ expect(url.searchParams.get('limit')).toBe('20')
43
+ expect(url.searchParams.get('offset')).toBe('0')
44
+ return HttpResponse.json({ customers: [{ id: 'cus_1', email: 'a@b.com', sales_channel_ids: ['sc_01'] }], count: 1 })
45
+ }),
46
+ )
47
+
48
+ const result = await client().getChannelCustomers('sc_01')
49
+ expect(result.customers).toHaveLength(1)
50
+ expect(result.count).toBe(1)
51
+ })
52
+
53
+ it('forwards custom limit/offset', async () => {
54
+ server.use(
55
+ http.get(`${BASE}/admin/thor/sales-channels/sc_01/customers`, ({ request }) => {
56
+ const url = new URL(request.url)
57
+ expect(url.searchParams.get('limit')).toBe('5')
58
+ expect(url.searchParams.get('offset')).toBe('10')
59
+ return HttpResponse.json({ customers: [], count: 0 })
60
+ }),
61
+ )
62
+
63
+ await client().getChannelCustomers('sc_01', { limit: 5, offset: 10 })
64
+ })
65
+ })
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Sales Channel — API Keys
69
+ // ---------------------------------------------------------------------------
70
+
71
+ describe('MedusaAdminClient.getChannelApiKeys', () => {
72
+ it('GET /admin/thor/sales-channels/:id/api-keys', async () => {
73
+ server.use(
74
+ http.get(`${BASE}/admin/thor/sales-channels/sc_01/api-keys`, ({ request }) => {
75
+ requireAdminKey(request)
76
+ return HttpResponse.json({ api_keys: [{ id: 'apk_1', token: 'pk_live_xxx', sales_channel_id: 'sc_01' }] })
77
+ }),
78
+ )
79
+
80
+ const result = await client().getChannelApiKeys('sc_01')
81
+ expect(result.api_keys).toHaveLength(1)
82
+ expect(result.api_keys[0].id).toBe('apk_1')
83
+ })
84
+ })
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // Sales Channel — Categories
88
+ // ---------------------------------------------------------------------------
89
+
90
+ describe('MedusaAdminClient.getChannelCategories', () => {
91
+ it('GET /admin/thor/sales-channels/:id/categories with include_descendants', async () => {
92
+ server.use(
93
+ http.get(`${BASE}/admin/thor/sales-channels/sc_01/categories`, ({ request }) => {
94
+ const url = new URL(request.url)
95
+ expect(url.searchParams.get('include_descendants')).toBe('true')
96
+ return HttpResponse.json({ categories: [], count: 0 })
97
+ }),
98
+ )
99
+
100
+ const result = await client().getChannelCategories('sc_01', { include_descendants: true })
101
+ expect(result.categories).toHaveLength(0)
102
+ })
103
+ })
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Category — Sales Channels
107
+ // ---------------------------------------------------------------------------
108
+
109
+ describe('MedusaAdminClient.getCategorySalesChannels', () => {
110
+ it('GET /admin/thor/categories/:id/sales-channels', async () => {
111
+ server.use(
112
+ http.get(`${BASE}/admin/thor/categories/cat_01/sales-channels`, ({ request }) => {
113
+ requireAdminKey(request)
114
+ return HttpResponse.json({ sales_channels: [{ id: 'sc_01', name: 'Main' }] })
115
+ }),
116
+ )
117
+
118
+ const result = await client().getCategorySalesChannels('cat_01')
119
+ expect(result.sales_channels).toHaveLength(1)
120
+ })
121
+ })
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // Category — Assign / Unassign
125
+ // ---------------------------------------------------------------------------
126
+
127
+ describe('MedusaAdminClient.assignCategoryToChannels', () => {
128
+ it('POST /admin/thor/categories/:id/sales-channels', async () => {
129
+ server.use(
130
+ http.post(`${BASE}/admin/thor/categories/cat_01/sales-channels`, async ({ request }) => {
131
+ requireAdminKey(request)
132
+ const body = await request.clone().json() as any
133
+ expect(body.sales_channel_ids).toEqual(['sc_01'])
134
+ return HttpResponse.json({ message: 'ok', linked: 1 })
135
+ }),
136
+ )
137
+
138
+ const result = await client().assignCategoryToChannels('cat_01', { sales_channel_ids: ['sc_01'] })
139
+ expect(result.linked).toBe(1)
140
+ })
141
+ })
142
+
143
+ describe('MedusaAdminClient.unassignCategoryFromChannels', () => {
144
+ it('DELETE /admin/thor/categories/:id/sales-channels with body', async () => {
145
+ server.use(
146
+ http.delete(`${BASE}/admin/thor/categories/cat_01/sales-channels`, async ({ request }) => {
147
+ requireAdminKey(request)
148
+ const body = await request.clone().json() as any
149
+ expect(body.sales_channel_ids).toEqual(['sc_01'])
150
+ return HttpResponse.json({ message: 'ok', dismissed: 1 })
151
+ }),
152
+ )
153
+
154
+ const result = await client().unassignCategoryFromChannels('cat_01', { sales_channel_ids: ['sc_01'] })
155
+ expect(result.dismissed).toBe(1)
156
+ })
157
+ })
158
+
159
+ // ---------------------------------------------------------------------------
160
+ // Collection — Sales Channels
161
+ // ---------------------------------------------------------------------------
162
+
163
+ describe('MedusaAdminClient.getCollectionSalesChannels', () => {
164
+ it('GET /admin/thor/collections/:id/sales-channels', async () => {
165
+ server.use(
166
+ http.get(`${BASE}/admin/thor/collections/col_01/sales-channels`, ({ request }) => {
167
+ requireAdminKey(request)
168
+ return HttpResponse.json({ sales_channels: [{ id: 'sc_01', name: 'Main' }] })
169
+ }),
170
+ )
171
+
172
+ const result = await client().getCollectionSalesChannels('col_01')
173
+ expect(result.sales_channels).toHaveLength(1)
174
+ })
175
+ })
176
+
177
+ describe('MedusaAdminClient.assignCollectionToChannels', () => {
178
+ it('POST /admin/thor/collections/:id/sales-channels', async () => {
179
+ server.use(
180
+ http.post(`${BASE}/admin/thor/collections/col_01/sales-channels`, async ({ request }) => {
181
+ const body = await request.clone().json() as any
182
+ expect(body.sales_channel_ids).toEqual(['sc_01'])
183
+ return HttpResponse.json({ message: 'ok', linked: 1 })
184
+ }),
185
+ )
186
+
187
+ const result = await client().assignCollectionToChannels('col_01', { sales_channel_ids: ['sc_01'] })
188
+ expect(result.linked).toBe(1)
189
+ })
190
+ })
191
+
192
+ describe('MedusaAdminClient.unassignCollectionFromChannels', () => {
193
+ it('DELETE /admin/thor/collections/:id/sales-channels', async () => {
194
+ server.use(
195
+ http.delete(`${BASE}/admin/thor/collections/col_01/sales-channels`, async ({ request }) => {
196
+ const body = await request.clone().json() as any
197
+ expect(body.sales_channel_ids).toEqual(['sc_01'])
198
+ return HttpResponse.json({ message: 'ok', dismissed: 1 })
199
+ }),
200
+ )
201
+
202
+ const result = await client().unassignCollectionFromChannels('col_01', { sales_channel_ids: ['sc_01'] })
203
+ expect(result.dismissed).toBe(1)
204
+ })
205
+ })
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // Storefront Config
209
+ // ---------------------------------------------------------------------------
210
+
211
+ describe('MedusaAdminClient.getStorefrontConfig', () => {
212
+ it('GET /admin/thor/storefront-config/:id', async () => {
213
+ server.use(
214
+ http.get(`${BASE}/admin/thor/storefront-config/sc_01`, ({ request }) => {
215
+ requireAdminKey(request)
216
+ return HttpResponse.json({
217
+ storefront_config: {
218
+ id: 'scfg_01',
219
+ sales_channel_id: 'sc_01',
220
+ theme_accent_color: '#000',
221
+ logo_url: '',
222
+ currency_code: 'USD',
223
+ seo_defaults: { title: 'Store', description: '' },
224
+ created_at: '2026-01-01T00:00:00Z',
225
+ updated_at: '2026-01-01T00:00:00Z',
226
+ },
227
+ })
228
+ }),
229
+ )
230
+
231
+ const result = await client().getStorefrontConfig('sc_01')
232
+ expect(result.storefront_config.id).toBe('scfg_01')
233
+ })
234
+ })
235
+
236
+ describe('MedusaAdminClient.updateStorefrontConfig', () => {
237
+ it('PUT /admin/thor/storefront-config/:id', async () => {
238
+ server.use(
239
+ http.put(`${BASE}/admin/thor/storefront-config/sc_01`, async ({ request }) => {
240
+ const body = await request.clone().json() as any
241
+ expect(body.theme_accent_color).toBe('#ff0000')
242
+ return HttpResponse.json({
243
+ storefront_config: {
244
+ id: 'scfg_01',
245
+ sales_channel_id: 'sc_01',
246
+ theme_accent_color: '#ff0000',
247
+ logo_url: '',
248
+ currency_code: 'USD',
249
+ seo_defaults: { title: 'Store', description: '' },
250
+ created_at: '2026-01-01T00:00:00Z',
251
+ updated_at: '2026-01-02T00:00:00Z',
252
+ },
253
+ })
254
+ }),
255
+ )
256
+
257
+ const result = await client().updateStorefrontConfig('sc_01', {
258
+ theme_accent_color: '#ff0000',
259
+ logo_url: '',
260
+ currency_code: 'USD',
261
+ seo_defaults: { title: 'Store', description: '' },
262
+ })
263
+ expect(result.storefront_config.theme_accent_color).toBe('#ff0000')
264
+ })
265
+ })
266
+
267
+ // ---------------------------------------------------------------------------
268
+ // Site Config
269
+ // ---------------------------------------------------------------------------
270
+
271
+ describe('MedusaAdminClient.getSiteConfig', () => {
272
+ it('GET /admin/thor/site-config/:sales_channel_id', async () => {
273
+ server.use(
274
+ http.get(`${BASE}/admin/thor/site-config/sc_01`, ({ request }) => {
275
+ requireAdminKey(request)
276
+ return HttpResponse.json({
277
+ site_config: {
278
+ id: 'scfg_01',
279
+ sales_channel_id: 'sc_01',
280
+ version: 'v1',
281
+ config: { theme: 'dark' },
282
+ created_at: '2026-01-01T00:00:00Z',
283
+ updated_at: '2026-01-01T00:00:00Z',
284
+ history: [],
285
+ },
286
+ })
287
+ }),
288
+ )
289
+
290
+ const result = await client().getSiteConfig('sc_01')
291
+ expect(result.site_config.id).toBe('scfg_01')
292
+ })
293
+ })
294
+
295
+ describe('MedusaAdminClient.updateSiteConfig', () => {
296
+ it('PUT /admin/thor/site-config/:sales_channel_id', async () => {
297
+ server.use(
298
+ http.put(`${BASE}/admin/thor/site-config/sc_01`, async ({ request }) => {
299
+ const body = await request.json() as any
300
+ expect(body.config).toEqual({ theme: 'light' })
301
+ return HttpResponse.json({
302
+ site_config: {
303
+ id: 'scfg_02',
304
+ sales_channel_id: 'sc_01',
305
+ version: 'v2',
306
+ config: { theme: 'light' },
307
+ created_at: '2026-01-01T00:00:00Z',
308
+ updated_at: '2026-01-02T00:00:00Z',
309
+ history: [],
310
+ },
311
+ })
312
+ }),
313
+ )
314
+
315
+ const result = await client().updateSiteConfig('sc_01', { config: { theme: 'light' } })
316
+ expect(result.site_config.version).toBe('v2')
317
+ })
318
+ })
319
+
320
+ describe('MedusaAdminClient.getSiteConfigHistory', () => {
321
+ it('GET /admin/thor/site-config/:id/history with defaults', async () => {
322
+ server.use(
323
+ http.get(`${BASE}/admin/thor/site-config/sc_01/history`, ({ request }) => {
324
+ requireAdminKey(request)
325
+ const url = new URL(request.url)
326
+ expect(url.searchParams.get('limit')).toBe('20')
327
+ expect(url.searchParams.get('offset')).toBe('0')
328
+ return HttpResponse.json({ history: [], count: 0 })
329
+ }),
330
+ )
331
+
332
+ const result = await client().getSiteConfigHistory('sc_01')
333
+ expect(result.count).toBe(0)
334
+ })
335
+
336
+ it('forwards custom limit/offset', async () => {
337
+ server.use(
338
+ http.get(`${BASE}/admin/thor/site-config/sc_01/history`, ({ request }) => {
339
+ const url = new URL(request.url)
340
+ expect(url.searchParams.get('limit')).toBe('5')
341
+ expect(url.searchParams.get('offset')).toBe('10')
342
+ return HttpResponse.json({ history: [], count: 0 })
343
+ }),
344
+ )
345
+
346
+ await client().getSiteConfigHistory('sc_01', { limit: 5, offset: 10 })
347
+ })
348
+ })
349
+
350
+ describe('MedusaAdminClient.restoreSiteConfig', () => {
351
+ it('POST /admin/thor/site-config/:id/restore/:version', async () => {
352
+ server.use(
353
+ http.post(`${BASE}/admin/thor/site-config/sc_01/restore/v1`, ({ request }) => {
354
+ requireAdminKey(request)
355
+ return HttpResponse.json({
356
+ site_config: {
357
+ id: 'scfg_03',
358
+ sales_channel_id: 'sc_01',
359
+ version: 'v3',
360
+ config: { theme: 'dark' },
361
+ created_at: '2026-01-01T00:00:00Z',
362
+ updated_at: '2026-01-03T00:00:00Z',
363
+ history: [],
364
+ },
365
+ })
366
+ }),
367
+ )
368
+
369
+ const result = await client().restoreSiteConfig('sc_01', 'v1')
370
+ expect(result.site_config.version).toBe('v3')
371
+ })
372
+ })
373
+
374
+ // ---------------------------------------------------------------------------
375
+ // Error handling
376
+ // ---------------------------------------------------------------------------
377
+
378
+ describe('MedusaAdminClient error handling', () => {
379
+ it('throws ProviderAPIError on 404', async () => {
380
+ server.use(
381
+ http.get(`${BASE}/admin/thor/sales-channels/sc_missing/customers`, () =>
382
+ HttpResponse.json({ message: 'Sales channel not found' }, { status: 404 }),
383
+ ),
384
+ )
385
+
386
+ await expect(client().getChannelCustomers('sc_missing')).rejects.toMatchObject({
387
+ name: 'ProviderAPIError',
388
+ provider: 'ADMIN_API_404',
389
+ })
390
+ })
391
+
392
+ it('throws ProviderAPIError on 401', async () => {
393
+ server.use(
394
+ http.get(`${BASE}/admin/thor/sales-channels/sc_01/api-keys`, () =>
395
+ HttpResponse.json({ message: 'Unauthorized' }, { status: 401 }),
396
+ ),
397
+ )
398
+
399
+ await expect(client().getChannelApiKeys('sc_01')).rejects.toMatchObject({
400
+ name: 'ProviderAPIError',
401
+ provider: 'ADMIN_API_401',
402
+ })
403
+ })
404
+ })