@prosopo/procaptcha-common 2.11.6 → 2.11.8

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.
@@ -12,9 +12,10 @@
12
12
  // See the License for the specific language governing permissions and
13
13
  // limitations under the License.
14
14
 
15
- import { describe, expect, it, vi } from "vitest";
15
+ import { beforeEach, describe, expect, it, vi } from "vitest";
16
16
  import {
17
17
  getProcaptchaRandomActiveProvider,
18
+ getRetryDelayMs,
18
19
  pickIpMode,
19
20
  providerRetry,
20
21
  } from "../providers.js";
@@ -29,9 +30,26 @@ vi.mock("@prosopo/load-balancer", () => ({
29
30
  : "http://localhost:9229",
30
31
  },
31
32
  })),
33
+ getRandomProviderFromList: vi.fn(
34
+ async (_env: string, _ipMode?: string, excludeUrl?: string) => ({
35
+ providerAccount: "5FromList",
36
+ provider: {
37
+ url: excludeUrl
38
+ ? "https://provider-from-list-b.io"
39
+ : "https://provider-from-list-a.io",
40
+ },
41
+ }),
42
+ ),
32
43
  }));
33
44
 
34
45
  describe("providers", () => {
46
+ // The load-balancer mock functions are module-level, so their call history
47
+ // accumulates across tests — clear it between cases to keep call-count
48
+ // assertions independent.
49
+ beforeEach(() => {
50
+ vi.clearAllMocks();
51
+ });
52
+
35
53
  describe("getProcaptchaRandomActiveProvider", () => {
36
54
  it("delegates to the load-balancer static-DNS resolver", async () => {
37
55
  const { getRandomActiveProvider } = await import(
@@ -59,6 +77,65 @@ describe("providers", () => {
59
77
  "ipv4",
60
78
  );
61
79
  });
80
+
81
+ it("uses the DNS-routed resolver on the first attempt", async () => {
82
+ const { getRandomActiveProvider, getRandomProviderFromList } =
83
+ await import("@prosopo/load-balancer");
84
+
85
+ const result = await getProcaptchaRandomActiveProvider(
86
+ "production",
87
+ undefined,
88
+ { attempt: 1 },
89
+ );
90
+
91
+ expect(getRandomActiveProvider).toHaveBeenCalled();
92
+ expect(getRandomProviderFromList).not.toHaveBeenCalled();
93
+ expect(result.provider.url).toBe("https://pronode.prosopo.io");
94
+ });
95
+
96
+ it("picks a random provider from the list on a retry", async () => {
97
+ const { getRandomProviderFromList } = await import(
98
+ "@prosopo/load-balancer"
99
+ );
100
+
101
+ const result = await getProcaptchaRandomActiveProvider(
102
+ "production",
103
+ "ipv4",
104
+ { attempt: 2, excludeUrl: "https://pronode.prosopo.io" },
105
+ );
106
+
107
+ expect(getRandomProviderFromList).toHaveBeenCalledWith(
108
+ "production",
109
+ "ipv4",
110
+ "https://pronode.prosopo.io",
111
+ );
112
+ // The mock returns a different url when an excludeUrl is supplied.
113
+ expect(result.provider.url).toBe("https://provider-from-list-b.io");
114
+ });
115
+ });
116
+
117
+ describe("getRetryDelayMs", () => {
118
+ it("grows exponentially from the base delay, capped at the max", () => {
119
+ // random=1 → full jittered value == the (capped) exponential ceiling.
120
+ expect(getRetryDelayMs(0, () => 1)).toBe(500);
121
+ expect(getRetryDelayMs(1, () => 1)).toBe(1000);
122
+ expect(getRetryDelayMs(2, () => 1)).toBe(2000);
123
+ expect(getRetryDelayMs(3, () => 1)).toBe(4000);
124
+ // 500 * 2^6 = 32000 → capped at 10000.
125
+ expect(getRetryDelayMs(6, () => 1)).toBe(10000);
126
+ expect(getRetryDelayMs(20, () => 1)).toBe(10000);
127
+ });
128
+
129
+ it("applies full jitter — a fraction of the ceiling", () => {
130
+ // random=0 → no delay; random=0.5 → half the ceiling.
131
+ expect(getRetryDelayMs(2, () => 0)).toBe(0);
132
+ expect(getRetryDelayMs(2, () => 0.5)).toBe(1000);
133
+ });
134
+
135
+ it("clamps negative or fractional attempt counts", () => {
136
+ expect(getRetryDelayMs(-5, () => 1)).toBe(500);
137
+ expect(getRetryDelayMs(1.9, () => 1)).toBe(1000);
138
+ });
62
139
  });
63
140
 
64
141
  describe("pickIpMode", () => {
@@ -95,7 +172,8 @@ describe("providers", () => {
95
172
  const retryFn = vi.fn().mockResolvedValue(undefined);
96
173
  const stateReset = vi.fn();
97
174
 
98
- await providerRetry(currentFn, retryFn, stateReset, 1, 3);
175
+ // backoffMs=0 keeps the test fast; timing is covered by getRetryDelayMs.
176
+ await providerRetry(currentFn, retryFn, stateReset, 1, 3, 0);
99
177
 
100
178
  expect(currentFn).toHaveBeenCalledTimes(1);
101
179
  expect(stateReset).toHaveBeenCalledTimes(1);
@@ -152,7 +230,7 @@ describe("providers", () => {
152
230
  .mockImplementation(() => {});
153
231
 
154
232
  await expect(
155
- providerRetry(currentFn, retryFn, stateReset, 1, 3),
233
+ providerRetry(currentFn, retryFn, stateReset, 1, 3, 0),
156
234
  ).rejects.toThrow("Retry failed");
157
235
 
158
236
  expect(currentFn).toHaveBeenCalledTimes(1);
@@ -161,5 +239,31 @@ describe("providers", () => {
161
239
 
162
240
  consoleErrorSpy.mockRestore();
163
241
  });
242
+
243
+ it("waits for the backoff delay before retrying", async () => {
244
+ const error = new Error("Provider failed");
245
+ const currentFn = vi.fn().mockRejectedValue(error);
246
+ const order: string[] = [];
247
+ const retryFn = vi.fn(async () => {
248
+ order.push("retry");
249
+ });
250
+ const stateReset = vi.fn(() => {
251
+ order.push("reset");
252
+ });
253
+ const consoleErrorSpy = vi
254
+ .spyOn(console, "error")
255
+ .mockImplementation(() => {});
256
+
257
+ const start = Date.now();
258
+ await providerRetry(currentFn, retryFn, stateReset, 1, 3, 40);
259
+ const elapsed = Date.now() - start;
260
+
261
+ // State is reset before the wait, then the retry fires after the delay.
262
+ expect(order).toEqual(["reset", "retry"]);
263
+ expect(elapsed).toBeGreaterThanOrEqual(30);
264
+ expect(retryFn).toHaveBeenCalledTimes(1);
265
+
266
+ consoleErrorSpy.mockRestore();
267
+ });
164
268
  });
165
269
  });