mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
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>
102 lines
3.2 KiB
TypeScript
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),
|
|
);
|
|
});
|
|
});
|