221 lines
9.2 KiB
TypeScript
221 lines
9.2 KiB
TypeScript
// @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",
|
|
});
|
|
});
|
|
});
|