@prosopo/procaptcha 2.10.57 → 2.10.59
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/.turbo/turbo-build$colon$cjs.log +15 -14
- package/.turbo/turbo-build$colon$tsc.log +20 -20
- package/.turbo/turbo-build.log +17 -14
- package/CHANGELOG.md +48 -0
- package/dist/_virtual/_rolldown/runtime.js +3 -0
- package/dist/cjs/index.cjs +6 -8
- package/dist/cjs/modules/Manager.cjs +288 -384
- package/dist/cjs/modules/ProsopoCaptchaApi.cjs +41 -74
- package/dist/cjs/modules/collector.cjs +43 -73
- package/dist/cjs/modules/index.cjs +6 -8
- package/dist/index.js +3 -7
- package/dist/modules/Manager.d.ts.map +1 -1
- package/dist/modules/Manager.js +285 -382
- package/dist/modules/Manager.js.map +1 -1
- package/dist/modules/ProsopoCaptchaApi.d.ts.map +1 -1
- package/dist/modules/ProsopoCaptchaApi.js +40 -74
- package/dist/modules/ProsopoCaptchaApi.js.map +1 -1
- package/dist/modules/collector.js +44 -74
- package/dist/modules/index.js +2 -6
- package/dist/tests/collector.unit.test.d.ts +2 -0
- package/dist/tests/collector.unit.test.d.ts.map +1 -0
- package/dist/tests/collector.unit.test.js +173 -0
- package/dist/tests/collector.unit.test.js.map +1 -0
- package/dist/tests/manager.unit.test.d.ts +2 -0
- package/dist/tests/manager.unit.test.d.ts.map +1 -0
- package/dist/tests/manager.unit.test.js +788 -0
- package/dist/tests/manager.unit.test.js.map +1 -0
- package/dist/tests/managerHarness.d.ts +18 -0
- package/dist/tests/managerHarness.d.ts.map +1 -0
- package/dist/tests/managerHarness.js +94 -0
- package/dist/tests/managerHarness.js.map +1 -0
- package/dist/tests/procaptcha.test-d.d.ts +2 -0
- package/dist/tests/procaptcha.test-d.d.ts.map +1 -0
- package/dist/tests/procaptcha.test-d.js +78 -0
- package/dist/tests/procaptcha.test-d.js.map +1 -0
- package/dist/tests/prosopoCaptchaApi.unit.test.d.ts +2 -0
- package/dist/tests/prosopoCaptchaApi.unit.test.d.ts.map +1 -0
- package/dist/tests/prosopoCaptchaApi.unit.test.js +155 -0
- package/dist/tests/prosopoCaptchaApi.unit.test.js.map +1 -0
- package/package.json +17 -12
- package/vite.test.config.ts +25 -0
|
@@ -0,0 +1,788 @@
|
|
|
1
|
+
import { ApiParams, CaptchaType, decodeProcaptchaOutput, } from "@prosopo/types";
|
|
2
|
+
import { extractData } from "@prosopo/util";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, test, vi, } from "vitest";
|
|
4
|
+
import { Manager } from "../modules/Manager.js";
|
|
5
|
+
import { OTHER_PROVIDER_URL, PROVIDER_URL, SITE_KEY, USER_ADDRESS, account, accountWithoutExtension, callbacks, captcha, challengeResponse, config, frictionless, randomProvider, signRawMock, solutionResponse, state, } from "./managerHarness.js";
|
|
6
|
+
const mocks = vi.hoisted(() => {
|
|
7
|
+
const providerApiConstructions = [];
|
|
8
|
+
class ProviderApiMock {
|
|
9
|
+
constructor(url, siteKey) {
|
|
10
|
+
providerApiConstructions.push({ url, siteKey });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const getAccount = vi.fn();
|
|
14
|
+
class ExtensionMock {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.getAccount = getAccount;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const extensionLoader = vi.fn();
|
|
20
|
+
const getProcaptchaRandomActiveProvider = vi.fn();
|
|
21
|
+
const sleep = vi.fn();
|
|
22
|
+
const getCaptchaChallenge = vi.fn();
|
|
23
|
+
const submitCaptchaSolution = vi.fn();
|
|
24
|
+
const captchaApiConstructions = [];
|
|
25
|
+
class ProsopoCaptchaApiMock {
|
|
26
|
+
constructor(userAccount, provider, _providerApi, web2, dappAccount) {
|
|
27
|
+
this.getCaptchaChallenge = getCaptchaChallenge;
|
|
28
|
+
this.submitCaptchaSolution = submitCaptchaSolution;
|
|
29
|
+
this.provider = provider;
|
|
30
|
+
captchaApiConstructions.push({
|
|
31
|
+
userAccount,
|
|
32
|
+
provider,
|
|
33
|
+
web2,
|
|
34
|
+
dappAccount,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
providerApiConstructions,
|
|
40
|
+
ProviderApiMock,
|
|
41
|
+
getAccount,
|
|
42
|
+
ExtensionMock,
|
|
43
|
+
extensionLoader,
|
|
44
|
+
getProcaptchaRandomActiveProvider,
|
|
45
|
+
sleep,
|
|
46
|
+
getCaptchaChallenge,
|
|
47
|
+
submitCaptchaSolution,
|
|
48
|
+
captchaApiConstructions,
|
|
49
|
+
ProsopoCaptchaApiMock,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
vi.mock("@prosopo/api", async (importOriginal) => {
|
|
53
|
+
const actual = await importOriginal();
|
|
54
|
+
return { ...actual, ProviderApi: mocks.ProviderApiMock };
|
|
55
|
+
});
|
|
56
|
+
vi.mock("../modules/ProsopoCaptchaApi.js", () => ({
|
|
57
|
+
default: mocks.ProsopoCaptchaApiMock,
|
|
58
|
+
ProsopoCaptchaApi: mocks.ProsopoCaptchaApiMock,
|
|
59
|
+
}));
|
|
60
|
+
vi.mock("@prosopo/procaptcha-common", async (importOriginal) => {
|
|
61
|
+
const actual = await importOriginal();
|
|
62
|
+
return {
|
|
63
|
+
...actual,
|
|
64
|
+
ExtensionLoader: mocks.extensionLoader,
|
|
65
|
+
getProcaptchaRandomActiveProvider: mocks.getProcaptchaRandomActiveProvider,
|
|
66
|
+
providerRetry: (currentFn, retryFn, stateReset, attemptCount, retryMax) => actual.providerRetry(currentFn, retryFn, stateReset, attemptCount, retryMax, 0),
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
vi.mock("@prosopo/util", async (importOriginal) => {
|
|
70
|
+
const actual = await importOriginal();
|
|
71
|
+
return { ...actual, sleep: mocks.sleep };
|
|
72
|
+
});
|
|
73
|
+
const build = (options = {}) => {
|
|
74
|
+
const currentState = state(options.initialState);
|
|
75
|
+
const updates = [];
|
|
76
|
+
const events = {
|
|
77
|
+
onHuman: vi.fn(),
|
|
78
|
+
onFailed: vi.fn(),
|
|
79
|
+
onExpired: vi.fn(),
|
|
80
|
+
onReset: vi.fn(),
|
|
81
|
+
onOpen: vi.fn(),
|
|
82
|
+
onClose: vi.fn(),
|
|
83
|
+
onError: vi.fn(),
|
|
84
|
+
onChallengeExpired: vi.fn(),
|
|
85
|
+
onReload: vi.fn(),
|
|
86
|
+
};
|
|
87
|
+
const restart = vi.fn();
|
|
88
|
+
const callbackInput = callbacks(events);
|
|
89
|
+
const frictionlessState = options.frictionlessState ??
|
|
90
|
+
(options.withFrictionless === false
|
|
91
|
+
? undefined
|
|
92
|
+
: frictionless({ restart }));
|
|
93
|
+
const manager = Manager(options.configInput ?? config(), currentState, (next) => {
|
|
94
|
+
updates.push({ ...next });
|
|
95
|
+
}, callbackInput, frictionlessState, options.honeypot);
|
|
96
|
+
return { manager, state: currentState, updates, events, restart };
|
|
97
|
+
};
|
|
98
|
+
const lastUpdate = (harness, key) => {
|
|
99
|
+
for (let i = harness.updates.length - 1; i >= 0; i--) {
|
|
100
|
+
const update = harness.updates[i];
|
|
101
|
+
if (update && key in update) {
|
|
102
|
+
return update[key];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
};
|
|
107
|
+
const wasUpdated = (harness, key) => harness.updates.some((update) => key in update);
|
|
108
|
+
beforeEach(() => {
|
|
109
|
+
vi.clearAllMocks();
|
|
110
|
+
mocks.providerApiConstructions.length = 0;
|
|
111
|
+
mocks.captchaApiConstructions.length = 0;
|
|
112
|
+
mocks.extensionLoader.mockResolvedValue(mocks.ExtensionMock);
|
|
113
|
+
mocks.getAccount.mockResolvedValue(account(signRawMock));
|
|
114
|
+
mocks.getProcaptchaRandomActiveProvider.mockResolvedValue(randomProvider());
|
|
115
|
+
mocks.sleep.mockResolvedValue(undefined);
|
|
116
|
+
mocks.getCaptchaChallenge.mockResolvedValue(challengeResponse());
|
|
117
|
+
mocks.submitCaptchaSolution.mockResolvedValue([
|
|
118
|
+
solutionResponse(),
|
|
119
|
+
"0xcommitment",
|
|
120
|
+
]);
|
|
121
|
+
signRawMock.mockResolvedValue({ id: 1, signature: "0xuser-signature" });
|
|
122
|
+
});
|
|
123
|
+
afterEach(() => {
|
|
124
|
+
vi.useRealTimers();
|
|
125
|
+
});
|
|
126
|
+
describe("start", () => {
|
|
127
|
+
test("fires onOpen before doing any work", async () => {
|
|
128
|
+
const harness = build();
|
|
129
|
+
await harness.manager.start();
|
|
130
|
+
expect(harness.events.onOpen).toHaveBeenCalledTimes(1);
|
|
131
|
+
});
|
|
132
|
+
test("does nothing when already loading", async () => {
|
|
133
|
+
const harness = build({ initialState: { loading: true } });
|
|
134
|
+
await harness.manager.start();
|
|
135
|
+
expect(mocks.getCaptchaChallenge).not.toHaveBeenCalled();
|
|
136
|
+
expect(harness.updates).toHaveLength(0);
|
|
137
|
+
});
|
|
138
|
+
test("does nothing when the user is already verified", async () => {
|
|
139
|
+
const harness = build({ initialState: { isHuman: true } });
|
|
140
|
+
await harness.manager.start();
|
|
141
|
+
expect(mocks.getCaptchaChallenge).not.toHaveBeenCalled();
|
|
142
|
+
expect(harness.updates).toHaveLength(0);
|
|
143
|
+
});
|
|
144
|
+
test("increments the attempt count from zero", async () => {
|
|
145
|
+
const harness = build();
|
|
146
|
+
await harness.manager.start();
|
|
147
|
+
expect(lastUpdate(harness, "attemptCount")).toBe(1);
|
|
148
|
+
});
|
|
149
|
+
test("increments an existing attempt count", async () => {
|
|
150
|
+
const harness = build({ initialState: { attemptCount: 4 } });
|
|
151
|
+
await harness.manager.start();
|
|
152
|
+
expect(harness.updates[1]).toEqual({ attemptCount: 5 });
|
|
153
|
+
});
|
|
154
|
+
test("snapshots the site key into dappAccount", async () => {
|
|
155
|
+
const harness = build();
|
|
156
|
+
await harness.manager.start();
|
|
157
|
+
expect(lastUpdate(harness, "dappAccount")).toBe(SITE_KEY);
|
|
158
|
+
});
|
|
159
|
+
test("lets the UI catch up with the loading state before working", async () => {
|
|
160
|
+
const harness = build();
|
|
161
|
+
await harness.manager.start();
|
|
162
|
+
expect(mocks.sleep).toHaveBeenCalledWith(100);
|
|
163
|
+
});
|
|
164
|
+
test("copies the frictionless session id into state", async () => {
|
|
165
|
+
const harness = build({
|
|
166
|
+
frictionlessState: frictionless({ sessionId: "session-1" }),
|
|
167
|
+
});
|
|
168
|
+
await harness.manager.start();
|
|
169
|
+
expect(lastUpdate(harness, "sessionId")).toBe("session-1");
|
|
170
|
+
});
|
|
171
|
+
test("carries the frictionless provider through instead of picking one", async () => {
|
|
172
|
+
const harness = build({
|
|
173
|
+
frictionlessState: frictionless({
|
|
174
|
+
provider: randomProvider(OTHER_PROVIDER_URL),
|
|
175
|
+
}),
|
|
176
|
+
});
|
|
177
|
+
await harness.manager.start();
|
|
178
|
+
expect(mocks.getProcaptchaRandomActiveProvider).not.toHaveBeenCalled();
|
|
179
|
+
expect(mocks.providerApiConstructions).toEqual([
|
|
180
|
+
{ url: OTHER_PROVIDER_URL, siteKey: SITE_KEY },
|
|
181
|
+
]);
|
|
182
|
+
});
|
|
183
|
+
test("picks a random provider when frictionless has none", async () => {
|
|
184
|
+
const harness = build({ withFrictionless: false });
|
|
185
|
+
await harness.manager.start();
|
|
186
|
+
expect(mocks.getProcaptchaRandomActiveProvider).toHaveBeenCalledWith("production", undefined, { attempt: 1, excludeUrl: undefined });
|
|
187
|
+
expect(mocks.providerApiConstructions).toEqual([
|
|
188
|
+
{ url: PROVIDER_URL, siteKey: SITE_KEY },
|
|
189
|
+
]);
|
|
190
|
+
});
|
|
191
|
+
test("excludes the previous provider on a retry", async () => {
|
|
192
|
+
const harness = build({ withFrictionless: false });
|
|
193
|
+
mocks.getCaptchaChallenge
|
|
194
|
+
.mockRejectedValueOnce(new Error("provider down"))
|
|
195
|
+
.mockResolvedValue(challengeResponse());
|
|
196
|
+
mocks.getProcaptchaRandomActiveProvider
|
|
197
|
+
.mockResolvedValueOnce(randomProvider(PROVIDER_URL))
|
|
198
|
+
.mockResolvedValue(randomProvider(OTHER_PROVIDER_URL));
|
|
199
|
+
await harness.manager.start();
|
|
200
|
+
expect(mocks.getProcaptchaRandomActiveProvider).toHaveBeenCalledTimes(2);
|
|
201
|
+
expect(mocks.getProcaptchaRandomActiveProvider.mock.calls[1]?.[2]).toEqual({
|
|
202
|
+
attempt: 2,
|
|
203
|
+
excludeUrl: PROVIDER_URL,
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
test("builds the captcha api with the account, provider and site key", async () => {
|
|
207
|
+
const harness = build({ withFrictionless: false });
|
|
208
|
+
await harness.manager.start();
|
|
209
|
+
expect(mocks.captchaApiConstructions).toEqual([
|
|
210
|
+
{
|
|
211
|
+
userAccount: USER_ADDRESS,
|
|
212
|
+
provider: randomProvider(),
|
|
213
|
+
web2: true,
|
|
214
|
+
dappAccount: SITE_KEY,
|
|
215
|
+
},
|
|
216
|
+
]);
|
|
217
|
+
});
|
|
218
|
+
test("stores the captcha api on state", async () => {
|
|
219
|
+
const harness = build();
|
|
220
|
+
await harness.manager.start();
|
|
221
|
+
expect(lastUpdate(harness, "captchaApi")).toBeDefined();
|
|
222
|
+
});
|
|
223
|
+
test("requests the challenge without simd readings when none are offered", async () => {
|
|
224
|
+
const harness = build();
|
|
225
|
+
await harness.manager.start();
|
|
226
|
+
expect(mocks.getCaptchaChallenge).toHaveBeenCalledWith(undefined, undefined);
|
|
227
|
+
});
|
|
228
|
+
test("attaches already-resolved simd readings to the challenge request", async () => {
|
|
229
|
+
const getSimdReadings = vi.fn();
|
|
230
|
+
getSimdReadings.mockResolvedValue("simd-data");
|
|
231
|
+
const harness = build({
|
|
232
|
+
frictionlessState: frictionless({ getSimdReadings }),
|
|
233
|
+
});
|
|
234
|
+
await harness.manager.start();
|
|
235
|
+
expect(getSimdReadings).toHaveBeenCalledWith(0);
|
|
236
|
+
expect(mocks.getCaptchaChallenge).toHaveBeenCalledWith(undefined, "simd-data");
|
|
237
|
+
});
|
|
238
|
+
test("clears a stale session id when frictionless has none", async () => {
|
|
239
|
+
const harness = build({ initialState: { sessionId: "session-9" } });
|
|
240
|
+
await harness.manager.start();
|
|
241
|
+
expect(harness.state.sessionId).toBeUndefined();
|
|
242
|
+
expect(mocks.getCaptchaChallenge).toHaveBeenCalledWith(undefined, undefined);
|
|
243
|
+
});
|
|
244
|
+
test("passes the frictionless session id to the challenge request", async () => {
|
|
245
|
+
const harness = build({
|
|
246
|
+
frictionlessState: frictionless({ sessionId: "session-9" }),
|
|
247
|
+
});
|
|
248
|
+
await harness.manager.start();
|
|
249
|
+
expect(mocks.getCaptchaChallenge).toHaveBeenCalledWith("session-9", undefined);
|
|
250
|
+
});
|
|
251
|
+
test("shows the modal and seeds an empty solution per captcha", async () => {
|
|
252
|
+
const harness = build();
|
|
253
|
+
mocks.getCaptchaChallenge.mockResolvedValue(challengeResponse({ captchas: [captcha(), captcha()] }));
|
|
254
|
+
await harness.manager.start();
|
|
255
|
+
expect(lastUpdate(harness, "showModal")).toBe(true);
|
|
256
|
+
expect(lastUpdate(harness, "solutions")).toEqual([[], []]);
|
|
257
|
+
expect(lastUpdate(harness, "index")).toBe(0);
|
|
258
|
+
expect(lastUpdate(harness, "loading")).toBe(false);
|
|
259
|
+
});
|
|
260
|
+
test("surfaces a challenge error onto state and the error callback", async () => {
|
|
261
|
+
const harness = build();
|
|
262
|
+
mocks.getCaptchaChallenge.mockResolvedValue(challengeResponse({
|
|
263
|
+
error: { message: "no dataset", key: "API.BAD_REQUEST", code: 400 },
|
|
264
|
+
}));
|
|
265
|
+
await harness.manager.start();
|
|
266
|
+
expect(lastUpdate(harness, "error")).toEqual({
|
|
267
|
+
message: "no dataset",
|
|
268
|
+
key: "API.BAD_REQUEST",
|
|
269
|
+
});
|
|
270
|
+
expect(lastUpdate(harness, "loading")).toBe(false);
|
|
271
|
+
expect(wasUpdated(harness, "showModal")).toBe(false);
|
|
272
|
+
expect(harness.events.onError).toHaveBeenCalledTimes(1);
|
|
273
|
+
});
|
|
274
|
+
test("falls back to a generic key when the error carries none", async () => {
|
|
275
|
+
const harness = build();
|
|
276
|
+
mocks.getCaptchaChallenge.mockResolvedValue(challengeResponse({ error: { message: "boom", code: 500 } }));
|
|
277
|
+
await harness.manager.start();
|
|
278
|
+
expect(lastUpdate(harness, "error")).toEqual({
|
|
279
|
+
message: "boom",
|
|
280
|
+
key: "API.UNKNOWN_ERROR",
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
test("retries then resets when the provider never returns captchas", async () => {
|
|
284
|
+
const harness = build();
|
|
285
|
+
mocks.getCaptchaChallenge.mockResolvedValue(challengeResponse({ captchas: [] }));
|
|
286
|
+
await expect(harness.manager.start()).resolves.toBeUndefined();
|
|
287
|
+
expect(harness.events.onReset).toHaveBeenCalled();
|
|
288
|
+
expect(lastUpdate(harness, "challenge")).toBeUndefined();
|
|
289
|
+
});
|
|
290
|
+
test("expires the challenge once the summed time limit elapses", async () => {
|
|
291
|
+
vi.useFakeTimers();
|
|
292
|
+
const harness = build();
|
|
293
|
+
mocks.getCaptchaChallenge.mockResolvedValue(challengeResponse({
|
|
294
|
+
captchas: [
|
|
295
|
+
captcha({ timeLimitMs: 1000 }),
|
|
296
|
+
captcha({ timeLimitMs: 2000 }),
|
|
297
|
+
],
|
|
298
|
+
}));
|
|
299
|
+
await harness.manager.start();
|
|
300
|
+
vi.advanceTimersByTime(2999);
|
|
301
|
+
expect(harness.events.onChallengeExpired).not.toHaveBeenCalled();
|
|
302
|
+
vi.advanceTimersByTime(1);
|
|
303
|
+
expect(harness.events.onChallengeExpired).toHaveBeenCalledTimes(1);
|
|
304
|
+
expect(lastUpdate(harness, "isHuman")).toBe(false);
|
|
305
|
+
expect(lastUpdate(harness, "showModal")).toBe(false);
|
|
306
|
+
});
|
|
307
|
+
test("falls back to the configured challenge timeout per captcha", async () => {
|
|
308
|
+
vi.useFakeTimers();
|
|
309
|
+
const configured = config();
|
|
310
|
+
const harness = build({ configInput: configured });
|
|
311
|
+
mocks.getCaptchaChallenge.mockResolvedValue(challengeResponse({ captchas: [captcha({ timeLimitMs: undefined })] }));
|
|
312
|
+
await harness.manager.start();
|
|
313
|
+
vi.advanceTimersByTime(configured.captchas.image.challengeTimeout - 1);
|
|
314
|
+
expect(harness.events.onChallengeExpired).not.toHaveBeenCalled();
|
|
315
|
+
vi.advanceTimersByTime(1);
|
|
316
|
+
expect(harness.events.onChallengeExpired).toHaveBeenCalledTimes(1);
|
|
317
|
+
});
|
|
318
|
+
test("gives up after the retry ceiling when the provider keeps failing", async () => {
|
|
319
|
+
const harness = build();
|
|
320
|
+
mocks.getCaptchaChallenge.mockRejectedValue(new Error("provider down"));
|
|
321
|
+
await harness.manager.start();
|
|
322
|
+
expect(mocks.getCaptchaChallenge).toHaveBeenCalledTimes(11);
|
|
323
|
+
expect(harness.events.onReset).toHaveBeenCalled();
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
describe("loadAccount", () => {
|
|
327
|
+
test("uses the frictionless account without touching the extension", async () => {
|
|
328
|
+
const frictionlessState = frictionless();
|
|
329
|
+
const harness = build({ frictionlessState });
|
|
330
|
+
await harness.manager.start();
|
|
331
|
+
expect(mocks.getAccount).not.toHaveBeenCalled();
|
|
332
|
+
expect(lastUpdate(harness, "account")).toBe(frictionlessState.userAccount);
|
|
333
|
+
});
|
|
334
|
+
test("asks the extension for an account when frictionless is absent", async () => {
|
|
335
|
+
const harness = build({ withFrictionless: false });
|
|
336
|
+
await harness.manager.start();
|
|
337
|
+
expect(mocks.extensionLoader).toHaveBeenCalledWith(true);
|
|
338
|
+
expect(mocks.getAccount).toHaveBeenCalledTimes(1);
|
|
339
|
+
});
|
|
340
|
+
test("refuses to run in web3 mode without a user account address", async () => {
|
|
341
|
+
const harness = build({
|
|
342
|
+
configInput: config({ web2: false }),
|
|
343
|
+
withFrictionless: false,
|
|
344
|
+
});
|
|
345
|
+
await harness.manager.start();
|
|
346
|
+
expect(mocks.getCaptchaChallenge).not.toHaveBeenCalled();
|
|
347
|
+
expect(harness.events.onReset).toHaveBeenCalled();
|
|
348
|
+
});
|
|
349
|
+
test("accepts web3 mode when a user account address is configured", async () => {
|
|
350
|
+
const harness = build({
|
|
351
|
+
configInput: config({ web2: false, userAccountAddress: USER_ADDRESS }),
|
|
352
|
+
withFrictionless: false,
|
|
353
|
+
});
|
|
354
|
+
await harness.manager.start();
|
|
355
|
+
expect(mocks.getAccount).toHaveBeenCalledTimes(1);
|
|
356
|
+
});
|
|
357
|
+
test("prefers the account already in state over the configured one", async () => {
|
|
358
|
+
const harness = build({
|
|
359
|
+
configInput: config({ web2: false, userAccountAddress: "configured" }),
|
|
360
|
+
initialState: { account: account(signRawMock) },
|
|
361
|
+
withFrictionless: false,
|
|
362
|
+
});
|
|
363
|
+
await harness.manager.start();
|
|
364
|
+
expect(mocks.extensionLoader).toHaveBeenCalledWith(false);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
describe("provider api construction", () => {
|
|
368
|
+
test("refuses to build a provider api without a site key", async () => {
|
|
369
|
+
const harness = build({
|
|
370
|
+
configInput: config({ account: { address: "" } }),
|
|
371
|
+
});
|
|
372
|
+
await harness.manager.start();
|
|
373
|
+
expect(mocks.providerApiConstructions).toHaveLength(0);
|
|
374
|
+
expect(harness.events.onReset).toHaveBeenCalled();
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
describe("submit", () => {
|
|
378
|
+
const started = async (options = {}, clickX = 0, clickY = 0) => {
|
|
379
|
+
const harness = build(options);
|
|
380
|
+
await harness.manager.start(clickX, clickY);
|
|
381
|
+
Object.assign(harness.state, {
|
|
382
|
+
solutions: [[["hash-1", 10, 20]]],
|
|
383
|
+
...options.afterStart,
|
|
384
|
+
});
|
|
385
|
+
harness.updates.length = 0;
|
|
386
|
+
return harness;
|
|
387
|
+
};
|
|
388
|
+
test("clears the challenge timeout before submitting", async () => {
|
|
389
|
+
const harness = await started();
|
|
390
|
+
await harness.manager.submit();
|
|
391
|
+
expect(harness.updates[0]).toEqual({ timeout: undefined });
|
|
392
|
+
});
|
|
393
|
+
test("hides the modal as soon as the solution is taken", async () => {
|
|
394
|
+
const harness = await started();
|
|
395
|
+
await harness.manager.submit();
|
|
396
|
+
expect(harness.updates[1]).toEqual({ showModal: false });
|
|
397
|
+
});
|
|
398
|
+
test("restarts the challenge when there is nothing in state to submit", async () => {
|
|
399
|
+
const harness = build({ initialState: { challenge: undefined } });
|
|
400
|
+
await harness.manager.submit();
|
|
401
|
+
expect(mocks.submitCaptchaSolution).not.toHaveBeenCalled();
|
|
402
|
+
expect(mocks.getCaptchaChallenge).toHaveBeenCalledTimes(1);
|
|
403
|
+
});
|
|
404
|
+
test("resets instead of submitting when the challenge has no dataset id", async () => {
|
|
405
|
+
mocks.getCaptchaChallenge.mockResolvedValue(challengeResponse({ captchas: [captcha({ datasetId: undefined })] }));
|
|
406
|
+
const harness = await started();
|
|
407
|
+
await harness.manager.submit();
|
|
408
|
+
expect(mocks.submitCaptchaSolution).not.toHaveBeenCalled();
|
|
409
|
+
expect(harness.events.onReset).toHaveBeenCalled();
|
|
410
|
+
});
|
|
411
|
+
test("does not submit when the account carries no extension", async () => {
|
|
412
|
+
const harness = await started({
|
|
413
|
+
frictionlessState: frictionless({
|
|
414
|
+
userAccount: accountWithoutExtension(),
|
|
415
|
+
}),
|
|
416
|
+
});
|
|
417
|
+
await harness.manager.submit();
|
|
418
|
+
expect(mocks.submitCaptchaSolution).not.toHaveBeenCalled();
|
|
419
|
+
expect(harness.events.onReset).toHaveBeenCalled();
|
|
420
|
+
});
|
|
421
|
+
test("does not submit when the extension signer cannot sign raw data", async () => {
|
|
422
|
+
const harness = await started({
|
|
423
|
+
frictionlessState: frictionless({ userAccount: account() }),
|
|
424
|
+
});
|
|
425
|
+
await harness.manager.submit();
|
|
426
|
+
expect(mocks.submitCaptchaSolution).not.toHaveBeenCalled();
|
|
427
|
+
expect(harness.events.onReset).toHaveBeenCalled();
|
|
428
|
+
});
|
|
429
|
+
test("signs the challenge timestamp with the user's account", async () => {
|
|
430
|
+
const harness = await started();
|
|
431
|
+
await harness.manager.submit();
|
|
432
|
+
expect(signRawMock).toHaveBeenCalledWith({
|
|
433
|
+
address: USER_ADDRESS,
|
|
434
|
+
data: expect.any(String),
|
|
435
|
+
type: "bytes",
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
test("embeds the checkbox click coordinates in the first captcha's salt", async () => {
|
|
439
|
+
const harness = await started({}, 7, 9);
|
|
440
|
+
await harness.manager.submit();
|
|
441
|
+
const solutions = mocks.submitCaptchaSolution.mock.calls[0]?.[2];
|
|
442
|
+
const first = solutions?.[0];
|
|
443
|
+
if (!first)
|
|
444
|
+
throw new Error("no solution submitted");
|
|
445
|
+
expect(extractData(first.salt)).toEqual([7, 9, 10, 20]);
|
|
446
|
+
expect(first.solution).toEqual(["hash-1"]);
|
|
447
|
+
});
|
|
448
|
+
test("omits the click coordinates from later captchas", async () => {
|
|
449
|
+
mocks.getCaptchaChallenge.mockResolvedValue(challengeResponse({ captchas: [captcha(), captcha()] }));
|
|
450
|
+
const harness = await started({ afterStart: { solutions: [[["hash-1", 1, 2]], [["hash-2", 3, 4]]] } }, 7, 9);
|
|
451
|
+
await harness.manager.submit();
|
|
452
|
+
const solutions = mocks.submitCaptchaSolution.mock.calls[0]?.[2];
|
|
453
|
+
expect(extractData(solutions?.[1]?.salt ?? "")).toEqual([3, 4]);
|
|
454
|
+
});
|
|
455
|
+
test("handles a captcha with no selections at all", async () => {
|
|
456
|
+
const harness = await started({ afterStart: { solutions: [[]] } });
|
|
457
|
+
await harness.manager.submit();
|
|
458
|
+
const solutions = mocks.submitCaptchaSolution.mock.calls[0]?.[2];
|
|
459
|
+
expect(solutions?.[0]?.solution).toEqual([]);
|
|
460
|
+
});
|
|
461
|
+
test("forwards the request hash, timestamp and provider signature", async () => {
|
|
462
|
+
const harness = await started();
|
|
463
|
+
await harness.manager.submit();
|
|
464
|
+
const call = mocks.submitCaptchaSolution.mock.calls[0];
|
|
465
|
+
expect(call?.[0]).toBe("0xuser-signature");
|
|
466
|
+
expect(call?.[1]).toBe("0xrequest-hash");
|
|
467
|
+
expect(call?.[3]).toBe("1700000000000");
|
|
468
|
+
expect(call?.[4]).toBe("0xprovider-request-hash");
|
|
469
|
+
});
|
|
470
|
+
test("marks the user as human when the provider verifies the solution", async () => {
|
|
471
|
+
const harness = await started();
|
|
472
|
+
await harness.manager.submit();
|
|
473
|
+
expect(lastUpdate(harness, "isHuman")).toBe(true);
|
|
474
|
+
expect(harness.events.onHuman).toHaveBeenCalledTimes(1);
|
|
475
|
+
});
|
|
476
|
+
test("emits a token describing the accepted solution", async () => {
|
|
477
|
+
const harness = await started();
|
|
478
|
+
await harness.manager.submit();
|
|
479
|
+
const token = harness.events.onHuman.mock.calls[0]?.[0];
|
|
480
|
+
if (!token)
|
|
481
|
+
throw new Error("no token emitted");
|
|
482
|
+
const decoded = decodeProcaptchaOutput(token);
|
|
483
|
+
expect(decoded[ApiParams.user]).toBe(USER_ADDRESS);
|
|
484
|
+
expect(decoded[ApiParams.dapp]).toBe(SITE_KEY);
|
|
485
|
+
expect(decoded[ApiParams.providerUrl]).toBe(PROVIDER_URL);
|
|
486
|
+
expect(decoded[ApiParams.captchaType]).toBe(CaptchaType.image);
|
|
487
|
+
expect(decoded[ApiParams.timestamp]).toBe("1700000000000");
|
|
488
|
+
});
|
|
489
|
+
test("expires the human verdict after the configured solution timeout", async () => {
|
|
490
|
+
vi.useFakeTimers();
|
|
491
|
+
const configured = config();
|
|
492
|
+
const harness = await started({ configInput: configured });
|
|
493
|
+
await harness.manager.submit();
|
|
494
|
+
vi.advanceTimersByTime(configured.captchas.image.solutionTimeout);
|
|
495
|
+
expect(harness.events.onExpired).toHaveBeenCalledTimes(1);
|
|
496
|
+
expect(lastUpdate(harness, "isHuman")).toBe(false);
|
|
497
|
+
});
|
|
498
|
+
test("fails and restarts frictionless when the solution is rejected", async () => {
|
|
499
|
+
const harness = await started();
|
|
500
|
+
mocks.submitCaptchaSolution.mockResolvedValue([
|
|
501
|
+
solutionResponse({ verified: false }),
|
|
502
|
+
"0xcommitment",
|
|
503
|
+
]);
|
|
504
|
+
await harness.manager.submit();
|
|
505
|
+
expect(harness.events.onFailed).toHaveBeenCalledTimes(1);
|
|
506
|
+
expect(harness.events.onReset).toHaveBeenCalled();
|
|
507
|
+
expect(harness.restart).toHaveBeenCalledTimes(1);
|
|
508
|
+
});
|
|
509
|
+
test("does not submit when no captcha api was ever built", async () => {
|
|
510
|
+
const harness = build({
|
|
511
|
+
initialState: {
|
|
512
|
+
challenge: challengeResponse(),
|
|
513
|
+
solutions: [[["hash-1", 1, 2]]],
|
|
514
|
+
account: account(signRawMock),
|
|
515
|
+
},
|
|
516
|
+
});
|
|
517
|
+
await harness.manager.submit();
|
|
518
|
+
expect(mocks.submitCaptchaSolution).not.toHaveBeenCalled();
|
|
519
|
+
expect(harness.events.onReset).toHaveBeenCalled();
|
|
520
|
+
});
|
|
521
|
+
test("does not submit when no account has been loaded", async () => {
|
|
522
|
+
const harness = build({
|
|
523
|
+
initialState: {
|
|
524
|
+
challenge: challengeResponse(),
|
|
525
|
+
solutions: [[["hash-1", 1, 2]]],
|
|
526
|
+
account: undefined,
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
await harness.manager.submit();
|
|
530
|
+
expect(mocks.submitCaptchaSolution).not.toHaveBeenCalled();
|
|
531
|
+
expect(harness.events.onReset).toHaveBeenCalled();
|
|
532
|
+
});
|
|
533
|
+
test("does not emit a token when the dapp account is missing from state", async () => {
|
|
534
|
+
const harness = await started({ afterStart: { dappAccount: undefined } });
|
|
535
|
+
await harness.manager.submit();
|
|
536
|
+
expect(harness.events.onHuman).not.toHaveBeenCalled();
|
|
537
|
+
expect(harness.events.onReset).toHaveBeenCalled();
|
|
538
|
+
});
|
|
539
|
+
test("sends the honeypot value as client metadata when filled", async () => {
|
|
540
|
+
const harness = await started({ honeypot: () => "i-am-a-bot" });
|
|
541
|
+
await harness.manager.submit();
|
|
542
|
+
expect(mocks.submitCaptchaSolution.mock.calls[0]?.[7]).toEqual({
|
|
543
|
+
hp: "i-am-a-bot",
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
test("omits client metadata when the honeypot is empty", async () => {
|
|
547
|
+
const harness = await started({ honeypot: () => "" });
|
|
548
|
+
await harness.manager.submit();
|
|
549
|
+
expect(mocks.submitCaptchaSolution.mock.calls[0]?.[7]).toBeUndefined();
|
|
550
|
+
});
|
|
551
|
+
test("omits client metadata when no honeypot reader is supplied", async () => {
|
|
552
|
+
const harness = await started();
|
|
553
|
+
await harness.manager.submit();
|
|
554
|
+
expect(mocks.submitCaptchaSolution.mock.calls[0]?.[7]).toBeUndefined();
|
|
555
|
+
});
|
|
556
|
+
test("waits for simd readings before submitting", async () => {
|
|
557
|
+
const getSimdReadings = vi.fn();
|
|
558
|
+
getSimdReadings.mockResolvedValue("simd-submit");
|
|
559
|
+
const harness = await started({
|
|
560
|
+
frictionlessState: frictionless({ getSimdReadings, restart: vi.fn() }),
|
|
561
|
+
});
|
|
562
|
+
await harness.manager.submit();
|
|
563
|
+
expect(mocks.submitCaptchaSolution.mock.calls[0]?.[6]).toBe("simd-submit");
|
|
564
|
+
});
|
|
565
|
+
describe("behavioural data", () => {
|
|
566
|
+
const collector = (data) => ({
|
|
567
|
+
start: () => undefined,
|
|
568
|
+
stop: () => undefined,
|
|
569
|
+
getData: () => data,
|
|
570
|
+
clear: () => undefined,
|
|
571
|
+
});
|
|
572
|
+
const mousePoint = (x) => ({
|
|
573
|
+
x,
|
|
574
|
+
y: 0,
|
|
575
|
+
timestamp: 1,
|
|
576
|
+
});
|
|
577
|
+
const touchPoint = (x) => ({
|
|
578
|
+
x,
|
|
579
|
+
y: 0,
|
|
580
|
+
timestamp: 1,
|
|
581
|
+
eventType: "touchstart",
|
|
582
|
+
touchCount: 1,
|
|
583
|
+
});
|
|
584
|
+
const clickPoint = (x) => ({
|
|
585
|
+
x,
|
|
586
|
+
y: 0,
|
|
587
|
+
timestamp: 1,
|
|
588
|
+
eventType: "click",
|
|
589
|
+
button: 0,
|
|
590
|
+
});
|
|
591
|
+
test("encrypts the collected data when an encryptor is present", async () => {
|
|
592
|
+
const encryptBehavioralData = vi.fn();
|
|
593
|
+
encryptBehavioralData.mockResolvedValue("0xencrypted");
|
|
594
|
+
const harness = await started({
|
|
595
|
+
frictionlessState: frictionless({
|
|
596
|
+
encryptBehavioralData,
|
|
597
|
+
behaviorCollector1: collector([mousePoint(1), mousePoint(2)]),
|
|
598
|
+
deviceCapability: "high",
|
|
599
|
+
restart: vi.fn(),
|
|
600
|
+
}),
|
|
601
|
+
});
|
|
602
|
+
await harness.manager.submit();
|
|
603
|
+
const payload = JSON.parse(encryptBehavioralData.mock.calls[0]?.[0] ?? "{}");
|
|
604
|
+
expect(payload.collector1).toEqual([mousePoint(1), mousePoint(2)]);
|
|
605
|
+
expect(payload.collector2).toEqual([]);
|
|
606
|
+
expect(payload.collector3).toEqual([]);
|
|
607
|
+
expect(payload.deviceCapability).toBe("high");
|
|
608
|
+
expect(mocks.submitCaptchaSolution.mock.calls[0]?.[5]).toBe("0xencrypted");
|
|
609
|
+
});
|
|
610
|
+
test("defaults the device capability when it was never measured", async () => {
|
|
611
|
+
const encryptBehavioralData = vi.fn();
|
|
612
|
+
encryptBehavioralData.mockResolvedValue("0xencrypted");
|
|
613
|
+
const harness = await started({
|
|
614
|
+
frictionlessState: frictionless({
|
|
615
|
+
encryptBehavioralData,
|
|
616
|
+
behaviorCollector1: collector([]),
|
|
617
|
+
restart: vi.fn(),
|
|
618
|
+
}),
|
|
619
|
+
});
|
|
620
|
+
await harness.manager.submit();
|
|
621
|
+
const payload = JSON.parse(encryptBehavioralData.mock.calls[0]?.[0] ?? "{}");
|
|
622
|
+
expect(payload.deviceCapability).toBe("unknown");
|
|
623
|
+
});
|
|
624
|
+
test("packs the data first when a packer is supplied", async () => {
|
|
625
|
+
const encryptBehavioralData = vi.fn();
|
|
626
|
+
encryptBehavioralData.mockResolvedValue("0xencrypted");
|
|
627
|
+
const packBehavioralData = vi.fn();
|
|
628
|
+
packBehavioralData.mockReturnValue({ c1: [], c2: [], c3: [], d: "x" });
|
|
629
|
+
const harness = await started({
|
|
630
|
+
frictionlessState: frictionless({
|
|
631
|
+
encryptBehavioralData,
|
|
632
|
+
behaviorCollector2: collector([touchPoint(3)]),
|
|
633
|
+
packBehavioralData,
|
|
634
|
+
restart: vi.fn(),
|
|
635
|
+
}),
|
|
636
|
+
});
|
|
637
|
+
await harness.manager.submit();
|
|
638
|
+
expect(packBehavioralData).toHaveBeenCalledTimes(1);
|
|
639
|
+
expect(encryptBehavioralData).toHaveBeenCalledWith('{"c1":[],"c2":[],"c3":[],"d":"x"}');
|
|
640
|
+
});
|
|
641
|
+
test("submits without behavioural data when encryption throws", async () => {
|
|
642
|
+
const encryptBehavioralData = vi.fn();
|
|
643
|
+
encryptBehavioralData.mockRejectedValue(new Error("no subtle crypto"));
|
|
644
|
+
const harness = await started({
|
|
645
|
+
frictionlessState: frictionless({
|
|
646
|
+
encryptBehavioralData,
|
|
647
|
+
behaviorCollector3: collector([clickPoint(4)]),
|
|
648
|
+
restart: vi.fn(),
|
|
649
|
+
}),
|
|
650
|
+
});
|
|
651
|
+
await harness.manager.submit();
|
|
652
|
+
expect(mocks.submitCaptchaSolution.mock.calls[0]?.[5]).toBeUndefined();
|
|
653
|
+
expect(harness.events.onHuman).toHaveBeenCalledTimes(1);
|
|
654
|
+
});
|
|
655
|
+
test("skips encryption entirely when no collector produced data", async () => {
|
|
656
|
+
const encryptBehavioralData = vi.fn();
|
|
657
|
+
const harness = await started({
|
|
658
|
+
frictionlessState: frictionless({
|
|
659
|
+
encryptBehavioralData,
|
|
660
|
+
restart: vi.fn(),
|
|
661
|
+
}),
|
|
662
|
+
});
|
|
663
|
+
await harness.manager.submit();
|
|
664
|
+
expect(encryptBehavioralData).not.toHaveBeenCalled();
|
|
665
|
+
});
|
|
666
|
+
test("skips encryption when collectors exist but no encryptor does", async () => {
|
|
667
|
+
const harness = await started({
|
|
668
|
+
frictionlessState: frictionless({
|
|
669
|
+
behaviorCollector1: collector([mousePoint(1)]),
|
|
670
|
+
restart: vi.fn(),
|
|
671
|
+
}),
|
|
672
|
+
});
|
|
673
|
+
await harness.manager.submit();
|
|
674
|
+
expect(mocks.submitCaptchaSolution.mock.calls[0]?.[5]).toBeUndefined();
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
describe("select", () => {
|
|
679
|
+
const selectable = () => build({
|
|
680
|
+
initialState: {
|
|
681
|
+
challenge: challengeResponse(),
|
|
682
|
+
solutions: [[]],
|
|
683
|
+
},
|
|
684
|
+
});
|
|
685
|
+
test("throws without a challenge", () => {
|
|
686
|
+
const harness = build();
|
|
687
|
+
expect(() => harness.manager.select("hash-1")).toThrow();
|
|
688
|
+
});
|
|
689
|
+
test("throws when the index has run past the challenge", () => {
|
|
690
|
+
const harness = build({
|
|
691
|
+
initialState: { challenge: challengeResponse(), index: 1 },
|
|
692
|
+
});
|
|
693
|
+
expect(() => harness.manager.select("hash-1")).toThrow();
|
|
694
|
+
});
|
|
695
|
+
test("throws when the index is negative", () => {
|
|
696
|
+
const harness = build({
|
|
697
|
+
initialState: { challenge: challengeResponse(), index: -1 },
|
|
698
|
+
});
|
|
699
|
+
expect(() => harness.manager.select("hash-1")).toThrow();
|
|
700
|
+
});
|
|
701
|
+
test("adds an unselected image with its coordinates", () => {
|
|
702
|
+
const harness = selectable();
|
|
703
|
+
harness.manager.select("hash-1", 5, 6);
|
|
704
|
+
expect(lastUpdate(harness, "solutions")).toEqual([[["hash-1", 5, 6]]]);
|
|
705
|
+
});
|
|
706
|
+
test("defaults missing coordinates to the origin", () => {
|
|
707
|
+
const harness = selectable();
|
|
708
|
+
harness.manager.select("hash-1");
|
|
709
|
+
expect(lastUpdate(harness, "solutions")).toEqual([[["hash-1", 0, 0]]]);
|
|
710
|
+
});
|
|
711
|
+
test("removes an image that was already selected", () => {
|
|
712
|
+
const harness = build({
|
|
713
|
+
initialState: {
|
|
714
|
+
challenge: challengeResponse(),
|
|
715
|
+
solutions: [[["hash-1", 5, 6]]],
|
|
716
|
+
},
|
|
717
|
+
});
|
|
718
|
+
harness.manager.select("hash-1");
|
|
719
|
+
expect(lastUpdate(harness, "solutions")).toEqual([[]]);
|
|
720
|
+
});
|
|
721
|
+
test("keeps selections for other rounds untouched", () => {
|
|
722
|
+
const harness = build({
|
|
723
|
+
initialState: {
|
|
724
|
+
challenge: challengeResponse({ captchas: [captcha(), captcha()] }),
|
|
725
|
+
solutions: [[["hash-1", 1, 1]], []],
|
|
726
|
+
index: 1,
|
|
727
|
+
},
|
|
728
|
+
});
|
|
729
|
+
harness.manager.select("hash-2");
|
|
730
|
+
expect(lastUpdate(harness, "solutions")).toEqual([
|
|
731
|
+
[["hash-1", 1, 1]],
|
|
732
|
+
[["hash-2", 0, 0]],
|
|
733
|
+
]);
|
|
734
|
+
});
|
|
735
|
+
});
|
|
736
|
+
describe("nextRound", () => {
|
|
737
|
+
test("throws without a challenge", () => {
|
|
738
|
+
const harness = build();
|
|
739
|
+
expect(() => harness.manager.nextRound()).toThrow();
|
|
740
|
+
});
|
|
741
|
+
test("throws on the last round", () => {
|
|
742
|
+
const harness = build({
|
|
743
|
+
initialState: { challenge: challengeResponse(), index: 0 },
|
|
744
|
+
});
|
|
745
|
+
expect(() => harness.manager.nextRound()).toThrow();
|
|
746
|
+
});
|
|
747
|
+
test("advances the index when another round remains", () => {
|
|
748
|
+
const harness = build({
|
|
749
|
+
initialState: {
|
|
750
|
+
challenge: challengeResponse({ captchas: [captcha(), captcha()] }),
|
|
751
|
+
index: 0,
|
|
752
|
+
},
|
|
753
|
+
});
|
|
754
|
+
harness.manager.nextRound();
|
|
755
|
+
expect(lastUpdate(harness, "index")).toBe(1);
|
|
756
|
+
});
|
|
757
|
+
});
|
|
758
|
+
describe("cancel", () => {
|
|
759
|
+
test("clears the timeout, resets and closes", async () => {
|
|
760
|
+
const harness = build();
|
|
761
|
+
await harness.manager.cancel();
|
|
762
|
+
expect(harness.updates[0]).toEqual({ timeout: undefined });
|
|
763
|
+
expect(harness.events.onReset).toHaveBeenCalledTimes(1);
|
|
764
|
+
expect(harness.events.onClose).toHaveBeenCalledTimes(1);
|
|
765
|
+
expect(harness.restart).toHaveBeenCalledTimes(1);
|
|
766
|
+
});
|
|
767
|
+
test("does not require a frictionless restart hook", async () => {
|
|
768
|
+
const harness = build({ withFrictionless: false });
|
|
769
|
+
await harness.manager.cancel();
|
|
770
|
+
expect(harness.events.onClose).toHaveBeenCalledTimes(1);
|
|
771
|
+
});
|
|
772
|
+
});
|
|
773
|
+
describe("reload", () => {
|
|
774
|
+
test("restarts the frictionless flow instead of re-running start", async () => {
|
|
775
|
+
const harness = build();
|
|
776
|
+
await harness.manager.reload();
|
|
777
|
+
expect(harness.events.onReload).toHaveBeenCalledTimes(1);
|
|
778
|
+
expect(harness.restart).toHaveBeenCalledTimes(1);
|
|
779
|
+
expect(mocks.getCaptchaChallenge).not.toHaveBeenCalled();
|
|
780
|
+
});
|
|
781
|
+
test("starts a fresh challenge when there is nothing to restart", async () => {
|
|
782
|
+
const harness = build({ withFrictionless: false });
|
|
783
|
+
await harness.manager.reload();
|
|
784
|
+
expect(harness.events.onReload).toHaveBeenCalledTimes(1);
|
|
785
|
+
expect(mocks.getCaptchaChallenge).toHaveBeenCalledTimes(1);
|
|
786
|
+
});
|
|
787
|
+
});
|
|
788
|
+
//# sourceMappingURL=manager.unit.test.js.map
|