@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.
- package/dist/core/cart/types.d.ts +75 -20
- package/dist/core/cart/types.d.ts.map +1 -1
- package/dist/core/cart/types.js +3 -0
- package/dist/core/image.d.ts +24 -2
- package/dist/core/image.d.ts.map +1 -1
- package/dist/core/image.js +145 -2
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -0
- package/dist/core/operations/cart.d.ts +15 -9
- package/dist/core/operations/cart.d.ts.map +1 -1
- package/dist/core/operations/cart.js +131 -58
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +19 -14
- package/src/__tests__/contract/storefront-api.contract.test.ts +0 -450
- package/src/__tests__/unit/auth-client.test.ts +0 -210
- package/src/__tests__/unit/bot-protection.test.ts +0 -461
- package/src/__tests__/unit/cart-client.test.ts +0 -233
- package/src/__tests__/unit/cart-store.test.ts +0 -349
- package/src/__tests__/unit/create-client.test.ts +0 -356
- package/src/__tests__/unit/helpers.test.ts +0 -377
- package/src/__tests__/unit/middleware.test.ts +0 -374
- package/src/__tests__/unit/test-helpers.ts +0 -103
- package/src/core/auth/auth-client.ts +0 -123
- package/src/core/auth/cookie-config.ts +0 -23
- package/src/core/auth/handlers.ts +0 -168
- package/src/core/auth/routes.ts +0 -26
- package/src/core/auth/token-client.ts +0 -51
- package/src/core/auth/types.ts +0 -54
- package/src/core/bot-protection/abstract-manager.ts +0 -185
- package/src/core/bot-protection/create-manager.ts +0 -37
- package/src/core/bot-protection/eucaptcha-manager.ts +0 -88
- package/src/core/bot-protection/fallback-manager.ts +0 -43
- package/src/core/bot-protection/turnstile-manager.ts +0 -92
- package/src/core/bot-protection/types/eucaptcha.d.ts +0 -28
- package/src/core/bot-protection/types/turnstile.d.ts +0 -33
- package/src/core/cache.ts +0 -102
- package/src/core/cart/cart-client.ts +0 -150
- package/src/core/cart/cookie-config.ts +0 -13
- package/src/core/cart/types.ts +0 -104
- package/src/core/client/compose.ts +0 -15
- package/src/core/client/create-client.ts +0 -129
- package/src/core/client/dedupe.ts +0 -19
- package/src/core/client/execute.ts +0 -70
- package/src/core/client/hash.ts +0 -21
- package/src/core/client/operation-name.ts +0 -12
- package/src/core/client/types.ts +0 -171
- package/src/core/currency/cookie-config.ts +0 -13
- package/src/core/errors.ts +0 -67
- package/src/core/format.ts +0 -254
- package/src/core/helpers/assert-no-user-errors.ts +0 -21
- package/src/core/helpers/normalize-connection.ts +0 -48
- package/src/core/helpers/sanitize-html.ts +0 -42
- package/src/core/image.ts +0 -22
- package/src/core/index.ts +0 -174
- package/src/core/language/cookie-config.ts +0 -13
- package/src/core/middleware/auth.ts +0 -27
- package/src/core/middleware/bot-protection.ts +0 -140
- package/src/core/middleware/currency.ts +0 -27
- package/src/core/middleware/errors.ts +0 -86
- package/src/core/middleware/language.ts +0 -30
- package/src/core/middleware/retry.ts +0 -75
- package/src/core/middleware/timeout.ts +0 -61
- package/src/core/operations/auth.ts +0 -123
- package/src/core/operations/cart.ts +0 -185
- package/src/index.ts +0 -25
- package/src/react/bot-protection/bot-protection-context.ts +0 -17
- package/src/react/bot-protection/bot-protection-widget.tsx +0 -46
- package/src/react/cookies.ts +0 -89
- package/src/react/helpers/create-store-context.ts +0 -56
- package/src/react/hooks/use-auth.ts +0 -218
- package/src/react/hooks/use-bot-protection.ts +0 -31
- package/src/react/hooks/use-cart-manager.ts +0 -236
- package/src/react/hooks/use-currency.ts +0 -23
- package/src/react/hooks/use-debounced-value.ts +0 -30
- package/src/react/hooks/use-hydrated.ts +0 -20
- package/src/react/hooks/use-storefront-client.ts +0 -12
- package/src/react/index.ts +0 -71
- package/src/react/providers/currency-provider.tsx +0 -30
- package/src/react/providers/language-provider.tsx +0 -34
- package/src/react/providers/storefront-client-provider.tsx +0 -107
- package/src/react/providers/storefront-provider.tsx +0 -99
- package/src/react/server/get-storefront-client.ts +0 -60
- package/src/react/server/index.ts +0 -1
- package/src/react/stores/auth.store.ts +0 -112
- package/src/react/stores/cart.context.ts +0 -10
- package/src/react/stores/cart.store.ts +0 -254
- package/src/react/stores/currency.store.ts +0 -93
- package/src/react/stores/index.ts +0 -17
- package/src/react/stores/language.store.ts +0 -90
- package/src/react/stores/store-context.tsx +0 -103
- package/src/react/types/shop-config.ts +0 -22
- package/tsconfig.json +0 -20
- package/vitest.config.ts +0 -14
|
@@ -1,374 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for middleware modules.
|
|
3
|
-
*
|
|
4
|
-
* Tests: authMiddleware, currencyMiddleware, retryMiddleware,
|
|
5
|
-
* timeoutMiddleware, errorMiddleware.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
9
|
-
import { authMiddleware } from '../../core/middleware/auth';
|
|
10
|
-
import { currencyMiddleware } from '../../core/middleware/currency';
|
|
11
|
-
import { retryMiddleware } from '../../core/middleware/retry';
|
|
12
|
-
import { timeoutMiddleware } from '../../core/middleware/timeout';
|
|
13
|
-
import { errorMiddleware } from '../../core/middleware/errors';
|
|
14
|
-
import { StorefrontError, ErrorCodes } 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
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
// authMiddleware
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
|
|
47
|
-
describe('authMiddleware', () => {
|
|
48
|
-
it('should add Authorization header when token is available', async () => {
|
|
49
|
-
const mw = authMiddleware(() => 'my-token');
|
|
50
|
-
const next = makeNext();
|
|
51
|
-
const req = makeRequest();
|
|
52
|
-
|
|
53
|
-
await mw(req, next);
|
|
54
|
-
|
|
55
|
-
expect(req.headers['Authorization']).toBe('Bearer my-token');
|
|
56
|
-
expect(next).toHaveBeenCalledWith(req);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('should not add header when token is null', async () => {
|
|
60
|
-
const mw = authMiddleware(() => null);
|
|
61
|
-
const next = makeNext();
|
|
62
|
-
const req = makeRequest();
|
|
63
|
-
|
|
64
|
-
await mw(req, next);
|
|
65
|
-
|
|
66
|
-
expect(req.headers['Authorization']).toBeUndefined();
|
|
67
|
-
expect(next).toHaveBeenCalled();
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('should not add header when token is undefined', async () => {
|
|
71
|
-
const mw = authMiddleware(() => undefined);
|
|
72
|
-
const next = makeNext();
|
|
73
|
-
const req = makeRequest();
|
|
74
|
-
|
|
75
|
-
await mw(req, next);
|
|
76
|
-
|
|
77
|
-
expect(req.headers['Authorization']).toBeUndefined();
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('should resolve token lazily on each call', async () => {
|
|
81
|
-
let token: string | null = null;
|
|
82
|
-
const mw = authMiddleware(() => token);
|
|
83
|
-
const next = makeNext();
|
|
84
|
-
|
|
85
|
-
const req1 = makeRequest();
|
|
86
|
-
await mw(req1, next);
|
|
87
|
-
expect(req1.headers['Authorization']).toBeUndefined();
|
|
88
|
-
|
|
89
|
-
token = 'new-token';
|
|
90
|
-
const req2 = makeRequest();
|
|
91
|
-
await mw(req2, next);
|
|
92
|
-
expect(req2.headers['Authorization']).toBe('Bearer new-token');
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
// ---------------------------------------------------------------------------
|
|
97
|
-
// currencyMiddleware
|
|
98
|
-
// ---------------------------------------------------------------------------
|
|
99
|
-
|
|
100
|
-
describe('currencyMiddleware', () => {
|
|
101
|
-
it('should add X-Preferred-Currency header when currency is available', async () => {
|
|
102
|
-
const mw = currencyMiddleware(() => 'EUR');
|
|
103
|
-
const next = makeNext();
|
|
104
|
-
const req = makeRequest();
|
|
105
|
-
|
|
106
|
-
await mw(req, next);
|
|
107
|
-
|
|
108
|
-
expect(req.headers['X-Preferred-Currency']).toBe('EUR');
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('should not add header when currency is null', async () => {
|
|
112
|
-
const mw = currencyMiddleware(() => null);
|
|
113
|
-
const next = makeNext();
|
|
114
|
-
const req = makeRequest();
|
|
115
|
-
|
|
116
|
-
await mw(req, next);
|
|
117
|
-
|
|
118
|
-
expect(req.headers['X-Preferred-Currency']).toBeUndefined();
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('should resolve currency lazily', async () => {
|
|
122
|
-
let currency: string | null = 'PLN';
|
|
123
|
-
const mw = currencyMiddleware(() => currency);
|
|
124
|
-
const next = makeNext();
|
|
125
|
-
|
|
126
|
-
const req1 = makeRequest();
|
|
127
|
-
await mw(req1, next);
|
|
128
|
-
expect(req1.headers['X-Preferred-Currency']).toBe('PLN');
|
|
129
|
-
|
|
130
|
-
currency = 'USD';
|
|
131
|
-
const req2 = makeRequest();
|
|
132
|
-
await mw(req2, next);
|
|
133
|
-
expect(req2.headers['X-Preferred-Currency']).toBe('USD');
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
// ---------------------------------------------------------------------------
|
|
138
|
-
// retryMiddleware
|
|
139
|
-
// ---------------------------------------------------------------------------
|
|
140
|
-
|
|
141
|
-
describe('retryMiddleware', () => {
|
|
142
|
-
it('should return response on success', async () => {
|
|
143
|
-
const mw = retryMiddleware({ maxRetries: 2, initialDelay: 1 });
|
|
144
|
-
const successResponse = makeResponse({ data: { ok: true } });
|
|
145
|
-
const next = makeNext(successResponse);
|
|
146
|
-
|
|
147
|
-
const result = await mw(makeRequest(), next);
|
|
148
|
-
expect(result.data).toEqual({ ok: true });
|
|
149
|
-
expect(next).toHaveBeenCalledTimes(1);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('should NEVER retry mutations', async () => {
|
|
153
|
-
const mw = retryMiddleware({ maxRetries: 3, initialDelay: 1 });
|
|
154
|
-
const next = vi.fn(async () => makeResponse({ status: 500 }));
|
|
155
|
-
|
|
156
|
-
const result = await mw(makeRequest({ isMutation: true }), next);
|
|
157
|
-
expect(result.status).toBe(500);
|
|
158
|
-
expect(next).toHaveBeenCalledTimes(1); // No retry
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('should retry on 5xx responses for queries', async () => {
|
|
162
|
-
let callCount = 0;
|
|
163
|
-
const next: ExecuteFn = vi.fn(async () => {
|
|
164
|
-
callCount++;
|
|
165
|
-
if (callCount < 3) return makeResponse({ status: 500 });
|
|
166
|
-
return makeResponse({ data: { retried: true } });
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
const mw = retryMiddleware({ maxRetries: 3, initialDelay: 1 });
|
|
170
|
-
const result = await mw(makeRequest(), next);
|
|
171
|
-
|
|
172
|
-
expect(result.data).toEqual({ retried: true });
|
|
173
|
-
expect(next).toHaveBeenCalledTimes(3);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('should retry on network errors for queries', async () => {
|
|
177
|
-
let callCount = 0;
|
|
178
|
-
const next: ExecuteFn = vi.fn(async () => {
|
|
179
|
-
callCount++;
|
|
180
|
-
if (callCount < 2) throw new TypeError('fetch failed');
|
|
181
|
-
return makeResponse({ data: { recovered: true } });
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
const mw = retryMiddleware({ maxRetries: 2, initialDelay: 1 });
|
|
185
|
-
const result = await mw(makeRequest(), next);
|
|
186
|
-
|
|
187
|
-
expect(result.data).toEqual({ recovered: true });
|
|
188
|
-
expect(next).toHaveBeenCalledTimes(2);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it('should NOT retry 4xx errors', async () => {
|
|
192
|
-
const next: ExecuteFn = vi.fn(async () => {
|
|
193
|
-
throw new StorefrontError({ code: ErrorCodes.HTTP_ERROR, message: 'Not Found', status: 404 });
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
const mw = retryMiddleware({ maxRetries: 3, initialDelay: 1 });
|
|
197
|
-
|
|
198
|
-
await expect(mw(makeRequest(), next)).rejects.toThrow(StorefrontError);
|
|
199
|
-
expect(next).toHaveBeenCalledTimes(1);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it('should retry on TIMEOUT errors', async () => {
|
|
203
|
-
let callCount = 0;
|
|
204
|
-
const next: ExecuteFn = vi.fn(async () => {
|
|
205
|
-
callCount++;
|
|
206
|
-
if (callCount < 2) throw new StorefrontError({ code: ErrorCodes.TIMEOUT, message: 'timeout' });
|
|
207
|
-
return makeResponse({ data: { ok: true } });
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
const mw = retryMiddleware({ maxRetries: 2, initialDelay: 1 });
|
|
211
|
-
const result = await mw(makeRequest(), next);
|
|
212
|
-
|
|
213
|
-
expect(result.data).toEqual({ ok: true });
|
|
214
|
-
expect(next).toHaveBeenCalledTimes(2);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it('should throw after exhausting all retries', async () => {
|
|
218
|
-
const next: ExecuteFn = vi.fn(async () => {
|
|
219
|
-
throw new TypeError('persistent failure');
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
const mw = retryMiddleware({ maxRetries: 2, initialDelay: 1 });
|
|
223
|
-
|
|
224
|
-
await expect(mw(makeRequest(), next)).rejects.toThrow('persistent failure');
|
|
225
|
-
expect(next).toHaveBeenCalledTimes(3); // 1 initial + 2 retries
|
|
226
|
-
});
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
// ---------------------------------------------------------------------------
|
|
230
|
-
// timeoutMiddleware
|
|
231
|
-
// ---------------------------------------------------------------------------
|
|
232
|
-
|
|
233
|
-
describe('timeoutMiddleware', () => {
|
|
234
|
-
it('should return response before timeout', async () => {
|
|
235
|
-
const mw = timeoutMiddleware({ timeout: 1000 });
|
|
236
|
-
const next = makeNext(makeResponse({ data: { fast: true } }));
|
|
237
|
-
|
|
238
|
-
const result = await mw(makeRequest(), next);
|
|
239
|
-
expect(result.data).toEqual({ fast: true });
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it('should throw StorefrontError with TIMEOUT code on timeout', async () => {
|
|
243
|
-
const mw = timeoutMiddleware({ timeout: 10 });
|
|
244
|
-
const next: ExecuteFn = async (req) => {
|
|
245
|
-
await new Promise((resolve, reject) => {
|
|
246
|
-
const timer = setTimeout(resolve, 5000);
|
|
247
|
-
req.signal?.addEventListener('abort', () => {
|
|
248
|
-
clearTimeout(timer);
|
|
249
|
-
reject(new DOMException('aborted', 'AbortError'));
|
|
250
|
-
});
|
|
251
|
-
});
|
|
252
|
-
return makeResponse();
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
try {
|
|
256
|
-
await mw(makeRequest(), next);
|
|
257
|
-
expect.fail('Should have thrown');
|
|
258
|
-
} catch (err) {
|
|
259
|
-
expect(err).toBeInstanceOf(StorefrontError);
|
|
260
|
-
expect((err as StorefrontError).code).toBe(ErrorCodes.TIMEOUT);
|
|
261
|
-
expect((err as StorefrontError).isTimeout).toBe(true);
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
// ---------------------------------------------------------------------------
|
|
267
|
-
// errorMiddleware
|
|
268
|
-
// ---------------------------------------------------------------------------
|
|
269
|
-
|
|
270
|
-
describe('errorMiddleware', () => {
|
|
271
|
-
it('should pass through successful responses with data', async () => {
|
|
272
|
-
const mw = errorMiddleware();
|
|
273
|
-
const response = makeResponse({ data: { shop: { id: '1' } } });
|
|
274
|
-
const next = makeNext(response);
|
|
275
|
-
|
|
276
|
-
const result = await mw(makeRequest(), next);
|
|
277
|
-
expect(result.data).toEqual({ shop: { id: '1' } });
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
it('should throw GRAPHQL_ERROR for responses with errors array', async () => {
|
|
281
|
-
const mw = errorMiddleware();
|
|
282
|
-
const next = makeNext(makeResponse({
|
|
283
|
-
data: null,
|
|
284
|
-
errors: [{ message: 'Not found' }],
|
|
285
|
-
status: 200,
|
|
286
|
-
}));
|
|
287
|
-
|
|
288
|
-
try {
|
|
289
|
-
await mw(makeRequest(), next);
|
|
290
|
-
expect.fail('Should have thrown');
|
|
291
|
-
} catch (err) {
|
|
292
|
-
expect(err).toBeInstanceOf(StorefrontError);
|
|
293
|
-
const sfErr = err as StorefrontError;
|
|
294
|
-
expect(sfErr.code).toBe(ErrorCodes.GRAPHQL_ERROR);
|
|
295
|
-
expect(sfErr.message).toBe('Not found');
|
|
296
|
-
expect(sfErr.graphqlErrors).toHaveLength(1);
|
|
297
|
-
}
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
it('should throw HTTP_ERROR for 4xx/5xx responses', async () => {
|
|
301
|
-
const mw = errorMiddleware();
|
|
302
|
-
const next = makeNext(makeResponse({ status: 401 }));
|
|
303
|
-
|
|
304
|
-
try {
|
|
305
|
-
await mw(makeRequest(), next);
|
|
306
|
-
expect.fail('Should have thrown');
|
|
307
|
-
} catch (err) {
|
|
308
|
-
expect(err).toBeInstanceOf(StorefrontError);
|
|
309
|
-
const sfErr = err as StorefrontError;
|
|
310
|
-
expect(sfErr.code).toBe(ErrorCodes.HTTP_ERROR);
|
|
311
|
-
expect(sfErr.status).toBe(401);
|
|
312
|
-
}
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
it('should throw NETWORK_ERROR for fetch failures', async () => {
|
|
316
|
-
const mw = errorMiddleware();
|
|
317
|
-
const next: ExecuteFn = async () => { throw new TypeError('fetch failed'); };
|
|
318
|
-
|
|
319
|
-
try {
|
|
320
|
-
await mw(makeRequest(), next);
|
|
321
|
-
expect.fail('Should have thrown');
|
|
322
|
-
} catch (err) {
|
|
323
|
-
expect(err).toBeInstanceOf(StorefrontError);
|
|
324
|
-
const sfErr = err as StorefrontError;
|
|
325
|
-
expect(sfErr.code).toBe(ErrorCodes.NETWORK_ERROR);
|
|
326
|
-
expect(sfErr.isNetworkError).toBe(true);
|
|
327
|
-
}
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
it('should throw TIMEOUT for AbortError', async () => {
|
|
331
|
-
const mw = errorMiddleware();
|
|
332
|
-
const next: ExecuteFn = async () => {
|
|
333
|
-
throw new DOMException('The operation was aborted', 'AbortError');
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
try {
|
|
337
|
-
await mw(makeRequest(), next);
|
|
338
|
-
expect.fail('Should have thrown');
|
|
339
|
-
} catch (err) {
|
|
340
|
-
expect(err).toBeInstanceOf(StorefrontError);
|
|
341
|
-
expect((err as StorefrontError).code).toBe(ErrorCodes.TIMEOUT);
|
|
342
|
-
}
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
it('should throw NO_DATA when response has null data', async () => {
|
|
346
|
-
const mw = errorMiddleware();
|
|
347
|
-
const next = makeNext(makeResponse({ data: null, status: 200 }));
|
|
348
|
-
|
|
349
|
-
try {
|
|
350
|
-
await mw(makeRequest(), next);
|
|
351
|
-
expect.fail('Should have thrown');
|
|
352
|
-
} catch (err) {
|
|
353
|
-
expect(err).toBeInstanceOf(StorefrontError);
|
|
354
|
-
expect((err as StorefrontError).code).toBe(ErrorCodes.NO_DATA);
|
|
355
|
-
}
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
it('should re-throw existing StorefrontErrors as-is', async () => {
|
|
359
|
-
const mw = errorMiddleware();
|
|
360
|
-
const original = new StorefrontError({
|
|
361
|
-
code: ErrorCodes.USER_ERROR,
|
|
362
|
-
message: 'validation failed',
|
|
363
|
-
userErrors: [{ message: 'Invalid email' }],
|
|
364
|
-
});
|
|
365
|
-
const next: ExecuteFn = async () => { throw original; };
|
|
366
|
-
|
|
367
|
-
try {
|
|
368
|
-
await mw(makeRequest(), next);
|
|
369
|
-
expect.fail('Should have thrown');
|
|
370
|
-
} catch (err) {
|
|
371
|
-
expect(err).toBe(original); // Same reference
|
|
372
|
-
}
|
|
373
|
-
});
|
|
374
|
-
});
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared test helpers — mock fetch factory, mock client builder.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { GraphQLResponse } from '../../core/client/types';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Create a mock fetch that returns the specified GraphQL response.
|
|
9
|
-
*/
|
|
10
|
-
export function createMockFetch(
|
|
11
|
-
responseData: unknown,
|
|
12
|
-
options?: {
|
|
13
|
-
errors?: Array<{ message: string }>;
|
|
14
|
-
status?: number;
|
|
15
|
-
delay?: number;
|
|
16
|
-
},
|
|
17
|
-
): typeof globalThis.fetch {
|
|
18
|
-
const { errors, status = 200, delay: delayMs } = options ?? {};
|
|
19
|
-
|
|
20
|
-
return async (_url: string | URL | Request, _init?: RequestInit) => {
|
|
21
|
-
if (delayMs) {
|
|
22
|
-
await new Promise(r => setTimeout(r, delayMs));
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Check for abort signal
|
|
26
|
-
if (_init?.signal?.aborted) {
|
|
27
|
-
throw new DOMException('The operation was aborted', 'AbortError');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const body: Record<string, unknown> = { data: responseData };
|
|
31
|
-
if (errors) body.errors = errors;
|
|
32
|
-
|
|
33
|
-
return new Response(JSON.stringify(body), {
|
|
34
|
-
status,
|
|
35
|
-
headers: { 'Content-Type': 'application/json' },
|
|
36
|
-
});
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Create a mock fetch that tracks all calls and returns specified response.
|
|
42
|
-
*/
|
|
43
|
-
export function createSpyFetch(
|
|
44
|
-
responseData: unknown,
|
|
45
|
-
options?: {
|
|
46
|
-
errors?: Array<{ message: string }>;
|
|
47
|
-
status?: number;
|
|
48
|
-
},
|
|
49
|
-
) {
|
|
50
|
-
const calls: Array<{ url: string; init: RequestInit }> = [];
|
|
51
|
-
const { errors, status = 200 } = options ?? {};
|
|
52
|
-
|
|
53
|
-
const fetchFn = async (url: string | URL | Request, init?: RequestInit) => {
|
|
54
|
-
calls.push({ url: url.toString(), init: init! });
|
|
55
|
-
|
|
56
|
-
if (init?.signal?.aborted) {
|
|
57
|
-
throw new DOMException('The operation was aborted', 'AbortError');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const body: Record<string, unknown> = { data: responseData };
|
|
61
|
-
if (errors) body.errors = errors;
|
|
62
|
-
|
|
63
|
-
return new Response(JSON.stringify(body), {
|
|
64
|
-
status,
|
|
65
|
-
headers: { 'Content-Type': 'application/json' },
|
|
66
|
-
});
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
return { fetch: fetchFn as typeof globalThis.fetch, calls };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Create a mock fetch that fails with a network error.
|
|
74
|
-
*/
|
|
75
|
-
export function createNetworkErrorFetch(errorMessage = 'fetch failed'): typeof globalThis.fetch {
|
|
76
|
-
return async () => {
|
|
77
|
-
throw new TypeError(errorMessage);
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Create a mock fetch that returns different responses on subsequent calls.
|
|
83
|
-
*/
|
|
84
|
-
export function createSequenceFetch(
|
|
85
|
-
responses: Array<{ data?: unknown; errors?: Array<{ message: string }>; status?: number }>,
|
|
86
|
-
): { fetch: typeof globalThis.fetch; callCount: () => number } {
|
|
87
|
-
let index = 0;
|
|
88
|
-
|
|
89
|
-
const fetchFn = async (_url: string | URL | Request, _init?: RequestInit) => {
|
|
90
|
-
const response = responses[Math.min(index, responses.length - 1)];
|
|
91
|
-
index++;
|
|
92
|
-
|
|
93
|
-
const body: Record<string, unknown> = { data: response.data ?? null };
|
|
94
|
-
if (response.errors) body.errors = response.errors;
|
|
95
|
-
|
|
96
|
-
return new Response(JSON.stringify(body), {
|
|
97
|
-
status: response.status ?? 200,
|
|
98
|
-
headers: { 'Content-Type': 'application/json' },
|
|
99
|
-
});
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
return { fetch: fetchFn as typeof globalThis.fetch, callCount: () => index };
|
|
103
|
-
}
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AuthClient — plain async API for customer authentication (no React).
|
|
3
|
-
*
|
|
4
|
-
* Wraps StorefrontClient.mutate/query with typed auth operations.
|
|
5
|
-
* Auto-throws on userErrors via assertNoUserErrors.
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* ```typescript
|
|
9
|
-
* const authClient = new AuthClient(storefrontClient);
|
|
10
|
-
*
|
|
11
|
-
* const result = await authClient.login('user@example.com', 'password');
|
|
12
|
-
* console.log(result.accessToken); // JWT token
|
|
13
|
-
*
|
|
14
|
-
* const customer = await authClient.getCustomer(result.accessToken);
|
|
15
|
-
* ```
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import type { StorefrontClient } from '../client/types';
|
|
19
|
-
import type { AuthResult, Customer, CustomerCreateInput } from './types';
|
|
20
|
-
import { assertNoUserErrors } from '../helpers/assert-no-user-errors';
|
|
21
|
-
import {
|
|
22
|
-
CUSTOMER_LOGIN,
|
|
23
|
-
CUSTOMER_LOGOUT,
|
|
24
|
-
CUSTOMER_TOKEN_RENEW,
|
|
25
|
-
CUSTOMER_CREATE,
|
|
26
|
-
CUSTOMER_QUERY,
|
|
27
|
-
} from '../operations/auth';
|
|
28
|
-
|
|
29
|
-
export class AuthClient {
|
|
30
|
-
constructor(private readonly client: StorefrontClient) {}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Login with email and password.
|
|
34
|
-
* Returns access token + expiry.
|
|
35
|
-
*/
|
|
36
|
-
async login(email: string, password: string): Promise<AuthResult> {
|
|
37
|
-
const data = await this.client.mutate<{
|
|
38
|
-
customerAccessTokenCreate: {
|
|
39
|
-
customerAccessToken: { accessToken: string; expiresAt: string } | null;
|
|
40
|
-
userErrors: Array<{ message: string; field?: string[]; code?: string }>;
|
|
41
|
-
};
|
|
42
|
-
}>(CUSTOMER_LOGIN, { input: { email, password } });
|
|
43
|
-
|
|
44
|
-
assertNoUserErrors(data.customerAccessTokenCreate);
|
|
45
|
-
|
|
46
|
-
const token = data.customerAccessTokenCreate.customerAccessToken!;
|
|
47
|
-
return {
|
|
48
|
-
accessToken: token.accessToken,
|
|
49
|
-
expiresAt: token.expiresAt,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Logout — invalidate token on backend.
|
|
55
|
-
* Does not throw on failure (token may already be expired).
|
|
56
|
-
*/
|
|
57
|
-
async logout(token: string): Promise<void> {
|
|
58
|
-
try {
|
|
59
|
-
await this.client.mutate<{
|
|
60
|
-
customerAccessTokenDelete: {
|
|
61
|
-
deletedAccessToken: string | null;
|
|
62
|
-
userErrors: Array<{ message: string; field?: string[]; code?: string }>;
|
|
63
|
-
};
|
|
64
|
-
}>(CUSTOMER_LOGOUT, { customerAccessToken: token });
|
|
65
|
-
} catch {
|
|
66
|
-
// Silently ignore — token may already be expired
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Renew access token — extends expiry.
|
|
72
|
-
*/
|
|
73
|
-
async renewToken(token: string): Promise<AuthResult> {
|
|
74
|
-
const data = await this.client.mutate<{
|
|
75
|
-
customerAccessTokenRenew: {
|
|
76
|
-
customerAccessToken: { accessToken: string; expiresAt: string } | null;
|
|
77
|
-
userErrors: Array<{ message: string; field?: string[]; code?: string }>;
|
|
78
|
-
};
|
|
79
|
-
}>(CUSTOMER_TOKEN_RENEW, { customerAccessToken: token });
|
|
80
|
-
|
|
81
|
-
assertNoUserErrors(data.customerAccessTokenRenew);
|
|
82
|
-
|
|
83
|
-
const renewed = data.customerAccessTokenRenew.customerAccessToken!;
|
|
84
|
-
return {
|
|
85
|
-
accessToken: renewed.accessToken,
|
|
86
|
-
expiresAt: renewed.expiresAt,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Register new customer account.
|
|
92
|
-
* Returns access token + customer data.
|
|
93
|
-
*/
|
|
94
|
-
async register(input: CustomerCreateInput): Promise<AuthResult> {
|
|
95
|
-
const data = await this.client.mutate<{
|
|
96
|
-
customerCreate: {
|
|
97
|
-
customer: Customer | null;
|
|
98
|
-
customerAccessToken: { accessToken: string; expiresAt: string } | null;
|
|
99
|
-
userErrors: Array<{ message: string; field?: string[]; code?: string }>;
|
|
100
|
-
};
|
|
101
|
-
}>(CUSTOMER_CREATE, { input });
|
|
102
|
-
|
|
103
|
-
assertNoUserErrors(data.customerCreate);
|
|
104
|
-
|
|
105
|
-
const token = data.customerCreate.customerAccessToken!;
|
|
106
|
-
return {
|
|
107
|
-
accessToken: token.accessToken,
|
|
108
|
-
expiresAt: token.expiresAt,
|
|
109
|
-
customer: data.customerCreate.customer ?? undefined,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Fetch customer data by access token.
|
|
115
|
-
*/
|
|
116
|
-
async getCustomer(accessToken: string): Promise<Customer | null> {
|
|
117
|
-
const data = await this.client.query<{
|
|
118
|
-
customer: Customer | null;
|
|
119
|
-
}>(CUSTOMER_QUERY, { customerAccessToken: accessToken });
|
|
120
|
-
|
|
121
|
-
return data.customer;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Auth Cookie Configuration — platform contract.
|
|
3
|
-
*
|
|
4
|
-
* Backend validates cookie by name `customerAccessToken`.
|
|
5
|
-
* Every storefront MUST use the same name. Security options
|
|
6
|
-
* (httpOnly, secure, sameSite) are the platform standard.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
export const AUTH_COOKIE_NAME = 'customerAccessToken';
|
|
10
|
-
|
|
11
|
-
export const AUTH_COOKIE_DEFAULTS = {
|
|
12
|
-
name: AUTH_COOKIE_NAME,
|
|
13
|
-
path: '/',
|
|
14
|
-
sameSite: 'lax' as const,
|
|
15
|
-
httpOnly: true,
|
|
16
|
-
secure:
|
|
17
|
-
typeof window !== 'undefined'
|
|
18
|
-
? window.location.protocol === 'https:'
|
|
19
|
-
: process.env.NODE_ENV === 'production',
|
|
20
|
-
maxAge: 60 * 60 * 24 * 30, // 30 days
|
|
21
|
-
} as const;
|
|
22
|
-
|
|
23
|
-
export type AuthCookieConfig = typeof AUTH_COOKIE_DEFAULTS;
|