mirror of
https://github.com/open-reception/appointment-booking-software.git
synced 2026-09-23 17:34:51 +02:00
* Make descriptions multilingual * Format and test fixes * Update src/lib/server/db/tenant-schema.ts * Fix tenant-switcher to update instantly * Fix sql migrations for description * Add languages to tenant table * Fix tenant schema * Use objects for translated fields * Fix format * Fix lint * Allow longer descriptions for channels * Field Localizations are based on tenant locales not supportedLocales * Fix agent image list preview * Improve language tab keyboard navigation * Fix falsy redirect after store init --------- Co-authored-by: Karl Ludwig Weise <office@ludwigweise.de> Co-authored-by: Karl Ludwig Weise <ludwig@ludwigweise.de>
551 lines
17 KiB
TypeScript
551 lines
17 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
|
|
// Mock environment variables
|
|
vi.mock("$env/dynamic/private", () => ({
|
|
env: {
|
|
SMTP_HOST: "smtp.test.com",
|
|
SMTP_PORT: "587",
|
|
SMTP_SECURE: "false",
|
|
SMTP_USER: "test@example.com",
|
|
SMTP_PASS: "test-password",
|
|
SMTP_FROM_NAME: "Test App",
|
|
SMTP_FROM_EMAIL: "noreply@test.com",
|
|
},
|
|
}));
|
|
|
|
// Mock nodemailer
|
|
const mockSendMail = vi.fn();
|
|
const mockVerify = vi.fn();
|
|
const mockTransporter = {
|
|
sendMail: mockSendMail,
|
|
verify: mockVerify,
|
|
};
|
|
|
|
vi.mock("nodemailer", () => ({
|
|
default: {
|
|
createTransport: vi.fn(() => mockTransporter),
|
|
},
|
|
}));
|
|
|
|
// Mock fs/promises
|
|
vi.mock("fs/promises", () => ({
|
|
readFile: vi.fn(),
|
|
}));
|
|
|
|
// Import modules after mocking
|
|
import { sendEmail, testEmailConnection } from "../mailer";
|
|
import { EmailTemplateEngine } from "../template-engine";
|
|
import { sendUserCreatedEmail, sendTemplatedEmail, sendConfirmationEmail } from "../email-service";
|
|
import { readFile } from "fs/promises";
|
|
import nodemailer from "nodemailer";
|
|
|
|
const mockReadFile = vi.mocked(readFile);
|
|
const mockCreateTransporter = vi.mocked(nodemailer.createTransport);
|
|
|
|
describe("Email System", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockSendMail.mockResolvedValue({ messageId: "test-message-id" });
|
|
mockVerify.mockResolvedValue(true);
|
|
});
|
|
|
|
describe("Mailer", () => {
|
|
it("should send email successfully", async () => {
|
|
const recipient = { email: "test@example.com", name: "Test User" };
|
|
const subject = "Test Subject";
|
|
const htmlContent = "<h1>Test HTML</h1>";
|
|
const textContent = "Test Text";
|
|
|
|
await sendEmail(recipient, subject, htmlContent, textContent);
|
|
|
|
expect(mockCreateTransporter).toHaveBeenCalledWith({
|
|
host: "smtp.test.com",
|
|
port: 587,
|
|
secure: false,
|
|
auth: {
|
|
user: "test@example.com",
|
|
pass: "test-password",
|
|
},
|
|
});
|
|
|
|
expect(mockSendMail).toHaveBeenCalledWith({
|
|
from: {
|
|
name: "Test App",
|
|
address: "noreply@test.com",
|
|
},
|
|
to: {
|
|
name: "Test User",
|
|
address: "test@example.com",
|
|
},
|
|
subject: "Test Subject",
|
|
html: "<h1>Test HTML</h1>",
|
|
text: "Test Text",
|
|
});
|
|
});
|
|
|
|
it("should test email connection", async () => {
|
|
const result = await testEmailConnection();
|
|
expect(result).toBe(true);
|
|
expect(mockVerify).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("Template Engine", () => {
|
|
let templateEngine: EmailTemplateEngine;
|
|
|
|
beforeEach(() => {
|
|
templateEngine = new EmailTemplateEngine("test/templates");
|
|
});
|
|
|
|
it("should render template with variables", async () => {
|
|
const mockHtml = "<h1>Hello {{recipient.name}}</h1>";
|
|
const mockText = "Hello {{recipient.name}}";
|
|
|
|
mockReadFile.mockResolvedValueOnce(mockHtml).mockResolvedValueOnce(mockText);
|
|
|
|
const mockTenant = {
|
|
id: "tenant-1",
|
|
shortName: "test",
|
|
longName: "Test Organization",
|
|
descriptions: { en: "" },
|
|
languages: ["en"],
|
|
logo: null,
|
|
databaseUrl: "postgresql://test",
|
|
setupState: "FIRST_CHANNEL_CREATED" as const,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
const result = await templateEngine.renderTemplate("user-created", {
|
|
recipient: { email: "test@example.com", name: "Test User" },
|
|
subject: "Test Subject",
|
|
language: "de",
|
|
tenant: mockTenant,
|
|
});
|
|
|
|
expect(result.html).toBe("<h1>Hello Test User</h1>");
|
|
expect(result.text).toBe("Hello Test User");
|
|
expect(result.subject).toBe("Test Subject");
|
|
});
|
|
|
|
it("should handle conditional blocks", async () => {
|
|
const mockHtml = "{{#if showButton}}<button>Click</button>{{/if}}";
|
|
const mockText = "{{#if showButton}}Button available{{/if}}";
|
|
|
|
mockReadFile.mockResolvedValueOnce(mockHtml).mockResolvedValueOnce(mockText);
|
|
|
|
const mockTenant = {
|
|
id: "tenant-1",
|
|
shortName: "test",
|
|
longName: "Test Organization",
|
|
descriptions: { en: "" },
|
|
languages: ["en"],
|
|
logo: null,
|
|
databaseUrl: "postgresql://test",
|
|
setupState: "FIRST_CHANNEL_CREATED" as const,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
const result = await templateEngine.renderTemplate("user-created", {
|
|
recipient: { email: "test@example.com" },
|
|
subject: "Test",
|
|
language: "de",
|
|
tenant: mockTenant,
|
|
showButton: true,
|
|
});
|
|
|
|
expect(result.html).toBe("<button>Click</button>");
|
|
expect(result.text).toBe("Button available");
|
|
});
|
|
|
|
it("should handle missing conditionals", async () => {
|
|
const mockHtml = "{{#if showButton}}<button>Click</button>{{/if}}";
|
|
const mockText = "{{#if showButton}}Button available{{/if}}";
|
|
|
|
mockReadFile.mockResolvedValueOnce(mockHtml).mockResolvedValueOnce(mockText);
|
|
|
|
const mockTenant = {
|
|
id: "tenant-1",
|
|
shortName: "test",
|
|
longName: "Test Organization",
|
|
descriptions: { en: "" },
|
|
languages: ["en"],
|
|
logo: null,
|
|
databaseUrl: "postgresql://test",
|
|
setupState: "FIRST_CHANNEL_CREATED" as const,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
const result = await templateEngine.renderTemplate("user-created", {
|
|
recipient: { email: "test@example.com" },
|
|
subject: "Test",
|
|
language: "de",
|
|
tenant: mockTenant,
|
|
showButton: false,
|
|
});
|
|
|
|
expect(result.html).toBe("");
|
|
expect(result.text).toBe("");
|
|
});
|
|
|
|
it("should handle English language", async () => {
|
|
const mockHtml = "<h1>Welcome {{recipient.name}}</h1>";
|
|
const mockText = "Welcome {{recipient.name}}";
|
|
|
|
mockReadFile.mockResolvedValueOnce(mockHtml).mockResolvedValueOnce(mockText);
|
|
|
|
const mockTenant = {
|
|
id: "tenant-1",
|
|
shortName: "test",
|
|
longName: "Test Organization",
|
|
descriptions: { en: "" },
|
|
languages: ["en"],
|
|
logo: null,
|
|
databaseUrl: "postgresql://test",
|
|
setupState: "FIRST_CHANNEL_CREATED" as const,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
const result = await templateEngine.renderTemplate("user-created", {
|
|
recipient: { email: "test@example.com", name: "John Doe" },
|
|
subject: "Welcome",
|
|
language: "en",
|
|
tenant: mockTenant,
|
|
});
|
|
|
|
expect(mockReadFile).toHaveBeenCalledWith(
|
|
expect.stringContaining("user-created.en.html"),
|
|
"utf-8",
|
|
);
|
|
expect(result.html).toBe("<h1>Welcome John Doe</h1>");
|
|
});
|
|
});
|
|
|
|
describe("Email Service", () => {
|
|
it("should send user created email in German", async () => {
|
|
const mockHtml = "<h1>Willkommen {{recipient.name}}</h1>";
|
|
const mockText = "Willkommen {{recipient.name}}";
|
|
|
|
mockReadFile.mockResolvedValueOnce(mockHtml).mockResolvedValueOnce(mockText);
|
|
|
|
// Create a mock staff user with German language
|
|
const staffUser = {
|
|
id: "test-id",
|
|
tenantId: "",
|
|
hashKey: "test-hash",
|
|
privateKeyShare: "test-share",
|
|
name: "Max Mustermann",
|
|
position: "Arzt",
|
|
email: "test@example.com",
|
|
language: "de",
|
|
role: "TENANT_ADMIN" as "GLOBAL_ADMIN" | "TENANT_ADMIN" | "STAFF",
|
|
createdAt: null,
|
|
updatedAt: null,
|
|
lastLoginAt: null,
|
|
isActive: null,
|
|
confirmed: null,
|
|
token: null,
|
|
tokenValidUntil: null,
|
|
passphraseHash: null,
|
|
recoveryPassphrase: null,
|
|
};
|
|
const mockTenant = {
|
|
id: "tenant-1",
|
|
shortName: "test",
|
|
longName: "Test Organization",
|
|
descriptions: { en: "" },
|
|
languages: ["en"],
|
|
logo: null,
|
|
databaseUrl: "postgresql://test",
|
|
setupState: "FIRST_CHANNEL_CREATED" as const,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
const loginUrl = "https://app.example.com/login";
|
|
|
|
await sendUserCreatedEmail(staffUser, mockTenant, loginUrl);
|
|
|
|
expect(mockReadFile).toHaveBeenCalledWith(
|
|
expect.stringContaining("user-created.de.html"),
|
|
"utf-8",
|
|
);
|
|
|
|
expect(mockSendMail).toHaveBeenCalledWith({
|
|
from: {
|
|
name: "Test App",
|
|
address: "noreply@test.com",
|
|
},
|
|
to: {
|
|
name: "Max Mustermann",
|
|
address: "test@example.com",
|
|
},
|
|
subject: "Willkommen bei Open Reception",
|
|
html: "<h1>Willkommen Max Mustermann</h1>",
|
|
text: "Willkommen Max Mustermann",
|
|
});
|
|
});
|
|
|
|
it("should send user created email in English", async () => {
|
|
const mockHtml = "<h1>Welcome {{recipient.name}}</h1>";
|
|
const mockText = "Welcome {{recipient.name}}";
|
|
|
|
mockReadFile.mockResolvedValueOnce(mockHtml).mockResolvedValueOnce(mockText);
|
|
|
|
// Create a mock staff user with English language
|
|
const staffUser = {
|
|
tenantId: "",
|
|
id: "test-id",
|
|
hashKey: "test-hash",
|
|
privateKeyShare: "test-share",
|
|
name: "John Doe",
|
|
position: "Doctor",
|
|
email: "test@example.com",
|
|
language: "en",
|
|
role: "TENANT_ADMIN" as "GLOBAL_ADMIN" | "TENANT_ADMIN" | "STAFF",
|
|
createdAt: null,
|
|
updatedAt: null,
|
|
lastLoginAt: null,
|
|
isActive: null,
|
|
confirmed: null,
|
|
token: null,
|
|
tokenValidUntil: null,
|
|
passphraseHash: null,
|
|
recoveryPassphrase: null,
|
|
};
|
|
|
|
const mockTenant = {
|
|
id: "tenant-1",
|
|
shortName: "test",
|
|
longName: "Test Organization",
|
|
descriptions: { en: "" },
|
|
languages: ["en"],
|
|
logo: null,
|
|
databaseUrl: "postgresql://test",
|
|
setupState: "FIRST_CHANNEL_CREATED" as const,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
const loginUrl = "https://app.example.com/login";
|
|
|
|
await sendUserCreatedEmail(staffUser, mockTenant, loginUrl);
|
|
|
|
expect(mockReadFile).toHaveBeenCalledWith(
|
|
expect.stringContaining("user-created.en.html"),
|
|
"utf-8",
|
|
);
|
|
|
|
expect(mockSendMail).toHaveBeenCalledWith({
|
|
from: {
|
|
name: "Test App",
|
|
address: "noreply@test.com",
|
|
},
|
|
to: {
|
|
name: "John Doe",
|
|
address: "test@example.com",
|
|
},
|
|
subject: "Welcome to Open Reception",
|
|
html: "<h1>Welcome John Doe</h1>",
|
|
text: "Welcome John Doe",
|
|
});
|
|
});
|
|
|
|
it("should handle template rendering errors", async () => {
|
|
mockReadFile.mockRejectedValue(new Error("Template not found"));
|
|
|
|
const recipient = { email: "test@example.com" };
|
|
const mockTenant = {
|
|
id: "tenant-1",
|
|
shortName: "test",
|
|
longName: "Test Organization",
|
|
descriptions: { en: "" },
|
|
languages: ["en"],
|
|
logo: null,
|
|
databaseUrl: "postgresql://test",
|
|
setupState: "FIRST_CHANNEL_CREATED" as const,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
await expect(
|
|
sendTemplatedEmail("user-created", recipient, "Test", "de", mockTenant, {}),
|
|
).rejects.toThrow("Template not found");
|
|
});
|
|
|
|
it("should handle SMTP errors", async () => {
|
|
const mockHtml = "<h1>Test</h1>";
|
|
const mockText = "Test";
|
|
|
|
mockReadFile.mockResolvedValueOnce(mockHtml).mockResolvedValueOnce(mockText);
|
|
|
|
mockSendMail.mockRejectedValueOnce(new Error("SMTP Error"));
|
|
|
|
const clientUser = {
|
|
id: "test-id",
|
|
hashKey: "test-hash",
|
|
publicKey: "test-key",
|
|
privateKeyShare: "test-share",
|
|
email: "test@example.com",
|
|
language: "de",
|
|
};
|
|
const mockTenant = {
|
|
id: "tenant-1",
|
|
shortName: "test",
|
|
longName: "Test Organization",
|
|
descriptions: { en: "" },
|
|
languages: ["en"],
|
|
logo: null,
|
|
databaseUrl: "postgresql://test",
|
|
setupState: "FIRST_CHANNEL_CREATED" as const,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
await expect(
|
|
sendUserCreatedEmail(clientUser, mockTenant, "https://example.com/login"),
|
|
).rejects.toThrow("Failed to send email to test@example.com");
|
|
});
|
|
|
|
it("should send confirmation email in German", async () => {
|
|
const mockHtml =
|
|
"<h1>Bestätigungscode: {{confirmationCode}}</h1><p>Gültig für {{expirationMinutes}} Minuten</p>";
|
|
const mockText =
|
|
"Bestätigungscode: {{confirmationCode}} - Gültig für {{expirationMinutes}} Minuten";
|
|
|
|
mockReadFile.mockResolvedValueOnce(mockHtml).mockResolvedValueOnce(mockText);
|
|
|
|
const staffUser = {
|
|
id: "test-id",
|
|
hashKey: "test-hash",
|
|
name: "Max Mustermann",
|
|
position: "Arzt",
|
|
email: "test@example.com",
|
|
language: "de",
|
|
};
|
|
const mockTenant = {
|
|
id: "tenant-1",
|
|
shortName: "test",
|
|
longName: "Test Organization",
|
|
descriptions: { en: "" },
|
|
languages: ["en"],
|
|
logo: null,
|
|
databaseUrl: "postgresql://test",
|
|
setupState: "FIRST_CHANNEL_CREATED" as const,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
const confirmationCode = "ABC123";
|
|
const expirationMinutes = 15;
|
|
|
|
await sendConfirmationEmail(staffUser, mockTenant, confirmationCode, expirationMinutes);
|
|
|
|
expect(mockReadFile).toHaveBeenCalledWith(
|
|
expect.stringContaining("confirmation.de.html"),
|
|
"utf-8",
|
|
);
|
|
|
|
expect(mockSendMail).toHaveBeenCalledWith({
|
|
from: {
|
|
name: "Test App",
|
|
address: "noreply@test.com",
|
|
},
|
|
to: {
|
|
name: "Max Mustermann",
|
|
address: "test@example.com",
|
|
},
|
|
subject: "Registrierung bestätigen",
|
|
html: "<h1>Bestätigungscode: ABC123</h1><p>Gültig für 15 Minuten</p>",
|
|
text: "Bestätigungscode: ABC123 - Gültig für 15 Minuten",
|
|
});
|
|
});
|
|
|
|
it("should send confirmation email in English", async () => {
|
|
const mockHtml =
|
|
"<h1>Confirmation code: {{confirmationCode}}</h1><p>Valid for {{expirationMinutes}} minutes</p>";
|
|
const mockText =
|
|
"Confirmation code: {{confirmationCode}} - Valid for {{expirationMinutes}} minutes";
|
|
|
|
mockReadFile.mockResolvedValueOnce(mockHtml).mockResolvedValueOnce(mockText);
|
|
|
|
const staffUser = {
|
|
id: "test-id",
|
|
hashKey: "test-hash",
|
|
name: "John Doe",
|
|
position: "Doctor",
|
|
email: "test@example.com",
|
|
language: "en",
|
|
};
|
|
const mockTenant = {
|
|
id: "tenant-1",
|
|
shortName: "test",
|
|
longName: "Test Organization",
|
|
descriptions: { en: "" },
|
|
languages: ["en"],
|
|
logo: null,
|
|
databaseUrl: "postgresql://test",
|
|
setupState: "FIRST_CHANNEL_CREATED" as const,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
const confirmationCode = "XYZ789";
|
|
const expirationMinutes = 10;
|
|
|
|
await sendConfirmationEmail(staffUser, mockTenant, confirmationCode, expirationMinutes);
|
|
|
|
expect(mockReadFile).toHaveBeenCalledWith(
|
|
expect.stringContaining("confirmation.en.html"),
|
|
"utf-8",
|
|
);
|
|
|
|
expect(mockSendMail).toHaveBeenCalledWith({
|
|
from: {
|
|
name: "Test App",
|
|
address: "noreply@test.com",
|
|
},
|
|
to: {
|
|
name: "John Doe",
|
|
address: "test@example.com",
|
|
},
|
|
subject: "Confirm Your Registration",
|
|
html: "<h1>Confirmation code: XYZ789</h1><p>Valid for 10 minutes</p>",
|
|
text: "Confirmation code: XYZ789 - Valid for 10 minutes",
|
|
});
|
|
});
|
|
|
|
it("should handle confirmation template rendering", async () => {
|
|
const mockHtml = "Code: {{confirmationCode}}";
|
|
const mockText = "Code: {{confirmationCode}}";
|
|
|
|
mockReadFile.mockResolvedValueOnce(mockHtml).mockResolvedValueOnce(mockText);
|
|
|
|
const templateEngine = new EmailTemplateEngine("test/templates");
|
|
const mockTenant = {
|
|
id: "tenant-1",
|
|
shortName: "test",
|
|
longName: "Test Organization",
|
|
descriptions: { en: "Tenant Description" },
|
|
languages: ["en"],
|
|
logo: null,
|
|
databaseUrl: "postgresql://test",
|
|
setupState: "FIRST_CHANNEL_CREATED" as const,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
const result = await templateEngine.renderTemplate("confirmation", {
|
|
recipient: { email: "test@example.com", name: "Test User" },
|
|
subject: "Test Confirmation",
|
|
language: "de",
|
|
tenant: mockTenant,
|
|
confirmationCode: "TEST123",
|
|
expirationMinutes: 15,
|
|
});
|
|
|
|
expect(result.html).toBe("Code: TEST123");
|
|
expect(result.text).toBe("Code: TEST123");
|
|
});
|
|
});
|
|
});
|