Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/utils/services/authService.transactional-email.test.ts
T

361 lines
14 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import { createRegistrationForm, createUserAndTenant, getRegistrationFormData } from "./authService";
import { getAppConfiguration } from "~/utils/db/appConfiguration.db.server";
import { createRegistration, getRegistrationByEmail, updateRegistration } from "~/utils/db/registration.db.server";
import { createTenant, createTenantUser } from "~/utils/db/tenants.db.server";
import { getAllRoles } from "~/utils/db/permissions/roles.db.server";
import { register } from "~/utils/db/users.db.server";
import { queueTransactionalEmail } from "~/modules/emails/services/TransactionalEmailQueue.server";
import EmailTemplates from "~/modules/emails/utils/EmailTemplates";
import { TenantTypesApi } from "~/utils/api/.server/TenantTypesApi";
import EventsService from "~/modules/events/services/.server/EventsService";
import { sendEmail } from "~/modules/emails/services/EmailService";
vi.mock("~/utils/db/appConfiguration.db.server", () => ({
getAppConfiguration: vi.fn(),
}));
vi.mock("~/utils/db/registration.db.server", () => ({
createRegistration: vi.fn(),
getRegistrationByEmail: vi.fn(),
updateRegistration: vi.fn(),
}));
vi.mock("~/utils/db/tenants.db.server", () => ({
createTenant: vi.fn(),
createTenantUser: vi.fn(),
getTenantBySlug: vi.fn(),
}));
vi.mock("~/utils/db/permissions/roles.db.server", () => ({
getAllRoles: vi.fn(),
}));
vi.mock("~/utils/db/users.db.server", () => ({
getUserByEmail: vi.fn(),
register: vi.fn(),
}));
vi.mock("~/modules/emails/services/TransactionalEmailQueue.server", () => ({
queueTransactionalEmail: vi.fn(),
}));
vi.mock("~/modules/emails/services/EmailService", () => ({
sendEmail: vi.fn(),
}));
vi.mock("~/utils/url.server", () => ({
getAbsoluteUrl: vi.fn((_request: Request, path: string) => `https://example.com${path}`),
}));
vi.mock("~/utils/session.server", () => ({
getUserInfo: vi.fn(async () => ({ lng: "en" })),
}));
vi.mock("crypto", () => ({
default: {
randomBytes: vi.fn(() => ({ toString: vi.fn(() => "registration-token") })),
},
}));
vi.mock("~/modules/emails/utils/EmailTemplates", () => ({
default: {
VERIFICATION_EMAIL: {
parse: vi.fn(({ data }) => ({
subject: "Verify your email",
body: `<p>Verify at ${data.action_url} for ${data.name}</p>`,
})),
},
WELCOME_EMAIL: {
parse: vi.fn(({ data }) => ({
subject: "Welcome",
body: `<p>Welcome at ${data.action_url} for ${data.name}</p>`,
})),
},
},
}));
vi.mock("~/utils/api/.server/TenantTypesApi", () => ({
TenantTypesApi: {
setTenantTypes: vi.fn(),
},
}));
vi.mock("~/modules/events/services/.server/EventsService", () => ({
default: {
create: vi.fn(),
},
}));
vi.mock("~/utils/stripe.server", () => ({
createStripeCustomer: vi.fn(),
isStripeConfigured: vi.fn(() => false),
}));
vi.mock("~/modules/ipAddress/services/IpAddressService.server", () => ({
default: {
log: vi.fn(async () => undefined),
},
}));
vi.mock("~/utils/db.server", () => ({
db: {},
}));
vi.mock("~/locale/i18next.server", () => ({
getTranslations: vi.fn(async () => ({ t: (key: string) => key })),
remixI18Next: {
getFixedT: vi.fn(async () => (key: string) => key),
getLocale: vi.fn(async () => "de"),
},
}));
vi.mock("~/locale/mail-translator.server", () => ({
getMailTranslator: vi.fn(async () => ({ t: (key: string) => key, locale: "de" })),
getMailTranslatorFromRequest: vi.fn(async () => ({ t: (key: string) => key, locale: "de" })),
resolveMailLocale: vi.fn((locale: string | null | undefined) => locale ?? "de"),
}));
const getAppConfigurationMock = vi.mocked(getAppConfiguration);
const getRegistrationByEmailMock = vi.mocked(getRegistrationByEmail);
const updateRegistrationMock = vi.mocked(updateRegistration);
const createRegistrationMock = vi.mocked(createRegistration);
const queueTransactionalEmailMock = vi.mocked(queueTransactionalEmail);
const verificationParseMock = vi.mocked(EmailTemplates.VERIFICATION_EMAIL.parse);
const welcomeParseMock = vi.mocked(EmailTemplates.WELCOME_EMAIL.parse);
const createTenantMock = vi.mocked(createTenant);
const registerMock = vi.mocked(register);
const getAllRolesMock = vi.mocked(getAllRoles);
const createTenantUserMock = vi.mocked(createTenantUser);
const setTenantTypesMock = vi.mocked(TenantTypesApi.setTenantTypes);
const eventsCreateMock = vi.mocked(EventsService.create);
const sendEmailMock = vi.mocked(sendEmail);
const request = new Request("https://example.com/register");
const appConfiguration = {
app: { name: "Example" },
email: { fromName: "Support", supportEmail: "support@example.com" },
} as Awaited<ReturnType<typeof getAppConfiguration>>;
describe("auth service transactional registration emails", () => {
beforeEach(() => {
vi.clearAllMocks();
getAppConfigurationMock.mockResolvedValue(appConfiguration);
queueTransactionalEmailMock.mockResolvedValue({ enqueued: true, jobId: "job_1" });
});
it("recreates pending registration tokens and enqueues verification email", async () => {
getRegistrationByEmailMock.mockResolvedValue({ id: "registration_1", createdTenantId: null } as Awaited<ReturnType<typeof getRegistrationByEmail>>);
updateRegistrationMock.mockResolvedValue({ id: "registration_1" } as Awaited<ReturnType<typeof updateRegistration>>);
await createRegistrationForm({ request, email: "ada@example.com", ipAddress: "127.0.0.1", firstName: "Ada", company: "Ada Co", recreateToken: true });
expect(updateRegistrationMock).toHaveBeenCalledWith("registration_1", {
firstName: "Ada",
lastName: undefined,
company: "Ada Co",
token: "registration-token",
leadConversionLeadId: null,
leadConversionToken: null,
leadConversionPlanId: null,
});
expect(verificationParseMock).toHaveBeenCalledWith(
expect.objectContaining({
appConfiguration,
data: {
action_url: "https://example.com/verify/registration-token",
name: "Ada",
},
t: expect.any(Function),
locale: expect.any(String),
})
);
expect(queueTransactionalEmailMock).toHaveBeenCalledWith({
to: "ada@example.com",
alias: "email-verification",
subject: "Verify your email",
body: "<p>Verify at https://example.com/verify/registration-token for Ada</p>",
data: {
action_url: "https://example.com/verify/registration-token",
name: "Ada",
},
priorityOverride: "critical",
});
expect(queueTransactionalEmailMock.mock.invocationCallOrder[0]).toBeGreaterThan(updateRegistrationMock.mock.invocationCallOrder[0]);
expect(sendEmailMock).not.toHaveBeenCalled();
});
it("refreshes persisted lead conversion context when recreating pending registration tokens", async () => {
getRegistrationByEmailMock.mockResolvedValue({ id: "registration_1", createdTenantId: null } as Awaited<ReturnType<typeof getRegistrationByEmail>>);
updateRegistrationMock.mockResolvedValue({ id: "registration_1" } as Awaited<ReturnType<typeof updateRegistration>>);
await createRegistrationForm({
request,
email: "lead@example.com",
ipAddress: "127.0.0.1",
firstName: "Ada",
lastName: "Lovelace",
company: "Ada Co",
recreateToken: true,
leadId: "lead_2",
leadToken: "signed-token-2",
leadPlanId: "plan_scale",
});
expect(updateRegistrationMock).toHaveBeenCalledWith("registration_1", {
firstName: "Ada",
lastName: "Lovelace",
company: "Ada Co",
token: "registration-token",
leadConversionLeadId: "lead_2",
leadConversionToken: "signed-token-2",
leadConversionPlanId: "plan_scale",
});
expect(queueTransactionalEmailMock).toHaveBeenCalledWith(
expect.objectContaining({
to: "lead@example.com",
alias: "email-verification",
priorityOverride: "critical",
})
);
expect(queueTransactionalEmailMock.mock.invocationCallOrder[0]).toBeGreaterThan(updateRegistrationMock.mock.invocationCallOrder[0]);
expect(sendEmailMock).not.toHaveBeenCalled();
});
it("creates new registration records and enqueues verification email", async () => {
getRegistrationByEmailMock.mockResolvedValue(null);
createRegistrationMock.mockResolvedValue({ id: "registration_1" } as Awaited<ReturnType<typeof createRegistration>>);
await createRegistrationForm({ request, email: "ada@example.com", ipAddress: "127.0.0.1", firstName: "Ada", lastName: "Lovelace", company: "Ada Co", slug: "ada-co" });
expect(createRegistrationMock).toHaveBeenCalledWith({
email: "ada@example.com",
firstName: "Ada",
lastName: "Lovelace",
company: "Ada Co",
token: "registration-token",
selectedSubscriptionPriceId: null,
leadConversionLeadId: null,
leadConversionToken: null,
leadConversionPlanId: null,
ipAddress: "127.0.0.1",
slug: "ada-co",
});
expect(queueTransactionalEmailMock).toHaveBeenCalledWith({
to: "ada@example.com",
alias: "email-verification",
subject: "Verify your email",
body: "<p>Verify at https://example.com/verify/registration-token for Ada</p>",
data: {
action_url: "https://example.com/verify/registration-token",
name: "Ada",
},
priorityOverride: "critical",
});
expect(queueTransactionalEmailMock.mock.invocationCallOrder[0]).toBeGreaterThan(createRegistrationMock.mock.invocationCallOrder[0]);
expect(sendEmailMock).not.toHaveBeenCalled();
});
it("persists signed lead conversion context for email verification registrations", async () => {
getRegistrationByEmailMock.mockResolvedValue(null);
createRegistrationMock.mockResolvedValue({ id: "registration_1" } as Awaited<ReturnType<typeof createRegistration>>);
await createRegistrationForm({
request,
email: "lead@example.com",
ipAddress: "127.0.0.1",
firstName: "Ada",
company: "Ada Co",
leadId: "lead_1",
leadToken: "signed-token",
leadPlanId: "plan_pro",
});
expect(createRegistrationMock).toHaveBeenCalledWith(
expect.objectContaining({
email: "lead@example.com",
leadConversionLeadId: "lead_1",
leadConversionToken: "signed-token",
leadConversionPlanId: "plan_pro",
})
);
});
it("parses signed lead conversion fields from registration posts", async () => {
const form = new URLSearchParams({
email: "Lead@Example.com ",
password: "Password123!",
company: "Ada Co",
leadId: "lead_1",
leadToken: "signed-token",
leadPlanId: "plan_pro",
});
await expect(
getRegistrationFormData(
new Request("https://example.com/register", {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: form,
})
)
).resolves.toMatchObject({
email: "lead@example.com",
company: "Ada Co",
leadId: "lead_1",
leadToken: "signed-token",
leadPlanId: "plan_pro",
});
});
it("creates new accounts before enqueueing welcome email with account metadata", async () => {
createTenantMock.mockResolvedValue({ id: "tenant_1", name: "Ada Co", slug: "ada-co" } as Awaited<ReturnType<typeof createTenant>>);
registerMock.mockResolvedValue({ id: "user_1", email: "ada@example.com" } as Awaited<ReturnType<typeof register>>);
getAllRolesMock.mockResolvedValue([{ id: "role_1" }] as Awaited<ReturnType<typeof getAllRoles>>);
createTenantUserMock.mockResolvedValue({ id: "tenant_user_1" } as Awaited<ReturnType<typeof createTenantUser>>);
setTenantTypesMock.mockResolvedValue(undefined);
eventsCreateMock.mockResolvedValue({} as Awaited<ReturnType<typeof EventsService.create>>);
const result = await createUserAndTenant({ request, email: "ada@example.com", password: "password", company: "Ada Co", firstName: "Ada", lastName: "Lovelace", locale: "en", slug: "ada-co" });
expect(result).toEqual({ user: { id: "user_1", email: "ada@example.com" }, tenant: { id: "tenant_1", name: "Ada Co", slug: "ada-co" } });
expect(queueTransactionalEmailMock).toHaveBeenCalledWith({
to: "ada@example.com",
alias: "welcome",
tenantId: "tenant_1",
userId: "user_1",
subject: "Welcome",
body: "<p>Welcome at https://example.com/login for Ada</p>",
data: {
action_url: "https://example.com/login",
name: "Ada",
},
});
expect(queueTransactionalEmailMock.mock.invocationCallOrder[0]).toBeGreaterThan(createTenantUserMock.mock.invocationCallOrder[0]);
expect(queueTransactionalEmailMock.mock.invocationCallOrder[0]).toBeGreaterThan(setTenantTypesMock.mock.invocationCallOrder[0]);
expect(welcomeParseMock).toHaveBeenCalledWith(
expect.objectContaining({
appConfiguration,
data: {
action_url: "https://example.com/login",
name: "Ada",
},
t: expect.any(Function),
locale: expect.any(String),
})
);
expect(sendEmailMock).not.toHaveBeenCalled();
});
it("throws for already-created registrations without enqueueing", async () => {
getRegistrationByEmailMock.mockResolvedValue({ id: "registration_1", createdTenantId: "tenant_1" } as Awaited<ReturnType<typeof getRegistrationByEmail>>);
await expect(createRegistrationForm({ request, email: "ada@example.com", ipAddress: "127.0.0.1", recreateToken: true })).rejects.toThrow("api.errors.userAlreadyRegistered");
expect(updateRegistrationMock).not.toHaveBeenCalled();
expect(createRegistrationMock).not.toHaveBeenCalled();
expect(queueTransactionalEmailMock).not.toHaveBeenCalled();
expect(sendEmailMock).not.toHaveBeenCalled();
});
});