@doswiftly/storefront-sdk 4.4.0 → 4.5.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.
Files changed (89) hide show
  1. package/dist/core/cart/types.d.ts +53 -20
  2. package/dist/core/cart/types.d.ts.map +1 -1
  3. package/dist/core/cart/types.js +3 -0
  4. package/dist/core/operations/cart.d.ts +15 -9
  5. package/dist/core/operations/cart.d.ts.map +1 -1
  6. package/dist/core/operations/cart.js +130 -58
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.js +1 -1
  9. package/package.json +9 -4
  10. package/src/__tests__/contract/storefront-api.contract.test.ts +0 -450
  11. package/src/__tests__/unit/auth-client.test.ts +0 -210
  12. package/src/__tests__/unit/bot-protection.test.ts +0 -461
  13. package/src/__tests__/unit/cart-client.test.ts +0 -233
  14. package/src/__tests__/unit/cart-store.test.ts +0 -349
  15. package/src/__tests__/unit/create-client.test.ts +0 -356
  16. package/src/__tests__/unit/helpers.test.ts +0 -377
  17. package/src/__tests__/unit/middleware.test.ts +0 -374
  18. package/src/__tests__/unit/test-helpers.ts +0 -103
  19. package/src/core/auth/auth-client.ts +0 -123
  20. package/src/core/auth/cookie-config.ts +0 -23
  21. package/src/core/auth/handlers.ts +0 -168
  22. package/src/core/auth/routes.ts +0 -26
  23. package/src/core/auth/token-client.ts +0 -51
  24. package/src/core/auth/types.ts +0 -54
  25. package/src/core/bot-protection/abstract-manager.ts +0 -185
  26. package/src/core/bot-protection/create-manager.ts +0 -37
  27. package/src/core/bot-protection/eucaptcha-manager.ts +0 -88
  28. package/src/core/bot-protection/fallback-manager.ts +0 -43
  29. package/src/core/bot-protection/turnstile-manager.ts +0 -92
  30. package/src/core/bot-protection/types/eucaptcha.d.ts +0 -28
  31. package/src/core/bot-protection/types/turnstile.d.ts +0 -33
  32. package/src/core/cache.ts +0 -102
  33. package/src/core/cart/cart-client.ts +0 -150
  34. package/src/core/cart/cookie-config.ts +0 -13
  35. package/src/core/cart/types.ts +0 -104
  36. package/src/core/client/compose.ts +0 -15
  37. package/src/core/client/create-client.ts +0 -129
  38. package/src/core/client/dedupe.ts +0 -19
  39. package/src/core/client/execute.ts +0 -70
  40. package/src/core/client/hash.ts +0 -21
  41. package/src/core/client/operation-name.ts +0 -12
  42. package/src/core/client/types.ts +0 -171
  43. package/src/core/currency/cookie-config.ts +0 -13
  44. package/src/core/errors.ts +0 -67
  45. package/src/core/format.ts +0 -254
  46. package/src/core/helpers/assert-no-user-errors.ts +0 -21
  47. package/src/core/helpers/normalize-connection.ts +0 -48
  48. package/src/core/helpers/sanitize-html.ts +0 -42
  49. package/src/core/image.ts +0 -22
  50. package/src/core/index.ts +0 -174
  51. package/src/core/language/cookie-config.ts +0 -13
  52. package/src/core/middleware/auth.ts +0 -27
  53. package/src/core/middleware/bot-protection.ts +0 -140
  54. package/src/core/middleware/currency.ts +0 -27
  55. package/src/core/middleware/errors.ts +0 -86
  56. package/src/core/middleware/language.ts +0 -30
  57. package/src/core/middleware/retry.ts +0 -75
  58. package/src/core/middleware/timeout.ts +0 -61
  59. package/src/core/operations/auth.ts +0 -123
  60. package/src/core/operations/cart.ts +0 -185
  61. package/src/index.ts +0 -25
  62. package/src/react/bot-protection/bot-protection-context.ts +0 -17
  63. package/src/react/bot-protection/bot-protection-widget.tsx +0 -46
  64. package/src/react/cookies.ts +0 -89
  65. package/src/react/helpers/create-store-context.ts +0 -56
  66. package/src/react/hooks/use-auth.ts +0 -218
  67. package/src/react/hooks/use-bot-protection.ts +0 -31
  68. package/src/react/hooks/use-cart-manager.ts +0 -236
  69. package/src/react/hooks/use-currency.ts +0 -23
  70. package/src/react/hooks/use-debounced-value.ts +0 -30
  71. package/src/react/hooks/use-hydrated.ts +0 -20
  72. package/src/react/hooks/use-storefront-client.ts +0 -12
  73. package/src/react/index.ts +0 -71
  74. package/src/react/providers/currency-provider.tsx +0 -30
  75. package/src/react/providers/language-provider.tsx +0 -34
  76. package/src/react/providers/storefront-client-provider.tsx +0 -107
  77. package/src/react/providers/storefront-provider.tsx +0 -99
  78. package/src/react/server/get-storefront-client.ts +0 -60
  79. package/src/react/server/index.ts +0 -1
  80. package/src/react/stores/auth.store.ts +0 -112
  81. package/src/react/stores/cart.context.ts +0 -10
  82. package/src/react/stores/cart.store.ts +0 -254
  83. package/src/react/stores/currency.store.ts +0 -93
  84. package/src/react/stores/index.ts +0 -17
  85. package/src/react/stores/language.store.ts +0 -90
  86. package/src/react/stores/store-context.tsx +0 -103
  87. package/src/react/types/shop-config.ts +0 -22
  88. package/tsconfig.json +0 -20
  89. package/vitest.config.ts +0 -14
@@ -1,461 +0,0 @@
1
- /**
2
- * Unit tests for bot protection: middleware, fallback manager, factory.
3
- *
4
- * Tests: botProtectionMiddleware, FallbackBotProtectionManager, createBotProtectionManager.
5
- */
6
-
7
- import { describe, it, expect, vi } from 'vitest';
8
- import {
9
- botProtectionMiddleware,
10
- BOT_PROTECTION_HEADER,
11
- type BotProtectionTokenProvider,
12
- } from '../../core/middleware/bot-protection';
13
- import { FallbackBotProtectionManager } from '../../core/bot-protection/fallback-manager';
14
- import { StorefrontError } from '../../core/errors';
15
- import type { GraphQLRequest, GraphQLResponse, ExecuteFn } from '../../core/client/types';
16
-
17
- // ---------------------------------------------------------------------------
18
- // Helpers
19
- // ---------------------------------------------------------------------------
20
-
21
- function makeRequest(overrides?: Partial<GraphQLRequest>): GraphQLRequest {
22
- return {
23
- query: '{ shop { id } }',
24
- headers: {},
25
- isMutation: false,
26
- ...overrides,
27
- };
28
- }
29
-
30
- function makeResponse(overrides?: Partial<GraphQLResponse>): GraphQLResponse {
31
- return {
32
- data: {},
33
- status: 200,
34
- headers: new Headers(),
35
- ...overrides,
36
- };
37
- }
38
-
39
- function makeNext(response?: GraphQLResponse): ExecuteFn {
40
- return vi.fn(async () => response ?? makeResponse());
41
- }
42
-
43
- function makeTokenProvider(
44
- token: string | null = 'test-token',
45
- options?: { delay?: number; shouldThrow?: boolean },
46
- ): BotProtectionTokenProvider {
47
- return {
48
- execute: vi.fn(async () => {
49
- if (options?.delay) {
50
- await new Promise((r) => setTimeout(r, options.delay));
51
- }
52
- if (options?.shouldThrow) {
53
- throw new Error('Provider error');
54
- }
55
- return token;
56
- }),
57
- destroy: vi.fn(),
58
- };
59
- }
60
-
61
- // ---------------------------------------------------------------------------
62
- // botProtectionMiddleware
63
- // ---------------------------------------------------------------------------
64
-
65
- describe('botProtectionMiddleware', () => {
66
- const protectedOperations = ['CustomerCreate', 'CheckoutComplete', 'ReviewCreate'];
67
-
68
- it('should skip queries (non-mutations)', async () => {
69
- const provider = makeTokenProvider();
70
- const mw = botProtectionMiddleware({
71
- tokenProvider: provider,
72
- protectedOperations,
73
- });
74
- const next = makeNext();
75
- const req = makeRequest({ isMutation: false, operationName: 'Shop' });
76
-
77
- await mw(req, next);
78
-
79
- expect(provider.execute).not.toHaveBeenCalled();
80
- expect(req.headers[BOT_PROTECTION_HEADER]).toBeUndefined();
81
- expect(next).toHaveBeenCalledWith(req);
82
- });
83
-
84
- it('should skip non-protected mutations', async () => {
85
- const provider = makeTokenProvider();
86
- const mw = botProtectionMiddleware({
87
- tokenProvider: provider,
88
- protectedOperations,
89
- });
90
- const next = makeNext();
91
- const req = makeRequest({
92
- isMutation: true,
93
- operationName: 'CartLinesAdd',
94
- });
95
-
96
- await mw(req, next);
97
-
98
- expect(provider.execute).not.toHaveBeenCalled();
99
- expect(req.headers[BOT_PROTECTION_HEADER]).toBeUndefined();
100
- expect(next).toHaveBeenCalledWith(req);
101
- });
102
-
103
- it('should skip mutations without operationName', async () => {
104
- const provider = makeTokenProvider();
105
- const mw = botProtectionMiddleware({
106
- tokenProvider: provider,
107
- protectedOperations,
108
- });
109
- const next = makeNext();
110
- const req = makeRequest({ isMutation: true }); // no operationName
111
-
112
- await mw(req, next);
113
-
114
- expect(provider.execute).not.toHaveBeenCalled();
115
- expect(next).toHaveBeenCalledWith(req);
116
- });
117
-
118
- it('should add token header for protected mutations', async () => {
119
- const provider = makeTokenProvider('fresh-token');
120
- const mw = botProtectionMiddleware({
121
- tokenProvider: provider,
122
- protectedOperations,
123
- });
124
- const next = makeNext();
125
- const req = makeRequest({
126
- isMutation: true,
127
- operationName: 'CustomerCreate',
128
- });
129
-
130
- await mw(req, next);
131
-
132
- expect(provider.execute).toHaveBeenCalledWith({
133
- action: 'CustomerCreate',
134
- timeoutMs: 10000,
135
- });
136
- expect(req.headers[BOT_PROTECTION_HEADER]).toBe('fresh-token');
137
- expect(next).toHaveBeenCalledWith(req);
138
- });
139
-
140
- it('should pass action matching operationName', async () => {
141
- const provider = makeTokenProvider('token');
142
- const mw = botProtectionMiddleware({
143
- tokenProvider: provider,
144
- protectedOperations,
145
- });
146
- const next = makeNext();
147
- const req = makeRequest({
148
- isMutation: true,
149
- operationName: 'CheckoutComplete',
150
- });
151
-
152
- await mw(req, next);
153
-
154
- expect(provider.execute).toHaveBeenCalledWith(
155
- expect.objectContaining({ action: 'CheckoutComplete' }),
156
- );
157
- });
158
-
159
- it('should fail-open by default when token is null', async () => {
160
- const provider = makeTokenProvider(null);
161
- const mw = botProtectionMiddleware({
162
- tokenProvider: provider,
163
- protectedOperations,
164
- });
165
- const next = makeNext();
166
- const req = makeRequest({
167
- isMutation: true,
168
- operationName: 'CustomerCreate',
169
- });
170
-
171
- await mw(req, next);
172
-
173
- // No header, but request passes through
174
- expect(req.headers[BOT_PROTECTION_HEADER]).toBeUndefined();
175
- expect(next).toHaveBeenCalledWith(req);
176
- });
177
-
178
- it('should throw on fail-closed when token is null', async () => {
179
- const provider = makeTokenProvider(null);
180
- const mw = botProtectionMiddleware({
181
- tokenProvider: provider,
182
- protectedOperations,
183
- defaultFailStrategy: 'closed',
184
- });
185
- const next = makeNext();
186
- const req = makeRequest({
187
- isMutation: true,
188
- operationName: 'CustomerCreate',
189
- });
190
-
191
- await expect(mw(req, next)).rejects.toThrow(StorefrontError);
192
- expect(next).not.toHaveBeenCalled();
193
- });
194
-
195
- it('should respect per-operation failStrategyOverrides', async () => {
196
- const provider = makeTokenProvider(null);
197
- const mw = botProtectionMiddleware({
198
- tokenProvider: provider,
199
- protectedOperations,
200
- defaultFailStrategy: 'open',
201
- failStrategyOverrides: {
202
- CheckoutComplete: 'closed',
203
- },
204
- });
205
- const next = makeNext();
206
-
207
- // CustomerCreate: default open → should pass
208
- const req1 = makeRequest({
209
- isMutation: true,
210
- operationName: 'CustomerCreate',
211
- });
212
- await mw(req1, next);
213
- expect(next).toHaveBeenCalled();
214
-
215
- // CheckoutComplete: override closed → should throw
216
- const req2 = makeRequest({
217
- isMutation: true,
218
- operationName: 'CheckoutComplete',
219
- });
220
- await expect(mw(req2, next)).rejects.toThrow(StorefrontError);
221
- });
222
-
223
- it('should throw StorefrontError with BOT_PROTECTION_REQUIRED code on closed fail', async () => {
224
- const provider = makeTokenProvider(null);
225
- const mw = botProtectionMiddleware({
226
- tokenProvider: provider,
227
- protectedOperations,
228
- defaultFailStrategy: 'closed',
229
- });
230
- const req = makeRequest({
231
- isMutation: true,
232
- operationName: 'CustomerCreate',
233
- });
234
-
235
- try {
236
- await mw(req, makeNext());
237
- expect.fail('Should have thrown');
238
- } catch (err) {
239
- expect(err).toBeInstanceOf(StorefrontError);
240
- expect((err as StorefrontError).code).toBe('BOT_PROTECTION_REQUIRED');
241
- }
242
- });
243
-
244
- it('should forward response from next() without modification', async () => {
245
- const provider = makeTokenProvider('token');
246
- const mw = botProtectionMiddleware({
247
- tokenProvider: provider,
248
- protectedOperations,
249
- });
250
- const expectedResponse = makeResponse({ data: { foo: 'bar' }, status: 201 });
251
- const next = makeNext(expectedResponse);
252
- const req = makeRequest({
253
- isMutation: true,
254
- operationName: 'CustomerCreate',
255
- });
256
-
257
- const result = await mw(req, next);
258
-
259
- expect(result).toBe(expectedResponse);
260
- });
261
-
262
- it('should propagate provider execute() exceptions to caller', async () => {
263
- const provider = makeTokenProvider(null, { shouldThrow: true });
264
- const mw = botProtectionMiddleware({
265
- tokenProvider: provider,
266
- protectedOperations,
267
- defaultFailStrategy: 'open', // fail-open, but execute throws before we get to fail strategy
268
- });
269
- const next = makeNext();
270
- const req = makeRequest({
271
- isMutation: true,
272
- operationName: 'CustomerCreate',
273
- });
274
-
275
- // Provider throws → error propagates (not caught by middleware)
276
- await expect(mw(req, next)).rejects.toThrow('Provider error');
277
- });
278
-
279
- it('should treat empty string token as falsy (no header added)', async () => {
280
- const provider = makeTokenProvider('');
281
- const mw = botProtectionMiddleware({
282
- tokenProvider: provider,
283
- protectedOperations,
284
- });
285
- const next = makeNext();
286
- const req = makeRequest({
287
- isMutation: true,
288
- operationName: 'CustomerCreate',
289
- });
290
-
291
- await mw(req, next);
292
-
293
- // Empty string is falsy → no header, but fail-open passes through
294
- expect(req.headers[BOT_PROTECTION_HEADER]).toBeUndefined();
295
- expect(next).toHaveBeenCalled();
296
- });
297
-
298
- it('should do nothing when protectedOperations list is empty', async () => {
299
- const provider = makeTokenProvider('token');
300
- const mw = botProtectionMiddleware({
301
- tokenProvider: provider,
302
- protectedOperations: [], // empty
303
- });
304
- const next = makeNext();
305
- const req = makeRequest({
306
- isMutation: true,
307
- operationName: 'CustomerCreate',
308
- });
309
-
310
- await mw(req, next);
311
-
312
- expect(provider.execute).not.toHaveBeenCalled();
313
- expect(req.headers[BOT_PROTECTION_HEADER]).toBeUndefined();
314
- });
315
-
316
- it('should call execute fresh on every request (no caching)', async () => {
317
- let callCount = 0;
318
- const provider: BotProtectionTokenProvider = {
319
- execute: vi.fn(async () => {
320
- callCount++;
321
- return `token-${callCount}`;
322
- }),
323
- destroy: vi.fn(),
324
- };
325
- const mw = botProtectionMiddleware({
326
- tokenProvider: provider,
327
- protectedOperations,
328
- });
329
-
330
- const req1 = makeRequest({ isMutation: true, operationName: 'CustomerCreate' });
331
- await mw(req1, makeNext());
332
- expect(req1.headers[BOT_PROTECTION_HEADER]).toBe('token-1');
333
-
334
- const req2 = makeRequest({ isMutation: true, operationName: 'CustomerCreate' });
335
- await mw(req2, makeNext());
336
- expect(req2.headers[BOT_PROTECTION_HEADER]).toBe('token-2');
337
-
338
- expect(provider.execute).toHaveBeenCalledTimes(2);
339
- });
340
- });
341
-
342
- // ---------------------------------------------------------------------------
343
- // FallbackBotProtectionManager
344
- // ---------------------------------------------------------------------------
345
-
346
- describe('FallbackBotProtectionManager', () => {
347
- it('should return token from primary on success', async () => {
348
- const primary = makeTokenProvider('primary-token');
349
- const fallback = makeTokenProvider('fallback-token');
350
- const manager = new FallbackBotProtectionManager(primary, fallback);
351
-
352
- const token = await manager.execute({ action: 'test' });
353
-
354
- expect(token).toBe('primary-token');
355
- expect(primary.execute).toHaveBeenCalled();
356
- expect(fallback.execute).not.toHaveBeenCalled();
357
- });
358
-
359
- it('should fallback when primary returns null', async () => {
360
- const primary = makeTokenProvider(null);
361
- const fallback = makeTokenProvider('fallback-token');
362
- const manager = new FallbackBotProtectionManager(primary, fallback);
363
-
364
- const token = await manager.execute({ action: 'test' });
365
-
366
- expect(token).toBe('fallback-token');
367
- expect(primary.execute).toHaveBeenCalled();
368
- expect(fallback.execute).toHaveBeenCalled();
369
- });
370
-
371
- it('should fallback when primary throws', async () => {
372
- const primary = makeTokenProvider(null, { shouldThrow: true });
373
- const fallback = makeTokenProvider('fallback-token');
374
- const manager = new FallbackBotProtectionManager(primary, fallback);
375
-
376
- const token = await manager.execute({ action: 'test' });
377
-
378
- expect(token).toBe('fallback-token');
379
- });
380
-
381
- it('should return null when both fail (fail-open)', async () => {
382
- const primary = makeTokenProvider(null, { shouldThrow: true });
383
- const fallback = makeTokenProvider(null, { shouldThrow: true });
384
- const manager = new FallbackBotProtectionManager(primary, fallback);
385
-
386
- const token = await manager.execute({ action: 'test' });
387
-
388
- expect(token).toBeNull();
389
- });
390
-
391
- it('should return null when primary fails and no fallback', async () => {
392
- const primary = makeTokenProvider(null, { shouldThrow: true });
393
- const manager = new FallbackBotProtectionManager(primary, null);
394
-
395
- const token = await manager.execute({ action: 'test' });
396
-
397
- expect(token).toBeNull();
398
- });
399
-
400
- it('should return null when primary returns null and no fallback', async () => {
401
- const primary = makeTokenProvider(null);
402
- const manager = new FallbackBotProtectionManager(primary, null);
403
-
404
- const token = await manager.execute({ action: 'test' });
405
-
406
- expect(token).toBeNull();
407
- });
408
-
409
- it('should destroy both primary and fallback', () => {
410
- const primary = makeTokenProvider();
411
- const fallback = makeTokenProvider();
412
- const manager = new FallbackBotProtectionManager(primary, fallback);
413
-
414
- manager.destroy();
415
-
416
- expect(primary.destroy).toHaveBeenCalled();
417
- expect(fallback.destroy).toHaveBeenCalled();
418
- });
419
-
420
- it('should handle destroy with null fallback', () => {
421
- const primary = makeTokenProvider();
422
- const manager = new FallbackBotProtectionManager(primary, null);
423
-
424
- // Should not throw
425
- manager.destroy();
426
- expect(primary.destroy).toHaveBeenCalled();
427
- });
428
-
429
- it('should pass options through to providers', async () => {
430
- const primary = makeTokenProvider('token');
431
- const manager = new FallbackBotProtectionManager(primary, null);
432
-
433
- await manager.execute({ action: 'CustomerCreate', timeoutMs: 5000 });
434
-
435
- expect(primary.execute).toHaveBeenCalledWith({
436
- action: 'CustomerCreate',
437
- timeoutMs: 5000,
438
- });
439
- });
440
-
441
- it('should treat empty string token from primary as falsy and fallback', async () => {
442
- const primary = makeTokenProvider('');
443
- const fallback = makeTokenProvider('fallback-token');
444
- const manager = new FallbackBotProtectionManager(primary, fallback);
445
-
446
- const token = await manager.execute();
447
-
448
- // Empty string is falsy → should fallback
449
- expect(token).toBe('fallback-token');
450
- });
451
-
452
- it('should work when called without options (undefined)', async () => {
453
- const primary = makeTokenProvider('token');
454
- const manager = new FallbackBotProtectionManager(primary, null);
455
-
456
- const token = await manager.execute();
457
-
458
- expect(token).toBe('token');
459
- expect(primary.execute).toHaveBeenCalledWith(undefined);
460
- });
461
- });
@@ -1,233 +0,0 @@
1
- /**
2
- * Unit tests for CartClient — plain async cart API.
3
- *
4
- * Tests all cart operations with mocked StorefrontClient.
5
- * Verifies assertNoUserErrors integration.
6
- */
7
-
8
- import { describe, it, expect, vi } from 'vitest';
9
- import { CartClient } from '../../core/cart/cart-client';
10
- import { StorefrontError, ErrorCodes } from '../../core/errors';
11
- import type { StorefrontClient } from '../../core/client/types';
12
-
13
- // ---------------------------------------------------------------------------
14
- // Mock StorefrontClient
15
- // ---------------------------------------------------------------------------
16
-
17
- function createMockClient(responses: Record<string, unknown>): StorefrontClient {
18
- return {
19
- query: vi.fn(async (_doc: unknown, _vars?: unknown) => responses),
20
- mutate: vi.fn(async (_doc: unknown, _vars?: unknown) => responses),
21
- use: vi.fn(),
22
- };
23
- }
24
-
25
- const MOCK_CART = {
26
- id: 'cart-1',
27
- lines: [
28
- { id: 'line-1', quantity: 2, merchandise: { id: 'var-1', title: 'Test Product' }, cost: { amountPerQuantity: { amount: '10.00', currencyCode: 'PLN' }, totalAmount: { amount: '20.00', currencyCode: 'PLN' } } },
29
- ],
30
- totalQuantity: 2,
31
- cost: { totalAmount: { amount: '20.00', currencyCode: 'PLN' }, subtotalAmount: { amount: '20.00', currencyCode: 'PLN' } },
32
- };
33
-
34
- // ---------------------------------------------------------------------------
35
- // Tests
36
- // ---------------------------------------------------------------------------
37
-
38
- describe('CartClient', () => {
39
- describe('get()', () => {
40
- it('should query cart by ID', async () => {
41
- const client = createMockClient({ cart: MOCK_CART });
42
- const cartClient = new CartClient(client);
43
-
44
- const cart = await cartClient.get('cart-1');
45
-
46
- expect(cart).toEqual(MOCK_CART);
47
- expect(client.query).toHaveBeenCalledWith(
48
- expect.any(String),
49
- { id: 'cart-1' },
50
- );
51
- });
52
-
53
- it('should return null for non-existent cart', async () => {
54
- const client = createMockClient({ cart: null });
55
- const cartClient = new CartClient(client);
56
-
57
- const cart = await cartClient.get('expired-cart');
58
- expect(cart).toBeNull();
59
- });
60
- });
61
-
62
- describe('create()', () => {
63
- it('should create empty cart', async () => {
64
- const client = createMockClient({
65
- cartCreate: { cart: MOCK_CART, userErrors: [] },
66
- });
67
- const cartClient = new CartClient(client);
68
-
69
- const cart = await cartClient.create();
70
-
71
- expect(cart).toEqual(MOCK_CART);
72
- expect(client.mutate).toHaveBeenCalledWith(
73
- expect.any(String),
74
- { input: {} },
75
- );
76
- });
77
-
78
- it('should create cart with initial lines', async () => {
79
- const client = createMockClient({
80
- cartCreate: { cart: MOCK_CART, userErrors: [] },
81
- });
82
- const cartClient = new CartClient(client);
83
-
84
- const cart = await cartClient.create({
85
- lines: [{ merchandiseId: 'var-1', quantity: 2 }],
86
- });
87
-
88
- expect(cart).toEqual(MOCK_CART);
89
- expect(client.mutate).toHaveBeenCalledWith(
90
- expect.any(String),
91
- { input: { lines: [{ merchandiseId: 'var-1', quantity: 2 }] } },
92
- );
93
- });
94
-
95
- it('should throw StorefrontError on user errors', async () => {
96
- const client = createMockClient({
97
- cartCreate: {
98
- cart: null,
99
- userErrors: [{ message: 'Invalid input', field: ['lines', '0', 'merchandiseId'] }],
100
- },
101
- });
102
- const cartClient = new CartClient(client);
103
-
104
- try {
105
- await cartClient.create({ lines: [{ merchandiseId: 'bad-id', quantity: 1 }] });
106
- expect.fail('Should have thrown');
107
- } catch (err) {
108
- expect(err).toBeInstanceOf(StorefrontError);
109
- const sfErr = err as StorefrontError;
110
- expect(sfErr.code).toBe(ErrorCodes.USER_ERROR);
111
- expect(sfErr.hasUserErrors).toBe(true);
112
- expect(sfErr.userErrors[0].message).toBe('Invalid input');
113
- }
114
- });
115
- });
116
-
117
- describe('addItems()', () => {
118
- it('should add lines to cart', async () => {
119
- const client = createMockClient({
120
- cartLinesAdd: { cart: MOCK_CART, userErrors: [] },
121
- });
122
- const cartClient = new CartClient(client);
123
-
124
- const cart = await cartClient.addItems('cart-1', [
125
- { merchandiseId: 'var-1', quantity: 1 },
126
- ]);
127
-
128
- expect(cart).toEqual(MOCK_CART);
129
- expect(client.mutate).toHaveBeenCalledWith(
130
- expect.any(String),
131
- { cartId: 'cart-1', lines: [{ merchandiseId: 'var-1', quantity: 1 }] },
132
- );
133
- });
134
-
135
- it('should throw on user errors (e.g., out of stock)', async () => {
136
- const client = createMockClient({
137
- cartLinesAdd: {
138
- cart: null,
139
- userErrors: [{ message: 'Out of stock', code: 'INSUFFICIENT_INVENTORY' }],
140
- },
141
- });
142
- const cartClient = new CartClient(client);
143
-
144
- await expect(
145
- cartClient.addItems('cart-1', [{ merchandiseId: 'var-1', quantity: 100 }]),
146
- ).rejects.toThrow(StorefrontError);
147
- });
148
- });
149
-
150
- describe('updateItems()', () => {
151
- it('should update line quantities', async () => {
152
- const updatedCart = { ...MOCK_CART, totalQuantity: 5 };
153
- const client = createMockClient({
154
- cartLinesUpdate: { cart: updatedCart, userErrors: [] },
155
- });
156
- const cartClient = new CartClient(client);
157
-
158
- const cart = await cartClient.updateItems('cart-1', [
159
- { id: 'line-1', quantity: 5 },
160
- ]);
161
-
162
- expect(cart.totalQuantity).toBe(5);
163
- });
164
- });
165
-
166
- describe('removeItems()', () => {
167
- it('should remove lines from cart', async () => {
168
- const emptyCart = { ...MOCK_CART, lines: [], totalQuantity: 0 };
169
- const client = createMockClient({
170
- cartLinesRemove: { cart: emptyCart, userErrors: [] },
171
- });
172
- const cartClient = new CartClient(client);
173
-
174
- const cart = await cartClient.removeItems('cart-1', ['line-1']);
175
-
176
- expect(cart.lines).toHaveLength(0);
177
- expect(cart.totalQuantity).toBe(0);
178
- });
179
- });
180
-
181
- describe('updateDiscountCodes()', () => {
182
- it('should apply discount codes', async () => {
183
- const client = createMockClient({
184
- cartDiscountCodesUpdate: { cart: MOCK_CART, userErrors: [] },
185
- });
186
- const cartClient = new CartClient(client);
187
-
188
- const cart = await cartClient.updateDiscountCodes('cart-1', ['SAVE10']);
189
-
190
- expect(cart).toEqual(MOCK_CART);
191
- expect(client.mutate).toHaveBeenCalledWith(
192
- expect.any(String),
193
- { cartId: 'cart-1', discountCodes: ['SAVE10'] },
194
- );
195
- });
196
- });
197
-
198
- describe('updateNote()', () => {
199
- it('should update cart note', async () => {
200
- const client = createMockClient({
201
- cartNoteUpdate: { cart: MOCK_CART, userErrors: [] },
202
- });
203
- const cartClient = new CartClient(client);
204
-
205
- const cart = await cartClient.updateNote('cart-1', 'Gift wrap please');
206
-
207
- expect(cart).toEqual(MOCK_CART);
208
- expect(client.mutate).toHaveBeenCalledWith(
209
- expect.any(String),
210
- { cartId: 'cart-1', note: 'Gift wrap please' },
211
- );
212
- });
213
- });
214
-
215
- describe('updateBuyerIdentity()', () => {
216
- it('should update buyer identity', async () => {
217
- const client = createMockClient({
218
- cartBuyerIdentityUpdate: { cart: MOCK_CART, userErrors: [] },
219
- });
220
- const cartClient = new CartClient(client);
221
-
222
- const cart = await cartClient.updateBuyerIdentity('cart-1', {
223
- email: 'test@example.com',
224
- });
225
-
226
- expect(cart).toEqual(MOCK_CART);
227
- expect(client.mutate).toHaveBeenCalledWith(
228
- expect.any(String),
229
- { cartId: 'cart-1', buyerIdentity: { email: 'test@example.com' } },
230
- );
231
- });
232
- });
233
- });