mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
Some checks failed
all-good: Did all the other checks pass? / all-good (push) Has been cancelled
Ensure Prisma migrations are in sync with the schema / check_prisma_migrations (22.x) (push) Has been cancelled
DB migration compat / Check if migrations changed (push) Has been cancelled
Docker Server Build and Push / Docker Build and Push Server (push) Has been cancelled
Docker Server Build and Run / docker (push) Has been cancelled
Runs E2E API Tests (Local Emulator) / E2E Tests (Local Emulator, Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (mock, 22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (prod, 22.x) (push) Has been cancelled
Runs E2E API Tests with custom port prefix / build (22.x) (push) Has been cancelled
Runs E2E Fallback Tests / E2E Fallback Tests (Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Lint & build / lint_and_build (24) (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
DB migration compat / Back-compat — Current branch migrations with ${{ needs.check-migrations-changed.outputs.base_branch }} branch code (push) Has been cancelled
DB migration compat / Forward-compat — Current branch code with ${{ needs.check-migrations-changed.outputs.base_branch }} branch migrations (push) Has been cancelled
DB migration compat / No migration changes (skipped) (push) Has been cancelled
289 lines
8.5 KiB
TypeScript
289 lines
8.5 KiB
TypeScript
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: "<h1>Test Email</h1><p>This is a test email with HTML content.</p>",
|
|
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: "<p>Bulk email test</p>",
|
|
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: "<p>Themed email test</p>",
|
|
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: "<p>Notification email test</p>",
|
|
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: "<p>This should fail</p>",
|
|
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: "<p>Non-existent user test</p>",
|
|
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: "<p>Test Email</p>",
|
|
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: "<p>Stats</p>",
|
|
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)
|
|
});
|