stack/packages/stack-shared/src/utils/turnstile-flow.test.ts
Mantra e59a70783e
Turnstile integration for fraud protection (#1239)
Enhances sign-up process with Turnstile integration for fraud
protection. Builds on top of fraud-protection-temp-emails.

Made with [Cursor](https://cursor.com)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Cloudflare Turnstile bot-protection across signup/sign-in flows
(including SDK JSON mode).
  * Email deliverability checks via Emailable.
* Sign-up risk scoring with persisted risk metrics and country code
tracking.
* UI: country-code selector, risk-score editing in user details, users
list refresh button, and Turnstile signup demo pages.

* **Bug Fixes**
  * Use actual sign-up timestamp for reporting/metrics.

* **Documentation**
* Expanded knowledge base on Turnstile, risk scoring, and env
configuration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Konstantin Wohlwend <n2d4xc@gmail.com>
Co-authored-by: BilalG1 <bg2002@gmail.com>
Co-authored-by: Armaan Jain <84474476+Developing-Gamer@users.noreply.github.com>
Co-authored-by: nams1570 <amanganapathy@gmail.com>
2026-03-20 21:26:45 +00:00

102 lines
3.2 KiB
TypeScript

// @vitest-environment jsdom
import { beforeEach, describe, expect, it, vi } from "vitest";
const loadTurnstileScriptMock = vi.fn(() => Promise.resolve());
const renderMock = vi.fn();
const executeMock = vi.fn();
const removeMock = vi.fn();
const captureErrorMock = vi.fn();
vi.mock("./turnstile-browser", () => ({
loadTurnstileScript: loadTurnstileScriptMock,
getTurnstileApi: () => ({
render: renderMock,
execute: executeMock,
remove: removeMock,
}),
}));
vi.mock("./errors", async () => {
const actual = await vi.importActual<typeof import("./errors")>("./errors");
return {
...actual,
captureError: captureErrorMock,
};
});
describe("withBotChallengeFlow", () => {
beforeEach(() => {
vi.clearAllMocks();
loadTurnstileScriptMock.mockResolvedValue(undefined);
renderMock.mockImplementation((_container, config: {
callback: (token: string) => void,
}) => {
config.callback("invisible-token");
return "widget-id";
});
executeMock.mockImplementation(() => {});
removeMock.mockImplementation(() => {});
});
it("throws a bot challenge execution error when the phase-2 visible challenge fails", async () => {
const { BotChallengeExecutionFailedError, withBotChallengeFlow } = await import("./turnstile-flow");
loadTurnstileScriptMock
.mockResolvedValueOnce(undefined)
.mockRejectedValueOnce(new Error("cloudflare unavailable"));
const execute = vi.fn(async ({ token, phase }: { token?: string, phase?: "invisible" | "visible" }) => {
if (token === "invisible-token" && phase === "invisible") {
return { requiresChallenge: true };
}
return { requiresChallenge: false };
});
await expect(withBotChallengeFlow({
visibleSiteKey: "visible-site-key",
invisibleSiteKey: "invisible-site-key",
action: "sign_up_with_credential",
execute,
isChallengeRequired: (result) => result.requiresChallenge,
})).rejects.toBeInstanceOf(BotChallengeExecutionFailedError);
expect(execute).toHaveBeenCalledTimes(1);
expect(execute).toHaveBeenCalledWith({
token: "invisible-token",
phase: "invisible",
});
expect(captureErrorMock).toHaveBeenCalledWith(
"turnstile-flow-visible-challenge-failed",
expect.any(Error),
);
});
it("marks the challenge as unavailable when both phase-1 challenge attempts fail", async () => {
const { withBotChallengeFlow } = await import("./turnstile-flow");
loadTurnstileScriptMock
.mockRejectedValueOnce(new Error("invisible unavailable"))
.mockRejectedValueOnce(new Error("visible unavailable"));
const execute = vi.fn(async ({ unavailable }: { unavailable?: true }) => ({
unavailable,
}));
await expect(withBotChallengeFlow({
visibleSiteKey: "visible-site-key",
invisibleSiteKey: "invisible-site-key",
action: "sign_up_with_credential",
execute,
isChallengeRequired: () => false,
})).resolves.toEqual({ unavailable: true });
expect(execute).toHaveBeenCalledTimes(1);
expect(execute).toHaveBeenCalledWith({ unavailable: true });
expect(captureErrorMock).toHaveBeenCalledWith(
"turnstile-flow-all-challenges-failed",
expect.any(Error),
);
});
});