import { KnownErrors } from "@hexclave/shared"; import { DEFAULT_EMAIL_THEME_ID, DEFAULT_TEMPLATE_IDS } from "@hexclave/shared/dist/helpers/emails"; import { wait } from "@hexclave/shared/dist/utils/promises"; import { it } from "../helpers"; import { withPortPrefix } from "../helpers/ports"; import { createApp } from "./js-helpers"; async function setupEmailServer(adminApp: any) { const project = await adminApp.getProject(); await project.updateConfig({ emails: { server: { isShared: false, host: "localhost", port: Number(withPortPrefix("29")), username: "test", password: "test", senderEmail: "test@example.com", senderName: "Test User", }, }, }); } it("should successfully send email with HTML content", async ({ expect }) => { const { adminApp, serverApp } = await createApp(); await setupEmailServer(adminApp); const user = await serverApp.createUser({ primaryEmail: "test@example.com", primaryEmailVerified: true, }); await expect(serverApp.sendEmail({ userIds: [user.id], html: "

Test Email

This is a test email with HTML content.

", subject: "Test Subject", })).resolves.not.toThrow(); }); it("should successfully send email with template", async ({ expect }) => { const { adminApp, serverApp } = await createApp(); await setupEmailServer(adminApp); const user = await serverApp.createUser({ primaryEmail: "test@example.com", primaryEmailVerified: true, }); await expect(serverApp.sendEmail({ userIds: [user.id], templateId: DEFAULT_TEMPLATE_IDS.sign_in_invitation, variables: { teamDisplayName: "Test Team", signInInvitationLink: "https://example.com", }, subject: "Welcome!", })).resolves.not.toThrow(); }); it("should successfully send email to multiple users", async ({ expect }) => { const { adminApp, serverApp } = await createApp(); await setupEmailServer(adminApp); const user1 = await serverApp.createUser({ primaryEmail: "test1@example.com", primaryEmailVerified: true, }); const user2 = await serverApp.createUser({ primaryEmail: "test2@example.com", primaryEmailVerified: true, }); await expect(serverApp.sendEmail({ userIds: [user1.id, user2.id], html: "

Bulk email test

", subject: "Bulk Email Test", })).resolves.not.toThrow(); }); it("should send email with theme customization", async ({ expect }) => { const { adminApp, serverApp } = await createApp(); await setupEmailServer(adminApp); const user = await serverApp.createUser({ primaryEmail: "test@example.com", primaryEmailVerified: true, }); await expect(serverApp.sendEmail({ userIds: [user.id], html: "

Themed email test

", subject: "Themed Email", themeId: DEFAULT_EMAIL_THEME_ID, })).resolves.not.toThrow(); }); it("should send email with notification category", async ({ expect }) => { const { adminApp, serverApp } = await createApp(); await setupEmailServer(adminApp); const user = await serverApp.createUser({ primaryEmail: "test@example.com", primaryEmailVerified: true, }); await expect(serverApp.sendEmail({ userIds: [user.id], html: "

Notification email test

", subject: "Notification Email", notificationCategoryName: "Transactional", })).resolves.not.toThrow(); }); it("should throw RequiresCustomEmailServer error when email server is not configured", async ({ expect }) => { const { serverApp } = await createApp(); const user = await serverApp.createUser({ primaryEmail: "test@example.com", primaryEmailVerified: true, }); await expect(serverApp.sendEmail({ userIds: [user.id], html: "

This should fail

", subject: "Test Email", })).rejects.toThrow(KnownErrors.RequiresCustomEmailServer); }); it("should handle non-existent user IDs", async ({ expect }) => { const { adminApp, serverApp } = await createApp(); await setupEmailServer(adminApp); // Use a properly formatted UUID that doesn't exist await expect(serverApp.sendEmail({ userIds: ["123e4567-e89b-12d3-a456-426614174000"], html: "

Non-existent user test

", subject: "Test Email", })).rejects.toThrow(KnownErrors.UserIdDoesNotExist); }); it("should handle missing required email content", async ({ expect }) => { const { adminApp, serverApp } = await createApp(); await setupEmailServer(adminApp); const user = await serverApp.createUser({ primaryEmail: "test@example.com", primaryEmailVerified: true, }); await expect(serverApp.sendEmail({ userIds: [user.id], subject: "Test Email", } as any)).rejects.toThrow(KnownErrors.SchemaError); }); it("should handle html and templateId at the same time", async ({ expect }) => { const { adminApp, serverApp } = await createApp(); await setupEmailServer(adminApp); const user = await serverApp.createUser({ primaryEmail: "test@example.com", primaryEmailVerified: true, }); await expect(serverApp.sendEmail({ userIds: [user.id], html: "

Test Email

", templateId: DEFAULT_TEMPLATE_IDS.sign_in_invitation, subject: "Test Email", } as any)).rejects.toThrow(KnownErrors.SchemaError); }); it("should provide delivery statistics", async ({ expect }) => { const { adminApp, serverApp } = await createApp(); await setupEmailServer(adminApp); const user = await serverApp.createUser({ primaryEmail: "stats@example.com", primaryEmailVerified: true, }); // Give Bulldozer's pg_cron tick time to materialise the billing team's // `emails_per_month` quota from its freshly-granted free plan. Without // this wait the first email gets quota-blocked into a permanent // server-error terminal and `stats.hour.sent` never reaches 1. await wait(2000); await serverApp.sendEmail({ userIds: [user.id], html: "

Stats

", subject: "Stats", }); let info; for (let i = 0; ; i++) { info = await serverApp.getEmailDeliveryStats(); if (info.stats.hour.sent >= 1) break; if (i >= 50) { throw new Error(`Timed out waiting for email delivery stats to reflect sent email: ${JSON.stringify(info)}`); } await wait(500); } expect(info).toMatchInlineSnapshot(` { "capacity": { "boost_expires_at": null, "boost_multiplier": 1, "is_boost_active": false, "penalty_factor": 1, "rate_per_second": 27.777779320987655, }, "stats": { "day": { "bounced": 0, "marked_as_spam": 0, "sent": 1, }, "hour": { "bounced": 0, "marked_as_spam": 0, "sent": 1, }, "month": { "bounced": 0, "marked_as_spam": 0, "sent": 1, }, "week": { "bounced": 0, "marked_as_spam": 0, "sent": 1, }, }, } `); }); it("should send test email with custom SMTP configuration", async ({ expect }) => { const { adminApp } = await createApp(); // First configure the email server await setupEmailServer(adminApp); // Get the project to access the email config const project = await adminApp.getProject(); const config = await project.getConfig(); // Verify config is not shared expect(config.emails.server.isShared).toBe(false); // Give Bulldozer's pg_cron tick time to materialise the billing team's // `emails_per_month` quota from its freshly-granted free plan; otherwise // the `tryDecreaseQuantity` inside the route rejects with // ItemQuantityInsufficientAmount before SMTP is ever dialled. await wait(2000); // Send a test email const result = await adminApp.sendTestEmail({ recipientEmail: "test-recipient@example.com", emailConfig: { host: config.emails.server.host!, port: config.emails.server.port!, username: config.emails.server.username!, password: config.emails.server.password!, senderEmail: config.emails.server.senderEmail!, senderName: config.emails.server.senderName!, } }); expect(result.status).toBe('ok'); }); it("should fail to send test email with shared server configuration", async ({ expect }) => { const { adminApp } = await createApp(); // Don't configure custom email server, so it defaults to shared const project = await adminApp.getProject(); const config = await project.getConfig(); // Verify config is shared expect(config.emails.server.isShared).toBe(true); // Attempting to send test email with shared config should fail in the UI // (This test documents the expected behavior in the dashboard UI) });