Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/modules/users/services/AuthService.test.ts
T

1077 lines
50 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import { CatalogError, foundationErrors } from "@hivegrid/foundation";
const loggerMocks = vi.hoisted(() => ({ warn: vi.fn(), error: vi.fn(), info: vi.fn(), debug: vi.fn() }));
const registrationRecoveryMocks = vi.hoisted(() => ({
read: vi.fn(),
commit: vi.fn(),
clear: vi.fn(),
}));
vi.mock("~/utils/logger.server", () => ({ createScopedLogger: () => loggerMocks }));
vi.mock("~/locale/i18next.server", () => ({ getTranslations: vi.fn() }));
vi.mock("~/utils/app/UserValidationUtils", () => ({ default: { validateEmail: vi.fn(), validatePassword: vi.fn() } }));
vi.mock("~/utils/db/logs.db.server", () => ({ createLogLogin: vi.fn() }));
vi.mock("~/utils/db/users.db.server", () => ({ getUserByEmail: vi.fn() }));
vi.mock("~/utils/session.server", () => ({
createUserSession: vi.fn(),
getUserInfo: vi.fn(),
setLoggedUser: vi.fn(),
validateCSRFToken: vi.fn(),
}));
vi.mock("bcryptjs", () => ({ default: { compare: vi.fn() } }));
vi.mock("~/utils/db/appConfiguration.db.server", () => ({ getAppConfiguration: vi.fn() }));
vi.mock("~/utils/services/authService", () => ({
createRegistrationForm: vi.fn(),
getRegistrationFormData: vi.fn(),
isSignupCreationRequestKey: vi.fn((value: string) => /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)),
RegistrationValidationError: class RegistrationValidationError extends Error {
readonly i18nKey: string;
readonly i18nArgs?: Record<string, string | number>;
constructor(i18nKey: string, i18nArgs?: Record<string, string | number>) {
super(i18nKey);
this.i18nKey = i18nKey;
this.i18nArgs = i18nArgs;
}
},
resolvePendingRegistrationResendRecovery: vi.fn(),
validateRegistration: vi.fn(),
}));
vi.mock("~/services/auth/registration-resend-recovery.server", () => ({
readRegistrationResendRecovery: (...args: unknown[]) => registrationRecoveryMocks.read(...args),
commitRegistrationResendRecovery: (...args: unknown[]) => registrationRecoveryMocks.commit(...args),
clearRegistrationResendRecovery: (...args: unknown[]) => registrationRecoveryMocks.clear(...args),
}));
vi.mock("~/utils/db/registration.db.server", () => ({ getRegistrationVerificationStateByToken: vi.fn(), updateRegistration: vi.fn() }));
vi.mock("~/services/auth/auth-rate-limit.server", () => ({ checkAuthRateLimit: vi.fn() }));
vi.mock("~/modules/ipAddress/services/IpAddressService.server", () => ({ default: { log: vi.fn() } }));
vi.mock("@hivegrid/config", async () => {
const actual = await vi.importActual<typeof import("@hivegrid/config")>("@hivegrid/config");
return {
...actual,
loadRequiredWebConfig: () => ({
routing: {
appBaseDomain: "example.com",
webAppHost: "app.example.com",
adminAppHost: "admin.example.com",
apiHost: "api.example.com",
protocol: "https",
},
}),
};
});
import { getTranslations } from "~/locale/i18next.server";
import { getAppConfiguration } from "~/utils/db/appConfiguration.db.server";
import {
createRegistrationForm,
getRegistrationFormData,
RegistrationValidationError,
resolvePendingRegistrationResendRecovery,
validateRegistration,
} from "~/utils/services/authService";
import { createUserSession, getUserInfo, setLoggedUser, validateCSRFToken } from "~/utils/session.server";
import { getRegistrationVerificationStateByToken, updateRegistration } from "~/utils/db/registration.db.server";
import { getSafeLoginRedirect, isProductionTestAccountBlocked, isStagingDemoLoginAllowed } from "./AuthService";
import { deriveVerificationCreationRequestKey } from "~/domain/tenant-bootstrap/signupCreationRequestKey.server";
import { checkAuthRateLimit } from "~/services/auth/auth-rate-limit.server";
const getTranslationsMock = vi.mocked(getTranslations);
const getAppConfigurationMock = vi.mocked(getAppConfiguration);
const createRegistrationFormMock = vi.mocked(createRegistrationForm);
const getRegistrationFormDataMock = vi.mocked(getRegistrationFormData);
const validateRegistrationMock = vi.mocked(validateRegistration);
const resolvePendingRegistrationResendRecoveryMock = vi.mocked(resolvePendingRegistrationResendRecovery);
const getRegistrationVerificationStateByTokenMock = vi.mocked(getRegistrationVerificationStateByToken);
const updateRegistrationMock = vi.mocked(updateRegistration);
const setLoggedUserMock = vi.mocked(setLoggedUser);
const createUserSessionMock = vi.mocked(createUserSession);
const getUserInfoMock = vi.mocked(getUserInfo);
const validateCSRFTokenMock = vi.mocked(validateCSRFToken);
const checkAuthRateLimitMock = vi.mocked(checkAuthRateLimit);
const CREATION_REQUEST_KEY = "d7077797-054b-497a-b1ce-8e4f36257a96";
const VERIFICATION_TOKEN = "signed-registration-token";
const BOUND_VERIFICATION_KEY = deriveVerificationCreationRequestKey("registration_1");
function verificationSubmission(creationRequestKey = BOUND_VERIFICATION_KEY, password = "Password123!") {
return {
email: "attacker@example.test",
password,
company: "Attacker Ltd",
firstName: "Mallory",
lastName: "Evil",
slug: "attacker",
leadId: "attacker-lead",
leadToken: "attacker-token",
leadPlanId: "attacker-plan",
selectedSubscriptionPriceId: "attacker-price",
creationRequestKey,
captchaToken: "captcha-proof",
};
}
describe("AuthService login redirects", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("rejects unsafe redirect targets", async () => {
await expect(
getSafeLoginRedirect({ redirectTo: "https://evil.example", isAdmin: false, defaultTenantId: "tenant-1", fallback: "/dashboard" })
).resolves.toBe("/dashboard");
await expect(getSafeLoginRedirect({ redirectTo: "//evil.example", isAdmin: false, defaultTenantId: "tenant-1", fallback: "/dashboard" })).resolves.toBe(
"/dashboard"
);
});
it("rejects removed app and API redirect targets", async () => {
await expect(
getSafeLoginRedirect({ redirectTo: "/app/staging-admin/dashboard", isAdmin: false, defaultTenantId: "tenant-1", fallback: "/dashboard" })
).resolves.toBe("/dashboard");
await expect(getSafeLoginRedirect({ redirectTo: "/v1/health", isAdmin: false, defaultTenantId: "tenant-1", fallback: "/dashboard" })).resolves.toBe(
"/dashboard"
);
await expect(getSafeLoginRedirect({ redirectTo: "/api/v1/health", isAdmin: true, defaultTenantId: "tenant-1", fallback: "/dashboard" })).resolves.toBe(
"/dashboard"
);
});
it("keeps app tenant redirects for non-admin users", async () => {
await expect(
getSafeLoginRedirect({ redirectTo: "/staging-admin/dashboard", isAdmin: false, defaultTenantId: "tenant-1", fallback: "/dashboard" })
).resolves.toBe("https://app.example.com/staging-admin/dashboard");
});
it("does not treat default tenant IDs as URL slugs", async () => {
await expect(
getSafeLoginRedirect({ redirectTo: "/tenant-1/dashboard", isAdmin: false, defaultTenantId: "tenant-1", fallback: "/dashboard" })
).resolves.toBe("/dashboard");
});
it("uses the membership-proven slug consistently without a cache-dependent tenant lookup", async () => {
const input = {
redirectTo: "/staging-admin/dashboard",
isAdmin: false,
defaultTenantId: "tenant-1",
defaultTenantSlug: "staging-admin",
fallback: "/dashboard",
};
await expect(getSafeLoginRedirect(input)).resolves.toBe("https://app.example.com/staging-admin/dashboard");
await expect(getSafeLoginRedirect(input)).resolves.toBe("https://app.example.com/staging-admin/dashboard");
});
it("rejects legacy admin redirects for non-admin users", async () => {
await expect(getSafeLoginRedirect({ redirectTo: "/admin", isAdmin: false, defaultTenantId: "tenant-1", fallback: "/dashboard" })).resolves.toBe(
"/dashboard"
);
await expect(
getSafeLoginRedirect({ redirectTo: "/admin/accounts/users?filter=active", isAdmin: false, defaultTenantId: "tenant-1", fallback: "/dashboard" })
).resolves.toBe("/dashboard");
});
it("rejects canonical admin-host redirects for non-admin users", async () => {
await expect(
getSafeLoginRedirect({ redirectTo: "/dashboard", isAdmin: false, defaultTenantId: "tenant-1", fallback: "/staging-admin/dashboard" })
).resolves.toBe("/staging-admin/dashboard");
await expect(
getSafeLoginRedirect({ redirectTo: "/accounts/users?filter=active", isAdmin: false, defaultTenantId: "tenant-1", fallback: "/staging-admin/dashboard" })
).resolves.toBe("/staging-admin/dashboard");
});
it("keeps non-default tenant deep links on the app host for non-admin users", async () => {
await expect(
getSafeLoginRedirect({ redirectTo: "/tenant-b/dashboard", isAdmin: false, defaultTenantId: "tenant-1", fallback: "/staging-admin/dashboard" })
).resolves.toBe("https://app.example.com/tenant-b/dashboard");
});
it("moves non-admin root fallback redirects to the app host", async () => {
await expect(getSafeLoginRedirect({ redirectTo: "/", isAdmin: false, defaultTenantId: "tenant-1", fallback: "/dashboard" })).resolves.toBe(
"https://app.example.com/"
);
});
it("falls back to admin home when an admin login carries an app tenant redirect", async () => {
await expect(
getSafeLoginRedirect({
redirectTo: "/staging-admin/dashboard",
isAdmin: true,
defaultTenantId: "tenant-1",
defaultTenantSlug: "staging-admin",
fallback: "https://admin.example.com/dashboard",
})
).resolves.toBe("https://admin.example.com/dashboard");
await expect(
getSafeLoginRedirect({
redirectTo: "/app/staging-admin/dashboard",
isAdmin: true,
defaultTenantId: "tenant-1",
defaultTenantSlug: "staging-admin",
fallback: "https://admin.example.com/dashboard",
})
).resolves.toBe("https://admin.example.com/dashboard");
});
it("moves admin redirects to the admin host for admin users", async () => {
await expect(getSafeLoginRedirect({ redirectTo: "/accounts/users", isAdmin: true, defaultTenantId: "tenant-1", fallback: "/dashboard" })).resolves.toBe(
"https://admin.example.com/accounts/users"
);
});
it("canonicalizes legacy admin redirects for admin users", async () => {
await expect(getSafeLoginRedirect({ redirectTo: "/admin/dashboard", isAdmin: true, defaultTenantId: "tenant-1", fallback: "/dashboard" })).resolves.toBe(
"https://admin.example.com/dashboard"
);
await expect(
getSafeLoginRedirect({ redirectTo: "/admin/accounts/users?filter=active", isAdmin: true, defaultTenantId: "tenant-1", fallback: "/dashboard" })
).resolves.toBe("https://admin.example.com/accounts/users?filter=active");
});
});
describe("AuthService production demo login guard", () => {
const stagingDemoEnv = { SENTRY_ENVIRONMENT: "staging", STAGING_DEMO_LOGIN_ENABLED: "true" };
it("blocks local default and legacy demo accounts even when staging demo login is enabled", () => {
expect(isProductionTestAccountBlocked("admin@email.com", stagingDemoEnv)).toBe(true);
expect(isProductionTestAccountBlocked("alex.martinez@company.com", stagingDemoEnv)).toBe(true);
});
it("keeps seeded CRM demo users blocked unless the staging demo guard is explicit", () => {
expect(isStagingDemoLoginAllowed(stagingDemoEnv)).toBe(true);
expect(isProductionTestAccountBlocked("john.doe@company.com", stagingDemoEnv)).toBe(false);
expect(isProductionTestAccountBlocked("luna.davis@company.com", stagingDemoEnv)).toBe(false);
expect(isProductionTestAccountBlocked("john.doe@company.com", { SENTRY_ENVIRONMENT: "staging" })).toBe(true);
expect(isProductionTestAccountBlocked("luna.davis@company.com", { STAGING_DEMO_LOGIN_ENABLED: "true" })).toBe(true);
});
it("allows managed staging employees only in the managed staging runtime", () => {
const email = "anna.becker.01@mitarbeiter.example.test";
expect(isProductionTestAccountBlocked(email, stagingDemoEnv)).toBe(false);
expect(isProductionTestAccountBlocked(email, { SENTRY_ENVIRONMENT: "production", STAGING_DEMO_LOGIN_ENABLED: "true" })).toBe(true);
expect(isProductionTestAccountBlocked(email, { SENTRY_ENVIRONMENT: "staging" })).toBe(true);
});
it("never unlocks a demo account through a SEED_* key, which the deploy contract forbids at runtime", () => {
// Regression: the gate used to ride on these, so enforcing the RLS deploy
// contract (no SEED_* on runtime resources) silently locked staging out.
const seedShapedEnv = {
SENTRY_ENVIRONMENT: "staging",
SEED_DEMO_DATA: "true",
ALLOW_STAGING_DEMO_SEED: "true",
SEED_DEMO_PRO_MANAGED: "true",
} as Record<string, string>;
expect(isStagingDemoLoginAllowed(seedShapedEnv)).toBe(false);
expect(isProductionTestAccountBlocked("john.doe@company.com", seedShapedEnv)).toBe(true);
expect(isProductionTestAccountBlocked("anna.becker.01@mitarbeiter.example.test", seedShapedEnv)).toBe(true);
});
});
describe("AuthService registerFromRequest", () => {
beforeEach(() => {
vi.clearAllMocks();
getTranslationsMock.mockResolvedValue({ t: (key: string) => key, translations: {}, locale: "en" });
getUserInfoMock.mockResolvedValue({ lng: "en" } as Awaited<ReturnType<typeof getUserInfo>>);
getAppConfigurationMock.mockResolvedValue({ app: { features: { tenantHome: "/:tenant/" } } } as Awaited<ReturnType<typeof getAppConfiguration>>);
getRegistrationFormDataMock.mockImplementation(async (request) => {
const formData = await request.formData();
const intent = formData.get("intent")?.toString().trim() || undefined;
const selectedOfficeSeatQuantity = formData.get("selectedOfficeSeatQuantity")?.toString().trim();
const selectedFieldSeatQuantity = formData.get("selectedFieldSeatQuantity")?.toString().trim();
return {
email: formData.get("email")?.toString().toLowerCase().trim(),
password: formData.get("password")?.toString(),
company: formData.get("company")?.toString(),
firstName: formData.get("first-name")?.toString(),
lastName: formData.get("last-name")?.toString(),
slug: formData.get("slug")?.toString(),
leadId: formData.get("leadId")?.toString().trim() || undefined,
leadToken: formData.get("leadToken")?.toString().trim() || undefined,
leadPlanId: formData.get("leadPlanId")?.toString().trim() || undefined,
selectedSubscriptionPriceId: formData.get("selectedSubscriptionPriceId")?.toString().trim() || undefined,
...(selectedOfficeSeatQuantity ? { selectedOfficeSeatQuantity: Number(selectedOfficeSeatQuantity) } : {}),
...(selectedFieldSeatQuantity ? { selectedFieldSeatQuantity: Number(selectedFieldSeatQuantity) } : {}),
creationRequestKey: formData.get("creationRequestKey")?.toString().trim() || undefined,
acceptTerms: formData.get("acceptTerms") === "true",
acceptPrivacy: formData.get("acceptPrivacy") === "true",
...(intent ? { intent } : {}),
};
});
validateRegistrationMock.mockResolvedValue({
email: "ada@example.test",
ipAddress: "203.0.113.5",
verificationRequired: false,
registered: {
user: { id: "user_1", email: "ada@example.test", locale: "de" },
tenant: { id: "tenant_1", slug: "ada-labs" },
},
} as Awaited<ReturnType<typeof validateRegistration>>);
setLoggedUserMock.mockResolvedValue({
userId: "user_1",
defaultTenantId: "tenant_1",
defaultTenant: { id: "tenant_1", name: "Ada Labs", slug: "ada-labs", active: true, deactivatedReason: null },
} as Awaited<ReturnType<typeof setLoggedUser>>);
createUserSessionMock.mockImplementation(
async (_session, redirectTo) =>
new Response(null, {
status: 302,
headers: { Location: redirectTo, "Set-Cookie": "__Host-hivegrid_app_session=signed; Path=/; HttpOnly" },
})
);
registrationRecoveryMocks.read.mockResolvedValue(null);
registrationRecoveryMocks.commit.mockResolvedValue("__Host-hivegrid_registration_resend=signed; Path=/; HttpOnly");
registrationRecoveryMocks.clear.mockResolvedValue("__Host-hivegrid_registration_resend=; Max-Age=0; Path=/; HttpOnly");
resolvePendingRegistrationResendRecoveryMock.mockResolvedValue(null);
});
it("passes lead conversion form fields to direct registration and starts the tenant session", async () => {
const AuthService = (await import("./AuthService")).default;
const formData = new URLSearchParams({
email: "ada@example.test",
password: "Password123!",
company: "Ada Labs",
"first-name": "Ada",
"last-name": "Lovelace",
slug: "ada-labs",
leadId: "lead_1",
leadToken: "signed-token",
leadPlanId: "plan_pro",
selectedSubscriptionPriceId: "price_pro",
selectedOfficeSeatQuantity: "5",
selectedFieldSeatQuantity: "15",
creationRequestKey: CREATION_REQUEST_KEY,
acceptTerms: "true",
acceptPrivacy: "true",
});
const request = new Request("https://app.example.com/register", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: formData,
});
validateRegistrationMock.mockResolvedValueOnce({
email: "ada@example.test",
ipAddress: "203.0.113.5",
verificationRequired: false,
selectedSubscriptionPriceId: "price_pro",
selectedOfficeSeatQuantity: 5,
selectedFieldSeatQuantity: 15,
registered: {
user: { id: "user_1", email: "ada@example.test", locale: "de" },
tenant: { id: "tenant_1", slug: "ada-labs" },
},
} as Awaited<ReturnType<typeof validateRegistration>>);
const response = await AuthService.registerFromRequest(request);
expect(response.status).toBe(302);
expect(response.headers.get("Location")).toBe(
"https://app.example.com/ada-labs/settings/billing?subscription_required=1&selected_price=price_pro&office_seats=5&field_seats=15"
);
expect(response.headers.get("Set-Cookie")).toContain("__Host-hivegrid_app_session=signed");
expect(response.headers.get("Set-Cookie")).toContain("Max-Age=0");
expect(registrationRecoveryMocks.clear).toHaveBeenCalledOnce();
expect(validateCSRFTokenMock).toHaveBeenCalledWith(request);
expect(getRegistrationFormDataMock).toHaveBeenCalledWith(request);
expect(validateRegistrationMock).toHaveBeenCalledWith({
request,
registrationData: {
email: "ada@example.test",
password: "Password123!",
company: "Ada Labs",
firstName: "Ada",
lastName: "Lovelace",
slug: "ada-labs",
leadId: "lead_1",
leadToken: "signed-token",
leadPlanId: "plan_pro",
selectedSubscriptionPriceId: "price_pro",
selectedOfficeSeatQuantity: 5,
selectedFieldSeatQuantity: 15,
creationRequestKey: CREATION_REQUEST_KEY,
acceptTerms: true,
acceptPrivacy: true,
},
addToTrialOrFreePlan: false,
requireSubscriptionSelection: true,
});
expect(createRegistrationFormMock).not.toHaveBeenCalled();
expect(setLoggedUserMock).toHaveBeenCalledWith({ id: "user_1", email: "ada@example.test", locale: "de" });
expect(createUserSessionMock).toHaveBeenCalledWith(
{
lng: "de",
userId: "user_1",
defaultTenantId: "tenant_1",
},
"https://app.example.com/ada-labs/settings/billing?subscription_required=1&selected_price=price_pro&office_seats=5&field_seats=15",
{ request: expect.any(Request), rotate: true }
);
});
it("returns the rotated registration key in action data after a manual resend", async () => {
const AuthService = (await import("./AuthService")).default;
const rotatedKey = "b79f4743-b5ee-48df-9287-10f24b2aedbf";
validateRegistrationMock.mockResolvedValue({
email: "ada@example.test",
ipAddress: "203.0.113.5",
verificationRequired: true,
} as Awaited<ReturnType<typeof validateRegistration>>);
createRegistrationFormMock.mockResolvedValueOnce({ creationRequestKey: CREATION_REQUEST_KEY }).mockResolvedValueOnce({ creationRequestKey: rotatedKey });
const body = {
email: "ada@example.test",
company: "Ada Labs",
"first-name": "Ada",
"last-name": "Lovelace",
creationRequestKey: CREATION_REQUEST_KEY,
acceptTerms: "true",
acceptPrivacy: "true",
};
const initial = await AuthService.registerFromRequest(
new Request("https://app.example.com/register", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams(body),
})
);
const resend = await AuthService.registerFromRequest(
new Request("https://app.example.com/register", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ ...body, intent: "resendVerification" }),
})
);
await expect(initial.json()).resolves.toEqual({ verificationEmailSent: true, creationRequestKey: CREATION_REQUEST_KEY });
await expect(resend.json()).resolves.toEqual({ verificationEmailSent: true, creationRequestKey: rotatedKey });
expect(initial.headers.get("Set-Cookie")).toContain("HttpOnly");
expect(resend.headers.get("Cache-Control")).toBe("no-store");
expect(registrationRecoveryMocks.commit).toHaveBeenNthCalledWith(1, {
email: "ada@example.test",
creationRequestKey: CREATION_REQUEST_KEY,
});
expect(registrationRecoveryMocks.commit).toHaveBeenNthCalledWith(2, {
email: "ada@example.test",
creationRequestKey: rotatedKey,
});
expect(createRegistrationFormMock).toHaveBeenCalledTimes(2);
expect(createRegistrationFormMock.mock.calls.map(([input]) => input.creationRequestKey)).toEqual([CREATION_REQUEST_KEY, CREATION_REQUEST_KEY]);
expect(validateRegistrationMock.mock.calls[1]?.[0]).toMatchObject({ captchaAlreadyVerified: true });
expect(createRegistrationFormMock.mock.calls[1]?.[0]).toMatchObject({ resendOnly: true });
});
it("recovers a reload resend only for the signed email and exact pending Registration binding", async () => {
const AuthService = (await import("./AuthService")).default;
const recoveredKey = "b79f4743-b5ee-48df-9287-10f24b2aedbf";
registrationRecoveryMocks.read.mockResolvedValue({
email: "ada@example.test",
creationRequestKey: CREATION_REQUEST_KEY,
issuedAt: Date.now(),
});
resolvePendingRegistrationResendRecoveryMock.mockResolvedValue({
email: "ada@example.test",
creationRequestKey: recoveredKey,
registrationId: "registration_1",
catchUpOnly: true,
});
validateRegistrationMock.mockImplementation(async ({ registrationData }) => ({
email: registrationData.email!,
ipAddress: "203.0.113.5",
verificationRequired: true,
}));
createRegistrationFormMock.mockResolvedValue({ creationRequestKey: recoveredKey });
const response = await AuthService.registerFromRequest(
new Request("https://app.example.com/register", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded", Cookie: "recovery=signed" },
body: new URLSearchParams({
email: "ada@example.test",
intent: "resendVerification",
creationRequestKey: "1ac679b8-fbc8-4d51-aa2d-51aa31ffa03f",
}),
})
);
expect(resolvePendingRegistrationResendRecoveryMock).toHaveBeenCalledWith(
expect.objectContaining({ email: "ada@example.test", creationRequestKey: CREATION_REQUEST_KEY })
);
expect(validateRegistrationMock).toHaveBeenCalledWith(
expect.objectContaining({ registrationData: expect.objectContaining({ creationRequestKey: recoveredKey }), captchaAlreadyVerified: true })
);
expect(createRegistrationFormMock).toHaveBeenCalledWith(
expect.objectContaining({
email: "ada@example.test",
creationRequestKey: recoveredKey,
resendOnly: true,
resendRecovery: { registrationId: "registration_1", catchUpOnly: true },
})
);
await expect(response.json()).resolves.toEqual({ verificationEmailSent: true, creationRequestKey: recoveredKey });
});
it("never applies a signed recovery binding to another email or a consumed Registration", async () => {
const AuthService = (await import("./AuthService")).default;
registrationRecoveryMocks.read.mockResolvedValue({
email: "ada@example.test",
creationRequestKey: CREATION_REQUEST_KEY,
issuedAt: Date.now(),
});
resolvePendingRegistrationResendRecoveryMock.mockResolvedValue(null);
validateRegistrationMock.mockImplementation(async ({ registrationData }) => ({
email: registrationData.email!,
ipAddress: "203.0.113.5",
verificationRequired: true,
}));
createRegistrationFormMock.mockRejectedValue(new CatalogError(foundationErrors.idempotencyConflict));
const response = await AuthService.registerFromRequest(
new Request("https://app.example.com/register", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded", Cookie: "recovery=signed" },
body: new URLSearchParams({
email: "mallory@example.test",
intent: "resendVerification",
creationRequestKey: "1ac679b8-fbc8-4d51-aa2d-51aa31ffa03f",
}),
})
);
expect(resolvePendingRegistrationResendRecoveryMock).not.toHaveBeenCalled();
expect(createRegistrationFormMock).toHaveBeenCalledWith(expect.not.objectContaining({ resendRecovery: expect.anything() }));
expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual({
verificationEmailSent: true,
creationRequestKey: "1ac679b8-fbc8-4d51-aa2d-51aa31ffa03f",
});
expect(registrationRecoveryMocks.commit).toHaveBeenCalledWith({
email: "mallory@example.test",
creationRequestKey: "1ac679b8-fbc8-4d51-aa2d-51aa31ffa03f",
});
});
it("returns the same public shape for a fresh send, an existing account, and a foreign pending registration", async () => {
const AuthService = (await import("./AuthService")).default;
validateRegistrationMock.mockResolvedValue({
email: "ada@example.test",
ipAddress: "203.0.113.5",
verificationRequired: true,
} as Awaited<ReturnType<typeof validateRegistration>>);
createRegistrationFormMock
.mockResolvedValueOnce({ creationRequestKey: CREATION_REQUEST_KEY })
.mockRejectedValueOnce(new RegistrationValidationError("api.errors.userAlreadyRegistered"))
.mockRejectedValueOnce(new CatalogError(foundationErrors.idempotencyConflict));
const body = new URLSearchParams({
email: "ada@example.test",
creationRequestKey: CREATION_REQUEST_KEY,
acceptTerms: "true",
acceptPrivacy: "true",
});
const responses = [];
for (const variant of ["fresh", "registered", "pending"]) {
responses.push(
await AuthService.registerFromRequest(
new Request(`https://app.example.com/register?variant=${variant}`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body,
})
)
);
}
const responseBodies = await Promise.all(responses.map((response) => response.json()));
expect(responses.map((response) => response.status)).toEqual([200, 200, 200]);
expect(responses.map((response) => response.headers.get("Cache-Control"))).toEqual(["no-store", "no-store", "no-store"]);
expect(responses.map((response) => response.headers.get("Set-Cookie"))).toEqual(
Array.from({ length: 3 }, () => "__Host-hivegrid_registration_resend=signed; Path=/; HttpOnly")
);
expect(responseBodies).toEqual(Array.from({ length: 3 }, () => ({ verificationEmailSent: true, creationRequestKey: CREATION_REQUEST_KEY })));
expect(loggerMocks.info).toHaveBeenCalledTimes(2);
expect(JSON.stringify(loggerMocks.info.mock.calls)).not.toContain("ada@example.test");
});
it("retries an exact direct signup after session response loss with the same creation key", async () => {
const AuthService = (await import("./AuthService")).default;
createUserSessionMock.mockRejectedValueOnce(new Error("response lost after commit"));
const body = {
email: "ada@example.test",
password: "Password123!",
company: "Ada Labs",
"first-name": "Ada",
"last-name": "Lovelace",
slug: "ada-labs",
creationRequestKey: CREATION_REQUEST_KEY,
acceptTerms: "true",
acceptPrivacy: "true",
};
const first = await AuthService.registerFromRequest(
new Request("https://app.example.com/register", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams(body),
})
);
const retry = await AuthService.registerFromRequest(
new Request("https://app.example.com/register", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams(body),
})
);
await expect(first.json()).resolves.toMatchObject({ error: "shared.unknownError", creationRequestKey: CREATION_REQUEST_KEY });
expect(retry.status).toBe(302);
expect(validateRegistrationMock).toHaveBeenCalledTimes(2);
expect(validateRegistrationMock.mock.calls.map((call) => call[0].registrationData.creationRequestKey)).toEqual([
CREATION_REQUEST_KEY,
CREATION_REQUEST_KEY,
]);
});
it("never exposes unknown database or provider error details to the unauthenticated client or logger", async () => {
const AuthService = (await import("./AuthService")).default;
const secretError = "postgres://signup_user:super-secret@db.internal:5432/hivegrid";
validateRegistrationMock.mockRejectedValueOnce(new Error(secretError));
const request = new Request("https://app.example.com/register", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
email: "ada@example.test",
password: "Password123!",
creationRequestKey: CREATION_REQUEST_KEY,
acceptTerms: "true",
acceptPrivacy: "true",
}),
});
const response = await AuthService.registerFromRequest(request);
const responseBody = await response.json();
expect(response.status).toBe(400);
expect(responseBody).toEqual({
error: "shared.unknownError",
creationRequestKey: CREATION_REQUEST_KEY,
captchaResetKey: expect.any(String),
});
expect(JSON.stringify(responseBody)).not.toContain(secretError);
expect(JSON.stringify(loggerMocks.warn.mock.calls)).not.toContain(secretError);
expect(loggerMocks.warn).toHaveBeenCalledWith(
expect.objectContaining({ err: { name: "Error", message: "[redacted]" }, op: "verifyAndRegister" }),
"Auth verify-and-register failed"
);
});
it("rejects a crafted signup POST when either required consent is missing", async () => {
const AuthService = (await import("./AuthService")).default;
const request = new Request("https://app.example.com/register", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
email: "ada@example.test",
password: "Password123!",
creationRequestKey: CREATION_REQUEST_KEY,
acceptTerms: "true",
acceptPrivacy: "false",
}),
});
const response = await AuthService.registerFromRequest(request);
expect(response.status).toBe(400);
await expect(response.json()).resolves.toEqual({
error: "account.register.errors.acceptTermsAndPrivacy",
creationRequestKey: CREATION_REQUEST_KEY,
captchaResetKey: expect.any(String),
});
expect(validateRegistrationMock).not.toHaveBeenCalled();
expect(createRegistrationFormMock).not.toHaveBeenCalled();
});
});
describe("AuthService verifyFromRequest", () => {
beforeEach(() => {
vi.clearAllMocks();
getTranslationsMock.mockResolvedValue({ t: (key: string) => key, translations: {}, locale: "en" });
getUserInfoMock.mockResolvedValue({ lng: "en" } as Awaited<ReturnType<typeof getUserInfo>>);
validateCSRFTokenMock.mockResolvedValue(undefined);
checkAuthRateLimitMock.mockResolvedValue({ allowed: true });
getRegistrationFormDataMock.mockResolvedValue(verificationSubmission());
getRegistrationVerificationStateByTokenMock.mockResolvedValue({
status: "pending",
registration: {
id: "registration_1",
createdTenantId: null,
email: "ada@example.test",
company: "Ada Labs",
firstName: "Ada",
lastName: "Lovelace",
slug: "ada-labs",
leadConversionLeadId: "lead_1",
leadConversionToken: "signed-token",
leadConversionPlanId: "plan_pro",
selectedSubscriptionPriceId: "price_pro",
selectedOfficeSeatQuantity: 5,
selectedFieldSeatQuantity: 15,
},
} as Awaited<ReturnType<typeof getRegistrationVerificationStateByToken>>);
validateRegistrationMock.mockResolvedValue({
email: "ada@example.test",
ipAddress: "203.0.113.5",
verificationRequired: false,
registered: {
user: { id: "user_1", email: "ada@example.test", locale: "de" },
tenant: { id: "tenant_1", slug: "ada-labs" },
},
} as Awaited<ReturnType<typeof validateRegistration>>);
setLoggedUserMock.mockResolvedValue({
userId: "user_1",
defaultTenantId: "tenant_1",
defaultTenant: { id: "tenant_1", name: "Ada Labs", slug: "ada-labs", active: true, deactivatedReason: null },
} as Awaited<ReturnType<typeof setLoggedUser>>);
createUserSessionMock.mockResolvedValue(
new Response(null, {
status: 302,
headers: { Location: "/ada/dashboard", "Set-Cookie": "__Host-hivegrid_app_session=signed; Path=/; HttpOnly" },
}) as Awaited<ReturnType<typeof createUserSession>>
);
registrationRecoveryMocks.clear.mockResolvedValue("__Host-hivegrid_registration_resend=; Max-Age=0; Path=/; HttpOnly");
});
it("uses only persisted identity and lead fields when validating the verified registration", async () => {
const AuthService = (await import("./AuthService")).default;
const request = new Request(`https://example.test/verify/${VERIFICATION_TOKEN}`, { method: "POST" });
validateRegistrationMock.mockResolvedValueOnce({
email: "ada@example.test",
ipAddress: "203.0.113.5",
verificationRequired: false,
selectedSubscriptionPriceId: "price_pro",
selectedOfficeSeatQuantity: 5,
selectedFieldSeatQuantity: 15,
registered: {
user: { id: "user_1", email: "ada@example.test", locale: "de" },
tenant: { id: "tenant_1", slug: "ada-labs" },
},
} as Awaited<ReturnType<typeof validateRegistration>>);
const response = await AuthService.verifyFromRequest({ request, params: { id: VERIFICATION_TOKEN } });
expect(validateRegistrationMock).toHaveBeenCalledWith({
request,
registrationData: {
email: "ada@example.test",
password: "Password123!",
company: "Ada Labs",
firstName: "Ada",
lastName: "Lovelace",
slug: "ada-labs",
leadId: "lead_1",
leadToken: "signed-token",
leadPlanId: "plan_pro",
selectedSubscriptionPriceId: "price_pro",
selectedOfficeSeatQuantity: 5,
selectedFieldSeatQuantity: 15,
creationRequestKey: BOUND_VERIFICATION_KEY,
captchaToken: "captcha-proof",
},
checkEmailVerification: false,
captchaAlreadyVerified: true,
addToTrialOrFreePlan: false,
requireSubscriptionSelection: true,
requireRegistrationSeatQuantities: true,
});
expect(updateRegistrationMock).toHaveBeenCalledWith("registration_1", { expectedToken: VERIFICATION_TOKEN }, { createdTenantId: "tenant_1" });
expect(createUserSessionMock).toHaveBeenCalledWith(
{ lng: "de", userId: "user_1", defaultTenantId: "tenant_1" },
"https://app.example.com/ada-labs/settings/billing?subscription_required=1&selected_price=price_pro&office_seats=5&field_seats=15",
{ request, rotate: true }
);
expect(response.headers.get("Set-Cookie")).toContain("Max-Age=0");
expect(response.headers.get("Set-Cookie")).toContain("__Host-hivegrid_app_session=signed");
expect(registrationRecoveryMocks.clear).toHaveBeenCalledOnce();
expect(validateCSRFTokenMock).toHaveBeenCalledWith(request);
expect(validateCSRFTokenMock.mock.invocationCallOrder[0]).toBeLessThan(
getRegistrationVerificationStateByTokenMock.mock.invocationCallOrder[0] ?? Number.MAX_SAFE_INTEGER
);
});
it("stops before token lookup and account creation when CSRF validation fails", async () => {
validateCSRFTokenMock.mockRejectedValueOnce(new Error("invalid csrf"));
const AuthService = (await import("./AuthService")).default;
const request = new Request("https://example.test/verify/signed-registration-token", { method: "POST" });
await expect(AuthService.verifyFromRequest({ request, params: { id: "signed-registration-token" } })).rejects.toThrow("invalid csrf");
expect(getRegistrationVerificationStateByTokenMock).not.toHaveBeenCalled();
expect(getRegistrationFormDataMock).not.toHaveBeenCalled();
expect(validateRegistrationMock).not.toHaveBeenCalled();
expect(updateRegistrationMock).not.toHaveBeenCalled();
});
it("rate-limits verification before token lookup, hashing or bootstrap", async () => {
checkAuthRateLimitMock.mockResolvedValueOnce({ allowed: false, retryAfterSeconds: 37 });
const AuthService = (await import("./AuthService")).default;
const request = new Request(`https://example.test/verify/${VERIFICATION_TOKEN}`, { method: "POST" });
const response = await AuthService.verifyFromRequest({ request, params: { id: VERIFICATION_TOKEN } });
expect(response.status).toBe(429);
expect(response.headers.get("Retry-After")).toBe("37");
expect(response.headers.get("Cache-Control")).toBe("no-store");
await expect(response.json()).resolves.toEqual({ error: "api.errors.tooManyRequests" });
expect(checkAuthRateLimitMock).toHaveBeenCalledWith({
request,
action: "registration-verify",
identifier: expect.stringMatching(/^[a-f0-9]{64}$/u),
limit: 5,
windowMs: 15 * 60 * 1000,
});
expect(checkAuthRateLimitMock.mock.calls[0]?.[0].identifier).not.toContain(VERIFICATION_TOKEN);
expect(getUserInfoMock).not.toHaveBeenCalled();
expect(getRegistrationVerificationStateByTokenMock).not.toHaveBeenCalled();
expect(getRegistrationFormDataMock).not.toHaveBeenCalled();
expect(validateRegistrationMock).not.toHaveBeenCalled();
expect(updateRegistrationMock).not.toHaveBeenCalled();
expect(setLoggedUserMock).not.toHaveBeenCalled();
expect(createUserSessionMock).not.toHaveBeenCalled();
});
it("rejects missing verification tokens before parsing form data or mutating registration state", async () => {
getRegistrationVerificationStateByTokenMock.mockResolvedValueOnce({ status: "invalid" });
const AuthService = (await import("./AuthService")).default;
const response = await AuthService.verifyFromRequest({
request: new Request("https://example.test/verify/missing-token", { method: "POST" }),
params: { id: "missing-token" },
});
const data = await response.json();
expect(response.status).toBe(400);
expect(data).toEqual({ error: "account.verify.invalidLink" });
expect(getRegistrationFormDataMock).not.toHaveBeenCalled();
expect(validateRegistrationMock).not.toHaveBeenCalled();
expect(updateRegistrationMock).not.toHaveBeenCalled();
expect(setLoggedUserMock).not.toHaveBeenCalled();
expect(createUserSessionMock).not.toHaveBeenCalled();
});
it("does not bootstrap or issue a session when the TTL helper rejects an expired token", async () => {
getRegistrationVerificationStateByTokenMock.mockResolvedValueOnce({
status: "expired",
registration: { id: "registration_1" },
} as Awaited<ReturnType<typeof getRegistrationVerificationStateByToken>>);
const AuthService = (await import("./AuthService")).default;
const response = await AuthService.verifyFromRequest({
request: new Request(`https://example.test/verify/${VERIFICATION_TOKEN}`, { method: "POST" }),
params: { id: VERIFICATION_TOKEN },
});
expect(response.status).toBe(400);
await expect(response.json()).resolves.toEqual({ error: "account.verify.expiredLink" });
expect(getRegistrationFormDataMock).not.toHaveBeenCalled();
expect(validateRegistrationMock).not.toHaveBeenCalled();
expect(updateRegistrationMock).not.toHaveBeenCalled();
expect(setLoggedUserMock).not.toHaveBeenCalled();
expect(createUserSessionMock).not.toHaveBeenCalled();
});
it("rejects already-consumed verification tokens before tenant/user creation work", async () => {
getRegistrationVerificationStateByTokenMock.mockResolvedValueOnce({
status: "used",
registration: { id: "registration_1", createdTenantId: "tenant_1" },
} as Awaited<ReturnType<typeof getRegistrationVerificationStateByToken>>);
const AuthService = (await import("./AuthService")).default;
const response = await AuthService.verifyFromRequest({
request: new Request("https://example.test/verify/used-token", { method: "POST" }),
params: { id: "used-token" },
});
const data = await response.json();
expect(response.status).toBe(400);
expect(data).toEqual({ error: "account.verify.usedLink" });
expect(getRegistrationFormDataMock).not.toHaveBeenCalled();
expect(validateRegistrationMock).not.toHaveBeenCalled();
expect(updateRegistrationMock).not.toHaveBeenCalled();
expect(setLoggedUserMock).not.toHaveBeenCalled();
expect(createUserSessionMock).not.toHaveBeenCalled();
});
it("does not consume the verification token when registration validation does not create an account", async () => {
validateRegistrationMock.mockResolvedValueOnce({
email: "ada@example.test",
ipAddress: "203.0.113.5",
verificationRequired: false,
registered: undefined,
} as Awaited<ReturnType<typeof validateRegistration>>);
const AuthService = (await import("./AuthService")).default;
const response = await AuthService.verifyFromRequest({
request: new Request("https://example.test/verify/signed-registration-token", { method: "POST" }),
params: { id: "signed-registration-token" },
});
const data = await response.json();
expect(response.status).toBe(400);
expect(data).toEqual({ error: "shared.unknownError", creationRequestKey: BOUND_VERIFICATION_KEY });
expect(updateRegistrationMock).not.toHaveBeenCalled();
expect(setLoggedUserMock).not.toHaveBeenCalled();
expect(createUserSessionMock).not.toHaveBeenCalled();
});
it("rejects a browser-selected creation key before bootstrap", async () => {
const AuthService = (await import("./AuthService")).default;
getRegistrationFormDataMock.mockResolvedValueOnce(verificationSubmission("b79f4743-b5ee-48df-9287-10f24b2aedbf"));
const response = await AuthService.verifyFromRequest({
request: new Request(`https://example.test/verify/${VERIFICATION_TOKEN}`, { method: "POST" }),
params: { id: VERIFICATION_TOKEN },
});
expect(response.status).toBe(400);
expect(validateRegistrationMock).not.toHaveBeenCalled();
expect(updateRegistrationMock).not.toHaveBeenCalled();
expect(setLoggedUserMock).not.toHaveBeenCalled();
expect(createUserSessionMock).not.toHaveBeenCalled();
});
it("does not consume the token or create a session when exact password replay validation fails", async () => {
const AuthService = (await import("./AuthService")).default;
getRegistrationFormDataMock.mockResolvedValueOnce(verificationSubmission(BOUND_VERIFICATION_KEY, "wrong password"));
validateRegistrationMock.mockRejectedValueOnce(new Error("signup replay conflict"));
const response = await AuthService.verifyFromRequest({
request: new Request(`https://example.test/verify/${VERIFICATION_TOKEN}`, { method: "POST" }),
params: { id: VERIFICATION_TOKEN },
});
expect(response.status).toBe(400);
expect(updateRegistrationMock).not.toHaveBeenCalled();
expect(setLoggedUserMock).not.toHaveBeenCalled();
expect(createUserSessionMock).not.toHaveBeenCalled();
});
it("leaves verification unconsumed until required subscription effects succeed on retry", async () => {
const AuthService = (await import("./AuthService")).default;
validateRegistrationMock.mockRejectedValueOnce(new Error("subscription transaction unavailable"));
const first = await AuthService.verifyFromRequest({
request: new Request(`https://example.test/verify/${VERIFICATION_TOKEN}`, { method: "POST" }),
params: { id: VERIFICATION_TOKEN },
});
expect(first.status).toBe(400);
expect(updateRegistrationMock).not.toHaveBeenCalled();
expect(setLoggedUserMock).not.toHaveBeenCalled();
expect(createUserSessionMock).not.toHaveBeenCalled();
const retry = await AuthService.verifyFromRequest({
request: new Request(`https://example.test/verify/${VERIFICATION_TOKEN}`, { method: "POST" }),
params: { id: VERIFICATION_TOKEN },
});
expect(retry.status).toBe(302);
expect(validateRegistrationMock).toHaveBeenCalledTimes(2);
expect(updateRegistrationMock).toHaveBeenCalledTimes(1);
expect(setLoggedUserMock).toHaveBeenCalledTimes(1);
expect(createUserSessionMock).toHaveBeenCalledTimes(1);
});
it("replays safely after account commit when response loss happens before token consumption", async () => {
const AuthService = (await import("./AuthService")).default;
updateRegistrationMock.mockRejectedValueOnce(new Error("connection lost before CAS response")).mockResolvedValueOnce(true);
const first = await AuthService.verifyFromRequest({
request: new Request(`https://example.test/verify/${VERIFICATION_TOKEN}`, { method: "POST" }),
params: { id: VERIFICATION_TOKEN },
});
const retry = await AuthService.verifyFromRequest({
request: new Request(`https://example.test/verify/${VERIFICATION_TOKEN}`, { method: "POST" }),
params: { id: VERIFICATION_TOKEN },
});
await expect(first.json()).resolves.toEqual({ error: "shared.unknownError", creationRequestKey: BOUND_VERIFICATION_KEY });
expect(retry.status).toBe(302);
expect(validateRegistrationMock).toHaveBeenCalledTimes(2);
expect(updateRegistrationMock).toHaveBeenCalledTimes(2);
expect(createUserSessionMock).toHaveBeenCalledTimes(1);
});
it("recovers with the same bootstrap identity when resend rotates the token after account commit", async () => {
const AuthService = (await import("./AuthService")).default;
const rotatedToken = "rotated-registration-token";
updateRegistrationMock.mockRejectedValueOnce(new Error("stale token after concurrent resend")).mockResolvedValueOnce(true);
const staleAttempt = await AuthService.verifyFromRequest({
request: new Request(`https://example.test/verify/${VERIFICATION_TOKEN}`, { method: "POST" }),
params: { id: VERIFICATION_TOKEN },
});
const currentAttempt = await AuthService.verifyFromRequest({
request: new Request(`https://example.test/verify/${rotatedToken}`, { method: "POST" }),
params: { id: rotatedToken },
});
await expect(staleAttempt.json()).resolves.toEqual({ error: "shared.unknownError", creationRequestKey: BOUND_VERIFICATION_KEY });
expect(currentAttempt.status).toBe(302);
expect(validateRegistrationMock.mock.calls.map(([input]) => input.registrationData.creationRequestKey)).toEqual([
BOUND_VERIFICATION_KEY,
BOUND_VERIFICATION_KEY,
]);
expect(updateRegistrationMock).toHaveBeenNthCalledWith(1, "registration_1", { expectedToken: VERIFICATION_TOKEN }, { createdTenantId: "tenant_1" });
expect(updateRegistrationMock).toHaveBeenNthCalledWith(2, "registration_1", { expectedToken: rotatedToken }, { createdTenantId: "tenant_1" });
expect(setLoggedUserMock).toHaveBeenCalledTimes(1);
expect(createUserSessionMock).toHaveBeenCalledTimes(1);
});
it("allows only the registration-bound key when parallel verify posts submit different UUIDs", async () => {
const AuthService = (await import("./AuthService")).default;
getRegistrationFormDataMock.mockImplementation(async (request) =>
new URL(request.url).searchParams.has("tampered") ? verificationSubmission("b79f4743-b5ee-48df-9287-10f24b2aedbf") : verificationSubmission()
);
const responses = await Promise.all([
AuthService.verifyFromRequest({
request: new Request(`https://example.test/verify/${VERIFICATION_TOKEN}`, { method: "POST" }),
params: { id: VERIFICATION_TOKEN },
}),
AuthService.verifyFromRequest({
request: new Request(`https://example.test/verify/${VERIFICATION_TOKEN}?tampered=1`, { method: "POST" }),
params: { id: VERIFICATION_TOKEN },
}),
]);
expect(responses.map((response) => response.status).sort()).toEqual([302, 400]);
expect(validateRegistrationMock).toHaveBeenCalledTimes(1);
expect(updateRegistrationMock).toHaveBeenCalledTimes(1);
expect(createUserSessionMock).toHaveBeenCalledTimes(1);
});
it("issues at most one session when parallel verify posts race the token consume CAS", async () => {
const AuthService = (await import("./AuthService")).default;
let consumed = false;
updateRegistrationMock.mockImplementation(async () => {
if (consumed) throw new Error("registration capability was already consumed");
consumed = true;
return true;
});
const responses = await Promise.all([
AuthService.verifyFromRequest({
request: new Request(`https://example.test/verify/${VERIFICATION_TOKEN}?attempt=1`, { method: "POST" }),
params: { id: VERIFICATION_TOKEN },
}),
AuthService.verifyFromRequest({
request: new Request(`https://example.test/verify/${VERIFICATION_TOKEN}?attempt=2`, { method: "POST" }),
params: { id: VERIFICATION_TOKEN },
}),
]);
expect(responses.map((response) => response.status).sort()).toEqual([302, 400]);
expect(validateRegistrationMock).toHaveBeenCalledTimes(2);
expect(updateRegistrationMock).toHaveBeenCalledTimes(2);
expect(setLoggedUserMock).toHaveBeenCalledTimes(1);
expect(createUserSessionMock).toHaveBeenCalledTimes(1);
});
});