@doswiftly/storefront-sdk 4.0.0 → 4.2.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.
- package/README.md +51 -9
- package/dist/core/bot-protection/abstract-manager.d.ts +57 -0
- package/dist/core/bot-protection/abstract-manager.d.ts.map +1 -0
- package/dist/core/bot-protection/abstract-manager.js +144 -0
- package/dist/core/bot-protection/create-manager.d.ts +15 -0
- package/dist/core/bot-protection/create-manager.d.ts.map +1 -0
- package/dist/core/bot-protection/create-manager.js +33 -0
- package/dist/core/bot-protection/eucaptcha-manager.d.ts +15 -0
- package/dist/core/bot-protection/eucaptcha-manager.d.ts.map +1 -0
- package/dist/core/bot-protection/eucaptcha-manager.js +76 -0
- package/dist/core/bot-protection/fallback-manager.d.ts +18 -0
- package/dist/core/bot-protection/fallback-manager.d.ts.map +1 -0
- package/dist/core/bot-protection/fallback-manager.js +42 -0
- package/dist/core/bot-protection/turnstile-manager.d.ts +15 -0
- package/dist/core/bot-protection/turnstile-manager.d.ts.map +1 -0
- package/dist/core/bot-protection/turnstile-manager.js +78 -0
- package/dist/core/cart/cookie-config.d.ts +14 -0
- package/dist/core/cart/cookie-config.d.ts.map +1 -0
- package/dist/core/cart/cookie-config.js +13 -0
- package/dist/core/currency/cookie-config.d.ts +14 -0
- package/dist/core/currency/cookie-config.d.ts.map +1 -0
- package/dist/core/currency/cookie-config.js +13 -0
- package/dist/core/image.d.ts +55 -0
- package/dist/core/image.d.ts.map +1 -0
- package/dist/core/image.js +48 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +13 -0
- package/dist/core/language/cookie-config.d.ts +14 -0
- package/dist/core/language/cookie-config.d.ts.map +1 -0
- package/dist/core/language/cookie-config.js +13 -0
- package/dist/core/middleware/bot-protection.d.ts +71 -0
- package/dist/core/middleware/bot-protection.d.ts.map +1 -0
- package/dist/core/middleware/bot-protection.js +63 -0
- package/dist/core/middleware/currency.d.ts.map +1 -1
- package/dist/core/middleware/currency.js +2 -1
- package/dist/core/middleware/language.d.ts +18 -0
- package/dist/core/middleware/language.d.ts.map +1 -0
- package/dist/core/middleware/language.js +25 -0
- package/dist/react/bot-protection/bot-protection-context.d.ts +12 -0
- package/dist/react/bot-protection/bot-protection-context.d.ts.map +1 -0
- package/dist/react/bot-protection/bot-protection-context.js +9 -0
- package/dist/react/bot-protection/bot-protection-widget.d.ts +13 -0
- package/dist/react/bot-protection/bot-protection-widget.d.ts.map +1 -0
- package/dist/react/bot-protection/bot-protection-widget.js +34 -0
- package/dist/react/cookies.d.ts +17 -0
- package/dist/react/cookies.d.ts.map +1 -1
- package/dist/react/cookies.js +36 -3
- package/dist/react/hooks/use-bot-protection.d.ts +16 -0
- package/dist/react/hooks/use-bot-protection.d.ts.map +1 -0
- package/dist/react/hooks/use-bot-protection.js +24 -0
- package/dist/react/index.d.ts +10 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +9 -1
- package/dist/react/providers/language-provider.d.ts +18 -0
- package/dist/react/providers/language-provider.d.ts.map +1 -0
- package/dist/react/providers/language-provider.js +24 -0
- package/dist/react/providers/storefront-client-provider.d.ts +7 -2
- package/dist/react/providers/storefront-client-provider.d.ts.map +1 -1
- package/dist/react/providers/storefront-client-provider.js +14 -3
- package/dist/react/providers/storefront-provider.d.ts +7 -1
- package/dist/react/providers/storefront-provider.d.ts.map +1 -1
- package/dist/react/providers/storefront-provider.js +11 -4
- package/dist/react/stores/cart.context.d.ts +12 -0
- package/dist/react/stores/cart.context.d.ts.map +1 -0
- package/dist/react/stores/cart.context.js +3 -0
- package/dist/react/stores/cart.store.d.ts +71 -0
- package/dist/react/stores/cart.store.d.ts.map +1 -0
- package/dist/react/stores/cart.store.js +166 -0
- package/dist/react/stores/currency.store.d.ts +6 -9
- package/dist/react/stores/currency.store.d.ts.map +1 -1
- package/dist/react/stores/currency.store.js +5 -22
- package/dist/react/stores/language.store.d.ts +33 -0
- package/dist/react/stores/language.store.d.ts.map +1 -0
- package/dist/react/stores/language.store.js +67 -0
- package/dist/react/stores/store-context.d.ts +5 -0
- package/dist/react/stores/store-context.d.ts.map +1 -1
- package/dist/react/stores/store-context.js +14 -0
- package/dist/react/types/shop-config.d.ts +19 -0
- package/dist/react/types/shop-config.d.ts.map +1 -0
- package/dist/react/types/shop-config.js +7 -0
- package/package.json +1 -1
- package/src/__tests__/unit/bot-protection.test.ts +461 -0
- package/src/__tests__/unit/cart-store.test.ts +349 -0
- package/src/core/bot-protection/abstract-manager.ts +185 -0
- package/src/core/bot-protection/create-manager.ts +37 -0
- package/src/core/bot-protection/eucaptcha-manager.ts +88 -0
- package/src/core/bot-protection/fallback-manager.ts +43 -0
- package/src/core/bot-protection/turnstile-manager.ts +92 -0
- package/src/core/bot-protection/types/eucaptcha.d.ts +28 -0
- package/src/core/bot-protection/types/turnstile.d.ts +33 -0
- package/src/core/cart/cookie-config.ts +13 -0
- package/src/core/currency/cookie-config.ts +13 -0
- package/src/core/image.ts +75 -0
- package/src/core/index.ts +30 -0
- package/src/core/language/cookie-config.ts +13 -0
- package/src/core/middleware/bot-protection.ts +140 -0
- package/src/core/middleware/currency.ts +2 -1
- package/src/core/middleware/language.ts +30 -0
- package/src/react/bot-protection/bot-protection-context.ts +17 -0
- package/src/react/bot-protection/bot-protection-widget.tsx +46 -0
- package/src/react/cookies.ts +39 -4
- package/src/react/hooks/use-bot-protection.ts +31 -0
- package/src/react/index.ts +27 -1
- package/src/react/providers/language-provider.tsx +34 -0
- package/src/react/providers/storefront-client-provider.tsx +20 -3
- package/src/react/providers/storefront-provider.tsx +34 -6
- package/src/react/stores/cart.context.ts +10 -0
- package/src/react/stores/cart.store.ts +254 -0
- package/src/react/stores/currency.store.ts +12 -32
- package/src/react/stores/language.store.ts +90 -0
- package/src/react/stores/store-context.tsx +21 -0
- package/src/react/types/shop-config.ts +22 -0
|
@@ -0,0 +1,461 @@
|
|
|
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
|
+
});
|