// @vitest-environment node import { beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ findInBlacklist: vi.fn(), getAppConfiguration: vi.fn(), getTenantBySlug: vi.fn(), getUserByEmail: vi.fn(), register: vi.fn(), createTenant: vi.fn(), createTenantUser: vi.fn(), getAllRoles: vi.fn(), verifyLeadToken: vi.fn(), recordExistingTenantLeadConversion: vi.fn(), leadFindUnique: vi.fn(), })); vi.mock("~/locale/i18next.server", () => ({ getTranslations: vi.fn(async () => ({ t: (key: string) => key })), })); vi.mock("~/utils/session.server", () => ({ getUserInfo: vi.fn(async () => ({ lng: "en" })), })); vi.mock("~/utils/db/appConfiguration.db.server", () => ({ getAppConfiguration: (...args: unknown[]) => mocks.getAppConfiguration(...args), })); vi.mock("~/utils/app/UserValidationUtils", () => ({ default: { validateEmail: vi.fn(() => true), validatePassword: vi.fn(() => true), }, })); vi.mock("~/services/auth/password-policy.server", () => ({ validatePassword: vi.fn(async () => ({ valid: true, errors: [] })), })); vi.mock("~/utils/db/blacklist.db.server", () => ({ addBlacklistAttempt: vi.fn(), findInBlacklist: (...args: unknown[]) => mocks.findInBlacklist(...args), })); vi.mock("~/utils/db/users.db.server", () => ({ getUserByEmail: (...args: unknown[]) => mocks.getUserByEmail(...args), register: (...args: unknown[]) => mocks.register(...args), })); vi.mock("~/utils/db/tenants.db.server", () => ({ createTenant: (...args: unknown[]) => mocks.createTenant(...args), createTenantUser: (...args: unknown[]) => mocks.createTenantUser(...args), getTenantBySlug: (...args: unknown[]) => mocks.getTenantBySlug(...args), })); vi.mock("~/utils/db/permissions/roles.db.server", () => ({ getAllRoles: (...args: unknown[]) => mocks.getAllRoles(...args) })); vi.mock("~/utils/db/registration.db.server", () => ({ createRegistration: vi.fn(), getRegistrationByEmail: vi.fn(), updateRegistration: vi.fn(), })); vi.mock("~/utils/db.server", () => ({ db: { lead: { findUnique: (...args: unknown[]) => mocks.leadFindUnique(...args) } } })); vi.mock("~/utils/stripe.server", () => ({ createStripeCustomer: vi.fn(), isStripeConfigured: vi.fn(() => false) })); vi.mock("~/utils/url.server", () => ({ getAbsoluteUrl: vi.fn(() => "https://example.test") })); vi.mock("~/modules/events/services/.server/EventsService", () => ({ default: { create: vi.fn() } })); vi.mock("~/modules/emails/services/TransactionalEmailQueue.server", () => ({ queueTransactionalEmail: vi.fn() })); vi.mock("~/modules/emails/utils/EmailTemplates", () => ({ default: { WELCOME_EMAIL: { parse: vi.fn(() => ({ subject: "Welcome", body: "Welcome" })) } } })); vi.mock("~/locale/mail-translator.server", () => ({ getMailTranslator: vi.fn(async () => ({ t: (key: string) => key, locale: "en" })), getMailTranslatorFromRequest: vi.fn(), })); vi.mock("~/modules/ipAddress/services/IpAddressService.server", () => ({ default: { log: vi.fn() } })); vi.mock("~/utils/api/.server/TenantTypesApi", () => ({ TenantTypesApi: { setTenantTypes: vi.fn() } })); vi.mock("~/utils/leads/leadToken.server", () => ({ verifyLeadToken: (...args: unknown[]) => mocks.verifyLeadToken(...args) })); vi.mock("~/utils/leads/convertLead.server", () => ({ recordExistingTenantLeadConversion: (...args: unknown[]) => mocks.recordExistingTenantLeadConversion(...args) })); import { validateRegistration } from "./authService"; const request = new Request("https://example.test/register"); function appConfiguration(slug: { require: boolean; type: "tenant" | "user" } = { require: false, type: "tenant" }) { return { auth: { requireEmailVerification: false, requireOrganization: false, requireName: false, slug, }, }; } describe("validateRegistration i18n errors", () => { beforeEach(() => { vi.clearAllMocks(); mocks.getAppConfiguration.mockResolvedValue(appConfiguration()); mocks.findInBlacklist.mockResolvedValue(null); mocks.getUserByEmail.mockResolvedValue(null); mocks.getTenantBySlug.mockResolvedValue(null); mocks.createTenant.mockResolvedValue({ id: "tenant_1", name: "Ada", slug: "ada" }); mocks.register.mockResolvedValue({ id: "user_1", email: "ada@example.test", locale: "en" }); mocks.createTenantUser.mockResolvedValue({}); mocks.getAllRoles.mockResolvedValue([]); mocks.verifyLeadToken.mockReturnValue(null); mocks.recordExistingTenantLeadConversion.mockResolvedValue({}); mocks.leadFindUnique.mockResolvedValue(null); delete process.env.LEAD_CONVERSION_ENABLED; }); it.each([ ["companyMaxLength", { company: "a".repeat(101) }], ["firstNameMaxLength", { firstName: "a".repeat(51) }], ["lastNameMaxLength", { lastName: "a".repeat(51) }], ])("uses a translation key for %s", async (errorKey, registrationData) => { await expect( validateRegistration({ request, registrationData: { email: "ada@example.test", password: "Password123!", ...registrationData }, addToTrialOrFreePlan: false, }) ).rejects.toThrow(`account.register.errors.${errorKey}`); }); it.each([ ["tenant", "slugRequired"], ["user", "usernameRequired"], ] as const)("uses a translation key when the %s slug is required", async (slugType, errorKey) => { mocks.getAppConfiguration.mockResolvedValueOnce(appConfiguration({ require: true, type: slugType })); await expect( validateRegistration({ request, registrationData: { email: "ada@example.test", password: "Password123!" }, addToTrialOrFreePlan: false, }) ).rejects.toThrow(`account.register.errors.${errorKey}`); }); it.each([ ["tenant", "slugTaken"], ["user", "usernameTaken"], ] as const)("uses a translation key when the %s slug is already taken", async (slugType, errorKey) => { mocks.getAppConfiguration.mockResolvedValueOnce(appConfiguration({ require: true, type: slugType })); mocks.getTenantBySlug.mockResolvedValueOnce({ id: "tenant_1" }); await expect( validateRegistration({ request, registrationData: { email: "ada@example.test", password: "Password123!", slug: "ada" }, addToTrialOrFreePlan: false, }) ).rejects.toThrow(`account.register.errors.${errorKey}`); }); it.each([ ["reserved", "slugReserved", "admin"], ["too short", "slugTooShort", "x"], ] as const)("rejects a user-provided slug that is %s, independent of auth.slug.require", async (_label, errorKey, slug) => { await expect( validateRegistration({ request, registrationData: { email: "ada@example.test", password: "Password123!", slug }, addToTrialOrFreePlan: false, }) ).rejects.toThrow(`account.register.errors.${errorKey}`); }); it.each([ ["only plan id", { email: "ada@example.test", password: "Password123!", leadPlanId: "plan_pro" }], ["missing token", { email: "ada@example.test", password: "Password123!", leadId: "lead_1" }], ["token for a different lead", { email: "ada@example.test", password: "Password123!", leadId: "lead_1", leadToken: "signed" }], ["converted lead", { email: "ada@example.test", password: "Password123!", leadId: "lead_1", leadToken: "signed" }], ["email mismatch", { email: "ada@example.test", password: "Password123!", leadId: "lead_1", leadToken: "signed" }], ])("rejects invalid lead conversion context: %s", async (name, registrationData) => { if (name === "token for a different lead") { mocks.verifyLeadToken.mockReturnValueOnce({ leadId: "other_lead", planId: "plan_token" }); } else if (name === "converted lead") { mocks.verifyLeadToken.mockReturnValueOnce({ leadId: "lead_1", planId: "plan_token" }); mocks.leadFindUnique.mockResolvedValueOnce({ email: "ada@example.test", convertedToTenantId: "tenant_existing" }); } else if (name === "email mismatch") { mocks.verifyLeadToken.mockReturnValueOnce({ leadId: "lead_1", planId: "plan_token" }); mocks.leadFindUnique.mockResolvedValueOnce({ email: "grace@example.test", convertedToTenantId: null }); } await expect( validateRegistration({ request, registrationData, addToTrialOrFreePlan: false, }) ).rejects.toThrow("account.register.errors.invalidLeadConversion"); expect(mocks.recordExistingTenantLeadConversion).not.toHaveBeenCalled(); }); it("records valid self-service lead conversion with token plan precedence", async () => { mocks.verifyLeadToken.mockReturnValueOnce({ leadId: "lead_1", planId: "plan_from_token" }); mocks.leadFindUnique.mockResolvedValueOnce({ email: "ada@example.test", convertedToTenantId: null }); await expect( validateRegistration({ request, registrationData: { email: "ada@example.test", password: "Password123!", leadId: "lead_1", leadToken: "signed", leadPlanId: "plan_from_form", }, addToTrialOrFreePlan: false, }) ).resolves.toMatchObject({ verificationRequired: false, registered: { tenant: { id: "tenant_1" }, user: { id: "user_1" } } }); expect(mocks.recordExistingTenantLeadConversion).toHaveBeenCalledWith({ leadId: "lead_1", tenantId: "tenant_1", planId: "plan_from_token", conversionPath: "self-service", }); }); });