@doswiftly/storefront-sdk 4.4.0 → 4.7.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.
Files changed (95) hide show
  1. package/dist/core/cart/types.d.ts +75 -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/image.d.ts +24 -2
  5. package/dist/core/image.d.ts.map +1 -1
  6. package/dist/core/image.js +145 -2
  7. package/dist/core/index.d.ts +1 -1
  8. package/dist/core/index.d.ts.map +1 -1
  9. package/dist/core/index.js +2 -0
  10. package/dist/core/operations/cart.d.ts +15 -9
  11. package/dist/core/operations/cart.d.ts.map +1 -1
  12. package/dist/core/operations/cart.js +131 -58
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.js +1 -1
  15. package/package.json +19 -14
  16. package/src/__tests__/contract/storefront-api.contract.test.ts +0 -450
  17. package/src/__tests__/unit/auth-client.test.ts +0 -210
  18. package/src/__tests__/unit/bot-protection.test.ts +0 -461
  19. package/src/__tests__/unit/cart-client.test.ts +0 -233
  20. package/src/__tests__/unit/cart-store.test.ts +0 -349
  21. package/src/__tests__/unit/create-client.test.ts +0 -356
  22. package/src/__tests__/unit/helpers.test.ts +0 -377
  23. package/src/__tests__/unit/middleware.test.ts +0 -374
  24. package/src/__tests__/unit/test-helpers.ts +0 -103
  25. package/src/core/auth/auth-client.ts +0 -123
  26. package/src/core/auth/cookie-config.ts +0 -23
  27. package/src/core/auth/handlers.ts +0 -168
  28. package/src/core/auth/routes.ts +0 -26
  29. package/src/core/auth/token-client.ts +0 -51
  30. package/src/core/auth/types.ts +0 -54
  31. package/src/core/bot-protection/abstract-manager.ts +0 -185
  32. package/src/core/bot-protection/create-manager.ts +0 -37
  33. package/src/core/bot-protection/eucaptcha-manager.ts +0 -88
  34. package/src/core/bot-protection/fallback-manager.ts +0 -43
  35. package/src/core/bot-protection/turnstile-manager.ts +0 -92
  36. package/src/core/bot-protection/types/eucaptcha.d.ts +0 -28
  37. package/src/core/bot-protection/types/turnstile.d.ts +0 -33
  38. package/src/core/cache.ts +0 -102
  39. package/src/core/cart/cart-client.ts +0 -150
  40. package/src/core/cart/cookie-config.ts +0 -13
  41. package/src/core/cart/types.ts +0 -104
  42. package/src/core/client/compose.ts +0 -15
  43. package/src/core/client/create-client.ts +0 -129
  44. package/src/core/client/dedupe.ts +0 -19
  45. package/src/core/client/execute.ts +0 -70
  46. package/src/core/client/hash.ts +0 -21
  47. package/src/core/client/operation-name.ts +0 -12
  48. package/src/core/client/types.ts +0 -171
  49. package/src/core/currency/cookie-config.ts +0 -13
  50. package/src/core/errors.ts +0 -67
  51. package/src/core/format.ts +0 -254
  52. package/src/core/helpers/assert-no-user-errors.ts +0 -21
  53. package/src/core/helpers/normalize-connection.ts +0 -48
  54. package/src/core/helpers/sanitize-html.ts +0 -42
  55. package/src/core/image.ts +0 -22
  56. package/src/core/index.ts +0 -174
  57. package/src/core/language/cookie-config.ts +0 -13
  58. package/src/core/middleware/auth.ts +0 -27
  59. package/src/core/middleware/bot-protection.ts +0 -140
  60. package/src/core/middleware/currency.ts +0 -27
  61. package/src/core/middleware/errors.ts +0 -86
  62. package/src/core/middleware/language.ts +0 -30
  63. package/src/core/middleware/retry.ts +0 -75
  64. package/src/core/middleware/timeout.ts +0 -61
  65. package/src/core/operations/auth.ts +0 -123
  66. package/src/core/operations/cart.ts +0 -185
  67. package/src/index.ts +0 -25
  68. package/src/react/bot-protection/bot-protection-context.ts +0 -17
  69. package/src/react/bot-protection/bot-protection-widget.tsx +0 -46
  70. package/src/react/cookies.ts +0 -89
  71. package/src/react/helpers/create-store-context.ts +0 -56
  72. package/src/react/hooks/use-auth.ts +0 -218
  73. package/src/react/hooks/use-bot-protection.ts +0 -31
  74. package/src/react/hooks/use-cart-manager.ts +0 -236
  75. package/src/react/hooks/use-currency.ts +0 -23
  76. package/src/react/hooks/use-debounced-value.ts +0 -30
  77. package/src/react/hooks/use-hydrated.ts +0 -20
  78. package/src/react/hooks/use-storefront-client.ts +0 -12
  79. package/src/react/index.ts +0 -71
  80. package/src/react/providers/currency-provider.tsx +0 -30
  81. package/src/react/providers/language-provider.tsx +0 -34
  82. package/src/react/providers/storefront-client-provider.tsx +0 -107
  83. package/src/react/providers/storefront-provider.tsx +0 -99
  84. package/src/react/server/get-storefront-client.ts +0 -60
  85. package/src/react/server/index.ts +0 -1
  86. package/src/react/stores/auth.store.ts +0 -112
  87. package/src/react/stores/cart.context.ts +0 -10
  88. package/src/react/stores/cart.store.ts +0 -254
  89. package/src/react/stores/currency.store.ts +0 -93
  90. package/src/react/stores/index.ts +0 -17
  91. package/src/react/stores/language.store.ts +0 -90
  92. package/src/react/stores/store-context.tsx +0 -103
  93. package/src/react/types/shop-config.ts +0 -22
  94. package/tsconfig.json +0 -20
  95. package/vitest.config.ts +0 -14
@@ -1,450 +0,0 @@
1
- /**
2
- * Contract Tests for Storefront GraphQL API (v4 SDK).
3
- *
4
- * These tests validate that the backend API responses match expected
5
- * shapes when accessed through the v4 SDK (createStorefrontClient,
6
- * CartClient, AuthClient).
7
- *
8
- * REQUIRES: Running backend at STOREFRONT_API_URL.
9
- *
10
- * Run: pnpm run test:contract
11
- */
12
-
13
- import { describe, it, expect, beforeAll } from 'vitest';
14
- import { createStorefrontClient } from '../../core/client/create-client';
15
- import { errorMiddleware } from '../../core/middleware/errors';
16
- import { CartClient } from '../../core/cart/cart-client';
17
- import { AuthClient } from '../../core/auth/auth-client';
18
- import { StorefrontError, ErrorCodes } from '../../core/errors';
19
- import type { StorefrontClient } from '../../core/client/types';
20
-
21
- // ============================================
22
- // Configuration
23
- // ============================================
24
-
25
- const API_URL = process.env.STOREFRONT_API_URL || 'http://localhost:8000';
26
- const SHOP_SLUG = process.env.TEST_SHOP_SLUG || 'test-shop';
27
-
28
- const PRODUCT_QUERY = `
29
- query Product($handle: String!) {
30
- product(handle: $handle) {
31
- id handle title description
32
- variants { id title sku quantityAvailable }
33
- priceRange { minVariantPrice { amount currencyCode } }
34
- }
35
- }
36
- `;
37
-
38
- const PRODUCTS_QUERY = `
39
- query Products($first: Int!) {
40
- products(first: $first) {
41
- edges { node { id handle title } }
42
- pageInfo { hasNextPage endCursor }
43
- totalCount
44
- }
45
- }
46
- `;
47
-
48
- const SHOP_QUERY = `
49
- query Shop {
50
- shop { id name currencyCode supportedCurrencies }
51
- }
52
- `;
53
-
54
- const CATEGORIES_QUERY = `
55
- query Categories {
56
- categories { roots { id slug name } totalCount }
57
- }
58
- `;
59
-
60
- const COLLECTIONS_QUERY = `
61
- query Collections($first: Int!) {
62
- collections(first: $first) {
63
- edges { node { id handle title } }
64
- pageInfo { hasNextPage endCursor }
65
- }
66
- }
67
- `;
68
-
69
- // ============================================
70
- // Setup
71
- // ============================================
72
-
73
- describe('Storefront API Contract Tests (v4 SDK)', () => {
74
- let client: StorefrontClient;
75
- let cartClient: CartClient;
76
- let authClient: AuthClient;
77
- let variantId: string | undefined;
78
-
79
- beforeAll(async () => {
80
- client = createStorefrontClient({
81
- apiUrl: API_URL,
82
- shopSlug: SHOP_SLUG,
83
- middleware: [errorMiddleware()],
84
- });
85
-
86
- cartClient = new CartClient(client);
87
- authClient = new AuthClient(client);
88
-
89
- // Get a variant ID for cart tests
90
- try {
91
- const data = await client.query<{
92
- products: { edges: Array<{ node: { id: string; handle: string } }> };
93
- }>(PRODUCTS_QUERY, { first: 1 });
94
-
95
- if (data.products.edges.length > 0) {
96
- const handle = data.products.edges[0].node.handle;
97
- const productData = await client.query<{
98
- product: { variants: Array<{ id: string; quantityAvailable: number }> } | null;
99
- }>(PRODUCT_QUERY, { handle });
100
-
101
- const variant = productData.product?.variants?.[0];
102
- if (variant && variant.quantityAvailable > 0) {
103
- variantId = variant.id;
104
- }
105
- }
106
- } catch (e) {
107
- console.warn('Could not fetch variant for cart tests:', e);
108
- }
109
- });
110
-
111
- // ============================================
112
- // Query Contract Tests
113
- // ============================================
114
-
115
- describe('Queries', () => {
116
- it('Shop — should return shop info with currency', async () => {
117
- const data = await client.query<{
118
- shop: { id: string; name: string; currencyCode: string; supportedCurrencies: string[] };
119
- }>(SHOP_QUERY);
120
-
121
- expect(data.shop).toBeDefined();
122
- expect(data.shop.id).toBeDefined();
123
- expect(data.shop.name).toBeDefined();
124
- expect(data.shop.currencyCode).toBeDefined();
125
- expect(Array.isArray(data.shop.supportedCurrencies)).toBe(true);
126
- });
127
-
128
- it('Product — should return product by handle', async () => {
129
- const data = await client.query<{
130
- product: { id: string; handle: string; title: string } | null;
131
- }>(PRODUCT_QUERY, { handle: 'test-product' });
132
-
133
- // Product may or may not exist — just validate shape
134
- if (data.product) {
135
- expect(data.product.id).toBeDefined();
136
- expect(data.product.handle).toBeDefined();
137
- expect(data.product.title).toBeDefined();
138
- }
139
- });
140
-
141
- it('Product — should return null for non-existent handle', async () => {
142
- const data = await client.query<{
143
- product: { id: string } | null;
144
- }>(PRODUCT_QUERY, { handle: 'definitely-non-existent-product-xyz-12345' });
145
-
146
- expect(data.product).toBeNull();
147
- });
148
-
149
- it('Products — should return paginated list', async () => {
150
- const data = await client.query<{
151
- products: {
152
- edges: Array<{ node: { id: string } }>;
153
- pageInfo: { hasNextPage: boolean; endCursor: string | null };
154
- totalCount: number;
155
- };
156
- }>(PRODUCTS_QUERY, { first: 5 });
157
-
158
- expect(data.products).toBeDefined();
159
- expect(Array.isArray(data.products.edges)).toBe(true);
160
- expect(data.products.pageInfo).toBeDefined();
161
- expect(typeof data.products.pageInfo.hasNextPage).toBe('boolean');
162
- expect(typeof data.products.totalCount).toBe('number');
163
- });
164
-
165
- it('Products — should support cursor pagination', async () => {
166
- const page1 = await client.query<{
167
- products: {
168
- edges: Array<{ node: { id: string } }>;
169
- pageInfo: { hasNextPage: boolean; endCursor: string | null };
170
- };
171
- }>(PRODUCTS_QUERY, { first: 2 });
172
-
173
- if (page1.products.pageInfo.hasNextPage && page1.products.pageInfo.endCursor) {
174
- const PRODUCTS_AFTER = `
175
- query ProductsAfter($first: Int!, $after: String) {
176
- products(first: $first, after: $after) {
177
- edges { node { id } }
178
- pageInfo { hasNextPage endCursor }
179
- }
180
- }
181
- `;
182
-
183
- const page2 = await client.query<{
184
- products: { edges: Array<{ node: { id: string } }> };
185
- }>(PRODUCTS_AFTER, { first: 2, after: page1.products.pageInfo.endCursor });
186
-
187
- expect(page2.products.edges).toBeDefined();
188
- }
189
- });
190
-
191
- it('Categories — should return category tree', async () => {
192
- const data = await client.query<{
193
- categories: { roots: Array<{ id: string; slug: string; name: string }>; totalCount: number };
194
- }>(CATEGORIES_QUERY);
195
-
196
- expect(data.categories).toBeDefined();
197
- expect(Array.isArray(data.categories.roots)).toBe(true);
198
- expect(typeof data.categories.totalCount).toBe('number');
199
- });
200
-
201
- it('Collections — should return paginated list', async () => {
202
- const data = await client.query<{
203
- collections: {
204
- edges: Array<{ node: { id: string } }>;
205
- pageInfo: { hasNextPage: boolean };
206
- };
207
- }>(COLLECTIONS_QUERY, { first: 5 });
208
-
209
- expect(data.collections).toBeDefined();
210
- expect(Array.isArray(data.collections.edges)).toBe(true);
211
- });
212
- });
213
-
214
- // ============================================
215
- // CartClient Contract Tests
216
- // ============================================
217
-
218
- describe('CartClient', () => {
219
- it('should create empty cart', async () => {
220
- const cart = await cartClient.create();
221
-
222
- expect(cart.id).toBeDefined();
223
- expect(cart.totalQuantity).toBe(0);
224
- expect(cart.lines).toHaveLength(0);
225
- });
226
-
227
- it('should get existing cart by ID', async () => {
228
- const created = await cartClient.create();
229
- const fetched = await cartClient.get(created.id);
230
-
231
- expect(fetched).not.toBeNull();
232
- expect(fetched!.id).toBe(created.id);
233
- });
234
-
235
- it('should return null for non-existent cart', async () => {
236
- const cart = await cartClient.get('non-existent-cart-id');
237
- expect(cart).toBeNull();
238
- });
239
-
240
- it('should complete full cart lifecycle', async () => {
241
- if (!variantId) {
242
- console.warn('Skipping cart lifecycle — no variant with stock');
243
- return;
244
- }
245
-
246
- // 1. Create
247
- const cart = await cartClient.create();
248
- expect(cart.id).toBeDefined();
249
-
250
- // 2. Add item
251
- const afterAdd = await cartClient.addItems(cart.id, [
252
- { merchandiseId: variantId, quantity: 1 },
253
- ]);
254
- expect(afterAdd.lines).toHaveLength(1);
255
- expect(afterAdd.totalQuantity).toBe(1);
256
-
257
- const lineId = afterAdd.lines[0].id;
258
-
259
- // 3. Update quantity
260
- const afterUpdate = await cartClient.updateItems(cart.id, [
261
- { id: lineId, quantity: 2 },
262
- ]);
263
- expect(afterUpdate.lines[0].quantity).toBe(2);
264
-
265
- // 4. Remove
266
- const afterRemove = await cartClient.removeItems(cart.id, [lineId]);
267
- expect(afterRemove.lines).toHaveLength(0);
268
- expect(afterRemove.totalQuantity).toBe(0);
269
- });
270
-
271
- it('should update cart note', async () => {
272
- const cart = await cartClient.create();
273
- const updated = await cartClient.updateNote(cart.id, 'Gift wrap please');
274
-
275
- expect(updated.note).toBe('Gift wrap please');
276
- });
277
-
278
- it('should handle discount codes', async () => {
279
- const cart = await cartClient.create();
280
-
281
- // Apply (may fail if code doesn't exist — that's OK, we test the API shape)
282
- try {
283
- const updated = await cartClient.updateDiscountCodes(cart.id, ['TESTCODE10']);
284
- expect(updated).toBeDefined();
285
- } catch (err) {
286
- // User errors are expected if discount code is invalid
287
- if (err instanceof StorefrontError) {
288
- expect(err.code).toBe(ErrorCodes.USER_ERROR);
289
- } else {
290
- throw err;
291
- }
292
- }
293
- });
294
- });
295
-
296
- // ============================================
297
- // AuthClient Contract Tests
298
- // ============================================
299
-
300
- describe('AuthClient', () => {
301
- const testEmail = `contract-v4-${Date.now()}@example.com`;
302
- const testPassword = 'ContractTest123!';
303
-
304
- it('should register new customer', async () => {
305
- const result = await authClient.register({
306
- email: testEmail,
307
- password: testPassword,
308
- firstName: 'Contract',
309
- lastName: 'Test',
310
- });
311
-
312
- expect(result.accessToken).toBeDefined();
313
- expect(result.expiresAt).toBeDefined();
314
- });
315
-
316
- it('should throw on duplicate registration', async () => {
317
- try {
318
- await authClient.register({
319
- email: testEmail,
320
- password: testPassword,
321
- });
322
- expect.fail('Should have thrown on duplicate email');
323
- } catch (err) {
324
- expect(err).toBeInstanceOf(StorefrontError);
325
- expect((err as StorefrontError).code).toBe(ErrorCodes.USER_ERROR);
326
- }
327
- });
328
-
329
- it('should login with valid credentials', async () => {
330
- const result = await authClient.login(testEmail, testPassword);
331
-
332
- expect(result.accessToken).toBeDefined();
333
- expect(result.expiresAt).toBeDefined();
334
- });
335
-
336
- it('should throw on invalid credentials', async () => {
337
- try {
338
- await authClient.login('nonexistent@example.com', 'WrongPassword123!');
339
- expect.fail('Should have thrown');
340
- } catch (err) {
341
- expect(err).toBeInstanceOf(StorefrontError);
342
- expect((err as StorefrontError).code).toBe(ErrorCodes.USER_ERROR);
343
- }
344
- });
345
-
346
- it('should fetch customer profile', async () => {
347
- const { accessToken } = await authClient.login(testEmail, testPassword);
348
-
349
- // Create authenticated client
350
- const authSfClient = createStorefrontClient({
351
- apiUrl: API_URL,
352
- shopSlug: SHOP_SLUG,
353
- middleware: [errorMiddleware()],
354
- defaultHeaders: { 'X-Customer-Token': accessToken },
355
- });
356
- const authCustomerClient = new AuthClient(authSfClient);
357
-
358
- const customer = await authCustomerClient.getCustomer(accessToken);
359
-
360
- expect(customer).not.toBeNull();
361
- expect(customer!.email).toBe(testEmail);
362
- });
363
-
364
- it('should renew token', async () => {
365
- const { accessToken } = await authClient.login(testEmail, testPassword);
366
-
367
- const renewed = await authClient.renewToken(accessToken);
368
-
369
- expect(renewed.accessToken).toBeDefined();
370
- expect(renewed.expiresAt).toBeDefined();
371
- });
372
-
373
- it('should logout without error', async () => {
374
- const { accessToken } = await authClient.login(testEmail, testPassword);
375
-
376
- // Should not throw
377
- await authClient.logout(accessToken);
378
- });
379
-
380
- it('should handle logout with expired token gracefully', async () => {
381
- // Should not throw
382
- await authClient.logout('definitely-expired-token');
383
- });
384
- });
385
-
386
- // ============================================
387
- // Error Handling Contract Tests
388
- // ============================================
389
-
390
- describe('Error Handling', () => {
391
- it('should throw StorefrontError for invalid GraphQL', async () => {
392
- try {
393
- await client.query('{ invalidField { nope } }');
394
- expect.fail('Should have thrown');
395
- } catch (err) {
396
- expect(err).toBeInstanceOf(StorefrontError);
397
- }
398
- });
399
-
400
- it('should handle product not found as null, not error', async () => {
401
- const data = await client.query<{ product: unknown | null }>(
402
- PRODUCT_QUERY,
403
- { handle: 'absolutely-nonexistent-product-12345' },
404
- );
405
- expect(data.product).toBeNull();
406
- });
407
- });
408
-
409
- // ============================================
410
- // Middleware Integration Contract Tests
411
- // ============================================
412
-
413
- describe('Middleware Integration', () => {
414
- it('should pass custom headers through middleware', async () => {
415
- const clientWithCurrency = createStorefrontClient({
416
- apiUrl: API_URL,
417
- shopSlug: SHOP_SLUG,
418
- middleware: [
419
- (req, next) => {
420
- req.headers['X-Preferred-Currency'] = 'EUR';
421
- return next(req);
422
- },
423
- errorMiddleware(),
424
- ],
425
- });
426
-
427
- // Should not throw — backend accepts the header
428
- const data = await clientWithCurrency.query<{
429
- shop: { currencyCode: string };
430
- }>(SHOP_QUERY);
431
-
432
- expect(data.shop).toBeDefined();
433
- });
434
-
435
- it('should support imperative use() API', async () => {
436
- const freshClient = createStorefrontClient({
437
- apiUrl: API_URL,
438
- shopSlug: SHOP_SLUG,
439
- });
440
-
441
- freshClient.use(errorMiddleware());
442
-
443
- const data = await freshClient.query<{
444
- shop: { id: string };
445
- }>(SHOP_QUERY);
446
-
447
- expect(data.shop.id).toBeDefined();
448
- });
449
- });
450
- });
@@ -1,210 +0,0 @@
1
- /**
2
- * Unit tests for AuthClient — plain async auth API.
3
- *
4
- * Tests login, logout, renewToken, register, getCustomer.
5
- */
6
-
7
- import { describe, it, expect, vi } from 'vitest';
8
- import { AuthClient } from '../../core/auth/auth-client';
9
- import { StorefrontError, ErrorCodes } from '../../core/errors';
10
- import type { StorefrontClient } from '../../core/client/types';
11
-
12
- // ---------------------------------------------------------------------------
13
- // Mock StorefrontClient
14
- // ---------------------------------------------------------------------------
15
-
16
- function createMockClient(responses: Record<string, unknown>): StorefrontClient {
17
- return {
18
- query: vi.fn(async () => responses),
19
- mutate: vi.fn(async () => responses),
20
- use: vi.fn(),
21
- };
22
- }
23
-
24
- // ---------------------------------------------------------------------------
25
- // Tests
26
- // ---------------------------------------------------------------------------
27
-
28
- describe('AuthClient', () => {
29
- describe('login()', () => {
30
- it('should return accessToken and expiresAt on success', async () => {
31
- const client = createMockClient({
32
- customerAccessTokenCreate: {
33
- customerAccessToken: {
34
- accessToken: 'token-123',
35
- expiresAt: '2026-04-01T00:00:00Z',
36
- },
37
- userErrors: [],
38
- },
39
- });
40
- const authClient = new AuthClient(client);
41
-
42
- const result = await authClient.login('user@example.com', 'password');
43
-
44
- expect(result.accessToken).toBe('token-123');
45
- expect(result.expiresAt).toBe('2026-04-01T00:00:00Z');
46
- expect(client.mutate).toHaveBeenCalledWith(
47
- expect.any(String),
48
- { input: { email: 'user@example.com', password: 'password' } },
49
- );
50
- });
51
-
52
- it('should throw StorefrontError on invalid credentials', async () => {
53
- const client = createMockClient({
54
- customerAccessTokenCreate: {
55
- customerAccessToken: null,
56
- userErrors: [{ message: 'Invalid email or password' }],
57
- },
58
- });
59
- const authClient = new AuthClient(client);
60
-
61
- try {
62
- await authClient.login('bad@example.com', 'wrong');
63
- expect.fail('Should have thrown');
64
- } catch (err) {
65
- expect(err).toBeInstanceOf(StorefrontError);
66
- const sfErr = err as StorefrontError;
67
- expect(sfErr.code).toBe(ErrorCodes.USER_ERROR);
68
- expect(sfErr.message).toBe('Invalid email or password');
69
- expect(sfErr.hasUserErrors).toBe(true);
70
- }
71
- });
72
- });
73
-
74
- describe('logout()', () => {
75
- it('should call mutate with token', async () => {
76
- const client = createMockClient({
77
- customerAccessTokenDelete: {
78
- deletedAccessToken: 'token-123',
79
- userErrors: [],
80
- },
81
- });
82
- const authClient = new AuthClient(client);
83
-
84
- await authClient.logout('token-123');
85
-
86
- expect(client.mutate).toHaveBeenCalledWith(
87
- expect.any(String),
88
- { customerAccessToken: 'token-123' },
89
- );
90
- });
91
-
92
- it('should NOT throw on failure (token may be expired)', async () => {
93
- const client: StorefrontClient = {
94
- query: vi.fn(),
95
- mutate: vi.fn(async () => { throw new Error('Server error'); }),
96
- use: vi.fn(),
97
- };
98
- const authClient = new AuthClient(client);
99
-
100
- // Should not throw
101
- await authClient.logout('expired-token');
102
- });
103
- });
104
-
105
- describe('renewToken()', () => {
106
- it('should return renewed token', async () => {
107
- const client = createMockClient({
108
- customerAccessTokenRenew: {
109
- customerAccessToken: {
110
- accessToken: 'new-token-456',
111
- expiresAt: '2026-05-01T00:00:00Z',
112
- },
113
- userErrors: [],
114
- },
115
- });
116
- const authClient = new AuthClient(client);
117
-
118
- const result = await authClient.renewToken('old-token');
119
-
120
- expect(result.accessToken).toBe('new-token-456');
121
- expect(result.expiresAt).toBe('2026-05-01T00:00:00Z');
122
- });
123
-
124
- it('should throw on user errors', async () => {
125
- const client = createMockClient({
126
- customerAccessTokenRenew: {
127
- customerAccessToken: null,
128
- userErrors: [{ message: 'Token expired' }],
129
- },
130
- });
131
- const authClient = new AuthClient(client);
132
-
133
- await expect(authClient.renewToken('expired')).rejects.toThrow(StorefrontError);
134
- });
135
- });
136
-
137
- describe('register()', () => {
138
- it('should return token and customer on success', async () => {
139
- const client = createMockClient({
140
- customerCreate: {
141
- customer: { id: 'cust-1', email: 'new@example.com', firstName: 'John', lastName: 'Doe' },
142
- customerAccessToken: {
143
- accessToken: 'reg-token',
144
- expiresAt: '2026-04-01T00:00:00Z',
145
- },
146
- userErrors: [],
147
- },
148
- });
149
- const authClient = new AuthClient(client);
150
-
151
- const result = await authClient.register({
152
- email: 'new@example.com',
153
- password: 'SecurePass123!',
154
- firstName: 'John',
155
- lastName: 'Doe',
156
- });
157
-
158
- expect(result.accessToken).toBe('reg-token');
159
- expect(result.customer?.email).toBe('new@example.com');
160
- });
161
-
162
- it('should throw on duplicate email', async () => {
163
- const client = createMockClient({
164
- customerCreate: {
165
- customer: null,
166
- customerAccessToken: null,
167
- userErrors: [{ message: 'Email already taken', field: ['email'] }],
168
- },
169
- });
170
- const authClient = new AuthClient(client);
171
-
172
- try {
173
- await authClient.register({ email: 'existing@example.com', password: 'Pass123!' });
174
- expect.fail('Should have thrown');
175
- } catch (err) {
176
- expect(err).toBeInstanceOf(StorefrontError);
177
- expect((err as StorefrontError).userErrors[0].field).toEqual(['email']);
178
- }
179
- });
180
- });
181
-
182
- describe('getCustomer()', () => {
183
- it('should return customer data', async () => {
184
- const mockCustomer = {
185
- id: 'cust-1',
186
- email: 'user@example.com',
187
- firstName: 'Jane',
188
- lastName: 'Smith',
189
- };
190
- const client = createMockClient({ customer: mockCustomer });
191
- const authClient = new AuthClient(client);
192
-
193
- const customer = await authClient.getCustomer('token-123');
194
-
195
- expect(customer).toEqual(mockCustomer);
196
- expect(client.query).toHaveBeenCalledWith(
197
- expect.any(String),
198
- { customerAccessToken: 'token-123' },
199
- );
200
- });
201
-
202
- it('should return null for invalid token', async () => {
203
- const client = createMockClient({ customer: null });
204
- const authClient = new AuthClient(client);
205
-
206
- const customer = await authClient.getCustomer('invalid-token');
207
- expect(customer).toBeNull();
208
- });
209
- });
210
- });