194 lines
5.8 KiB
TypeScript
194 lines
5.8 KiB
TypeScript
import type { EmailClient } from "../types.js";
|
|
import { beforeEach, describe, expect, it, mock } from "bun:test";
|
|
import {
|
|
buildOrgInviteEmailHtml,
|
|
buildOrgInviteEmailText,
|
|
sendOrgInviteEmail,
|
|
} from "./org-invite.js";
|
|
|
|
describe("buildOrgInviteEmailHtml", () => {
|
|
const params = {
|
|
email: "user@example.com",
|
|
orgName: "Acme Corp",
|
|
inviterName: "John Doe",
|
|
role: "admin" as const,
|
|
inviteUrl: "https://example.com/invite/accept?token=invite123",
|
|
expiresIn: "7 days",
|
|
};
|
|
|
|
it("should include the invite URL", () => {
|
|
const html = buildOrgInviteEmailHtml(params);
|
|
expect(html).toContain(
|
|
'href="https://example.com/invite/accept?token=invite123"',
|
|
);
|
|
});
|
|
|
|
it("should include the organization name", () => {
|
|
const html = buildOrgInviteEmailHtml(params);
|
|
expect(html).toContain("Acme Corp");
|
|
});
|
|
|
|
it("should include the inviter name", () => {
|
|
const html = buildOrgInviteEmailHtml(params);
|
|
expect(html).toContain("John Doe");
|
|
});
|
|
|
|
it("should include the role with correct article", () => {
|
|
const html = buildOrgInviteEmailHtml(params);
|
|
expect(html).toContain("an <strong>Admin</strong>");
|
|
});
|
|
|
|
it("should include the expiry time", () => {
|
|
const html = buildOrgInviteEmailHtml(params);
|
|
expect(html).toContain("This invitation expires in 7 days.");
|
|
});
|
|
|
|
it("should include the recipient email", () => {
|
|
const html = buildOrgInviteEmailHtml(params);
|
|
expect(html).toContain("user@example.com");
|
|
});
|
|
|
|
it("should escape HTML in organization name", () => {
|
|
const paramsWithXss = {
|
|
...params,
|
|
orgName: '<script>alert("xss")</script>',
|
|
};
|
|
const html = buildOrgInviteEmailHtml(paramsWithXss);
|
|
expect(html).not.toContain("<script>");
|
|
expect(html).toContain("<script>");
|
|
});
|
|
|
|
it("should escape HTML in inviter name", () => {
|
|
const paramsWithXss = {
|
|
...params,
|
|
inviterName: '<img src="x" onerror="alert(1)">',
|
|
};
|
|
const html = buildOrgInviteEmailHtml(paramsWithXss);
|
|
expect(html).not.toContain("<img");
|
|
expect(html).toContain("<img");
|
|
});
|
|
|
|
it("should use 'a' article for member role", () => {
|
|
const memberParams = { ...params, role: "member" as const };
|
|
const html = buildOrgInviteEmailHtml(memberParams);
|
|
expect(html).toContain("a <strong>Member</strong>");
|
|
});
|
|
|
|
it("should use 'an' article for owner role", () => {
|
|
const ownerParams = { ...params, role: "owner" as const };
|
|
const html = buildOrgInviteEmailHtml(ownerParams);
|
|
expect(html).toContain("an <strong>Owner</strong>");
|
|
});
|
|
});
|
|
|
|
describe("buildOrgInviteEmailText", () => {
|
|
const params = {
|
|
email: "user@example.com",
|
|
orgName: "Acme Corp",
|
|
inviterName: "John Doe",
|
|
role: "admin" as const,
|
|
inviteUrl: "https://example.com/invite/accept?token=invite123",
|
|
expiresIn: "7 days",
|
|
};
|
|
|
|
it("should include the invite URL", () => {
|
|
const text = buildOrgInviteEmailText(params);
|
|
expect(text).toContain("https://example.com/invite/accept?token=invite123");
|
|
});
|
|
|
|
it("should include the organization name", () => {
|
|
const text = buildOrgInviteEmailText(params);
|
|
expect(text).toContain("Acme Corp");
|
|
});
|
|
|
|
it("should include the inviter name", () => {
|
|
const text = buildOrgInviteEmailText(params);
|
|
expect(text).toContain("John Doe");
|
|
});
|
|
|
|
it("should include the role with correct article", () => {
|
|
const text = buildOrgInviteEmailText(params);
|
|
expect(text).toContain("an Admin");
|
|
});
|
|
|
|
it("should include the expiry time", () => {
|
|
const text = buildOrgInviteEmailText(params);
|
|
expect(text).toContain("This invitation expires in 7 days.");
|
|
});
|
|
});
|
|
|
|
describe("sendOrgInviteEmail", () => {
|
|
const createMockClient = () => {
|
|
const sendEmailMock = mock(() =>
|
|
Promise.resolve({ messageId: "test-message-id" }),
|
|
);
|
|
return {
|
|
sendEmail: sendEmailMock,
|
|
} as EmailClient & { sendEmail: ReturnType<typeof mock> };
|
|
};
|
|
|
|
let mockClient: ReturnType<typeof createMockClient>;
|
|
|
|
beforeEach(() => {
|
|
mockClient = createMockClient();
|
|
});
|
|
|
|
it("should send org invite email with correct details", async () => {
|
|
const result = await sendOrgInviteEmail({
|
|
client: mockClient,
|
|
fromAddress: "noreply@example.com",
|
|
baseUrl: "https://app.example.com",
|
|
email: "newuser@example.com",
|
|
token: "invite-token-abc",
|
|
orgName: "Acme Corp",
|
|
inviterName: "Jane Doe",
|
|
role: "admin",
|
|
expiryDays: 7,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(mockClient.sendEmail).toHaveBeenCalledTimes(1);
|
|
|
|
const call = mockClient.sendEmail.mock.calls[0];
|
|
const params = call?.[0] as {
|
|
from: string;
|
|
to: string;
|
|
subject: string;
|
|
htmlBody: string;
|
|
textBody: string;
|
|
};
|
|
|
|
expect(params.to).toBe("newuser@example.com");
|
|
expect(params.subject).toBe("You've been invited to join Acme Corp");
|
|
expect(params.htmlBody).toContain(
|
|
"https://app.example.com/invite/accept?token=invite-token-abc",
|
|
);
|
|
expect(params.htmlBody).toContain("Acme Corp");
|
|
expect(params.htmlBody).toContain("Jane Doe");
|
|
expect(params.htmlBody).toContain("Admin");
|
|
expect(params.htmlBody).toContain("7 days");
|
|
expect(params.textBody).toContain("Acme Corp");
|
|
expect(params.textBody).toContain("Jane Doe");
|
|
expect(params.textBody).toContain("an Admin");
|
|
});
|
|
|
|
it("should use correct article for member role", async () => {
|
|
await sendOrgInviteEmail({
|
|
client: mockClient,
|
|
fromAddress: "noreply@example.com",
|
|
baseUrl: "https://app.example.com",
|
|
email: "newuser@example.com",
|
|
token: "invite-token-abc",
|
|
orgName: "Acme Corp",
|
|
inviterName: "Jane Doe",
|
|
role: "member",
|
|
expiryDays: 7,
|
|
});
|
|
|
|
const call = mockClient.sendEmail.mock.calls[0];
|
|
const params = call?.[0] as { textBody: string };
|
|
|
|
expect(params.textBody).toContain("a Member");
|
|
});
|
|
});
|