383 lines
20 KiB
TypeScript
383 lines
20 KiB
TypeScript
function assert(condition: unknown, message = "assertion failed"): void {
|
|
if (!condition) throw new Error(message);
|
|
}
|
|
|
|
function assertEqual<T>(actual: T, expected: T, message = "values should be equal"): void {
|
|
if (actual !== expected) throw new Error(`${message}: expected ${String(expected)}, got ${String(actual)}`);
|
|
}
|
|
|
|
function assertDeepEqual(actual: unknown, expected: unknown, message = "values should be deeply equal"): void {
|
|
const actualJson = JSON.stringify(actual);
|
|
const expectedJson = JSON.stringify(expected);
|
|
if (actualJson !== expectedJson) throw new Error(`${message}: expected ${expectedJson}, got ${actualJson}`);
|
|
}
|
|
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
import CredentialPanel from "../src/components/CredentialPanel";
|
|
import PasswordField from "../src/components/PasswordField";
|
|
import MessageDisplayPanel, { buildSafeMessageHtmlDocument } from "../src/components/MessageDisplayPanel";
|
|
import MailServerSettingsPanel, { MailServerFolderLookupResultView, mailImapSettingsPayload, normalizeMailImapFolderMappings, resolveMailServerSettingsActiveSection } from "../src/components/mail/MailServerSettingsPanel";
|
|
import EmailAddressInput from "../src/components/email/EmailAddressInput";
|
|
import { PlatformLanguageProvider } from "../src/i18n/LanguageContext";
|
|
import { dedupeAddresses } from "../src/utils/emailAddresses";
|
|
|
|
function noop() {}
|
|
|
|
const orderedAddresses = [
|
|
{ name: "Zulu", email: " ZULU@example.test " },
|
|
{ email: "alpha@example.test" },
|
|
{ name: "Alpha", email: "ALPHA@example.test" },
|
|
{ name: "Beta", email: "beta@example.test" }
|
|
];
|
|
assertDeepEqual(
|
|
dedupeAddresses(orderedAddresses).map((item) => item.name),
|
|
["Alpha", "Beta", "Zulu"],
|
|
"address suggestion consumers retain the alphabetical default"
|
|
);
|
|
assertDeepEqual(dedupeAddresses(orderedAddresses, { preserveOrder: true }), [
|
|
{ name: "Zulu", email: "zulu@example.test" },
|
|
{ name: "Alpha", email: "alpha@example.test" },
|
|
{ name: "Beta", email: "beta@example.test" }
|
|
], "authored recipient order is preserved while duplicates enrich the first occurrence");
|
|
assertEqual(orderedAddresses[0].email, " ZULU@example.test ", "deduplication does not mutate its input");
|
|
|
|
const allMailServerSections = ["smtp", "imap"] as const;
|
|
assertEqual(
|
|
resolveMailServerSettingsActiveSection("imap", "smtp", "smtp", allMailServerSections),
|
|
"imap",
|
|
"user-selected mail settings section is not reset to initialSection"
|
|
);
|
|
assertEqual(
|
|
resolveMailServerSettingsActiveSection("smtp", "imap", "smtp", allMailServerSections),
|
|
"imap",
|
|
"changed initialSection is applied when parent intentionally changes it"
|
|
);
|
|
assertEqual(
|
|
resolveMailServerSettingsActiveSection("imap", "smtp", "smtp", ["smtp"] as const),
|
|
"smtp",
|
|
"active mail settings section falls back when hidden by visibleSections"
|
|
);
|
|
assertEqual(
|
|
resolveMailServerSettingsActiveSection("smtp", "imap", "imap", ["smtp"] as const),
|
|
"smtp",
|
|
"hidden initialSection falls back to the first visible section"
|
|
);
|
|
|
|
const savedPassword = renderToStaticMarkup(
|
|
<PasswordField value="" saved savedPlaceholder="Saved password configured" onValueChange={noop} />
|
|
);
|
|
assert(savedPassword.includes('placeholder="Saved password configured"'), "saved password placeholder is rendered");
|
|
assert(!savedPassword.includes("password-field-action"), "saved empty password without generator does not show an action button");
|
|
|
|
const typedPassword = renderToStaticMarkup(
|
|
<PasswordField value="secret" saved savedPlaceholder="Saved password configured" onValueChange={noop} />
|
|
);
|
|
assert(typedPassword.includes('aria-label="i18n:govoplan-core.show_password.044b852f"'), "typed password shows reveal button");
|
|
assert(typedPassword.includes("password-field-action"), "typed password gets a reveal action");
|
|
|
|
const translatedAddressInput = renderToStaticMarkup(
|
|
<PlatformLanguageProvider>
|
|
<EmailAddressInput
|
|
value={[]}
|
|
onChange={noop}
|
|
addLabel="i18n:govoplan-core.add_address.a71075c4"
|
|
emptyText="i18n:govoplan-core.no_address_added_yet.809c4247"
|
|
suggestions={[{ name: "Ada Lovelace", email: "ada@example.local" }]}
|
|
/>
|
|
</PlatformLanguageProvider>
|
|
);
|
|
assert(translatedAddressInput.includes("No address added yet"), "email input empty text is translated");
|
|
assert(!translatedAddressInput.includes("i18n:govoplan-core.no_address_added_yet.809c4247"), "email input does not leak empty-text i18n key");
|
|
assert(translatedAddressInput.includes('aria-label="Type a name and email address, then press Enter"'), "email input aria label is translated");
|
|
|
|
const savedCredentialPanel = renderToStaticMarkup(
|
|
<CredentialPanel
|
|
values={{ username: "sender", password: "" }}
|
|
onChange={noop}
|
|
savedPassword
|
|
savedPasswordPlaceholder="Saved credential"
|
|
heading="Credentials"
|
|
/>
|
|
);
|
|
assert(savedCredentialPanel.includes("Credentials"), "credential panel renders heading");
|
|
assert(savedCredentialPanel.includes('value="sender"'), "credential panel renders username");
|
|
assert(savedCredentialPanel.includes('placeholder="Saved credential"'), "credential panel renders saved password placeholder");
|
|
assert(savedCredentialPanel.includes('aria-label="i18n:govoplan-core.generate_password.bd5bede8"'), "credential fields opt into the password generator");
|
|
assert(!savedCredentialPanel.includes('aria-label="i18n:govoplan-core.show_password.044b852f"'), "saved empty credential does not show a reveal action");
|
|
|
|
const settingsPanel = renderToStaticMarkup(
|
|
<MailServerSettingsPanel
|
|
smtp={{ host: "smtp.example.org", port: 587, username: "sender", password: "", security: "starttls", timeout_seconds: 30 }}
|
|
imap={{ host: "imap.example.org", port: 993, username: "sender", password: "", security: "tls", sent_folder: "auto", timeout_seconds: 30 }}
|
|
onSmtpChange={noop}
|
|
onImapChange={noop}
|
|
smtpPasswordSaved
|
|
smtpSavedPasswordPlaceholder="Saved SMTP password"
|
|
imapPasswordSaved
|
|
imapSavedPasswordPlaceholder="Saved IMAP password"
|
|
onTestSmtp={noop}
|
|
onTestImap={noop}
|
|
/>
|
|
);
|
|
assert(settingsPanel.includes("i18n:govoplan-core.test_smtp.e5697981"), "SMTP test action is rendered");
|
|
assert(settingsPanel.includes("i18n:govoplan-core.server.cb0cb170"), "server field is rendered");
|
|
assert(settingsPanel.includes("i18n:govoplan-core.username.84c29015"), "username field is rendered");
|
|
assert(settingsPanel.includes("i18n:govoplan-core.password.8be3c943"), "password field is rendered");
|
|
assert(settingsPanel.includes("i18n:govoplan-core.timeout_seconds.0bfc6553"), "timeout field is rendered with SMTP server settings");
|
|
assert(!settingsPanel.includes("i18n:govoplan-core.advanced.4d064726"), "advanced section tab is not rendered");
|
|
assert(!settingsPanel.includes("Enable IMAP"), "legacy IMAP enable toggle is not rendered");
|
|
assert(settingsPanel.includes('placeholder="Saved SMTP password"'), "SMTP saved credential placeholder is rendered");
|
|
|
|
const disabledSettingsPanel = renderToStaticMarkup(
|
|
<MailServerSettingsPanel
|
|
smtp={{ host: "", port: 587, security: "starttls", timeout_seconds: 30 }}
|
|
imap={{ host: "", port: 993, security: "tls", sent_folder: "auto", timeout_seconds: 30 }}
|
|
onSmtpChange={noop}
|
|
onImapChange={noop}
|
|
onTestSmtp={noop}
|
|
smtpActionDisabled
|
|
smtpActionDisabledReason="Enter an SMTP server hostname before testing the connection."
|
|
/>
|
|
);
|
|
assert(disabledSettingsPanel.includes("disabled-action-tooltip"), "disabled SMTP test reason uses the shared focusable tooltip");
|
|
assert(disabledSettingsPanel.includes('tabindex="0"'), "disabled SMTP test reason is keyboard focusable");
|
|
|
|
const imapSettingsPanel = renderToStaticMarkup(
|
|
<MailServerSettingsPanel
|
|
initialSection="imap"
|
|
smtp={{ host: "smtp.example.org", port: 587, username: "sender", password: "", security: "starttls", timeout_seconds: 30 }}
|
|
imap={{ host: "imap.example.org", port: 993, username: "sender", password: "", security: "tls", sent_folder: "auto", timeout_seconds: 30 }}
|
|
onSmtpChange={noop}
|
|
onImapChange={noop}
|
|
imapPasswordSaved
|
|
imapSavedPasswordPlaceholder="Saved IMAP password"
|
|
onTestImap={noop}
|
|
/>
|
|
);
|
|
assert(imapSettingsPanel.includes("i18n:govoplan-core.test_imap.ef1bd79c"), "IMAP test action is rendered");
|
|
assert(imapSettingsPanel.includes("i18n:govoplan-core.server.cb0cb170"), "IMAP server field is rendered");
|
|
assert(imapSettingsPanel.includes("i18n:govoplan-core.username.84c29015"), "IMAP username field is rendered");
|
|
assert(imapSettingsPanel.includes("i18n:govoplan-core.password.8be3c943"), "IMAP password field is rendered");
|
|
assert(imapSettingsPanel.includes("i18n:govoplan-core.timeout_seconds.0bfc6553"), "IMAP timeout field is rendered with IMAP server settings");
|
|
assert(!imapSettingsPanel.includes("Enable IMAP"), "legacy IMAP enable toggle is not rendered in IMAP tab");
|
|
assert(!imapSettingsPanel.includes("i18n:govoplan-core.folders.c603ab65"), "folder lookup action is not rendered in the IMAP tab");
|
|
assert(imapSettingsPanel.includes('placeholder="Saved IMAP password"'), "IMAP saved credential placeholder is rendered");
|
|
assert(!imapSettingsPanel.includes("Folders loaded"), "folder lookup result is not rendered in the IMAP tab");
|
|
assert(!imapSettingsPanel.includes("Default sent folder"), "default sent folder field is not rendered");
|
|
|
|
assertDeepEqual(
|
|
normalizeMailImapFolderMappings({ inbox: " INBOX ", sent: "Sent Items", junk: " " }),
|
|
{ inbox: "INBOX", sent: "Sent Items", drafts: null, trash: null, archive: null, junk: null },
|
|
"standard IMAP mappings are normalized without inventing folder names"
|
|
);
|
|
assertDeepEqual(
|
|
mailImapSettingsPayload(
|
|
{
|
|
host: "imap.example.org",
|
|
port: 993,
|
|
security: "tls",
|
|
sent_folder: "Legacy Sent",
|
|
folder_mappings: { sent: "Mapped Sent", archive: "Archive" },
|
|
timeout_seconds: 30
|
|
},
|
|
{ fallbackSecurity: "tls" }
|
|
),
|
|
{
|
|
host: "imap.example.org",
|
|
port: 993,
|
|
security: "tls",
|
|
sent_folder: "Mapped Sent",
|
|
folder_mappings: { inbox: null, sent: "Mapped Sent", drafts: null, trash: null, archive: "Archive", junk: null },
|
|
timeout_seconds: 30
|
|
},
|
|
"the typed Sent mapping remains synchronized with the legacy append field"
|
|
);
|
|
|
|
const mappedImapSettingsPanel = renderToStaticMarkup(
|
|
<MailServerSettingsPanel
|
|
initialSection="imap"
|
|
visibleSections={["imap"]}
|
|
mode="server"
|
|
smtp={{}}
|
|
imap={{
|
|
host: "imap.example.org",
|
|
port: 993,
|
|
security: "tls",
|
|
sent_folder: "Sent",
|
|
folder_mappings: { inbox: "INBOX", sent: "Sent" },
|
|
timeout_seconds: 30
|
|
}}
|
|
onSmtpChange={noop}
|
|
onImapChange={noop}
|
|
onLookupImapFolders={noop}
|
|
imapFolderLookupResult={{
|
|
ok: true,
|
|
protocol: "imap",
|
|
message: "Folders loaded",
|
|
detected_sent_folder: "Sent",
|
|
detected_folder_mappings: { inbox: "INBOX", sent: "Sent", drafts: "Drafts" },
|
|
folders: [{ name: "INBOX" }, { name: "Sent" }, { name: "Drafts" }]
|
|
}}
|
|
/>
|
|
);
|
|
assert(mappedImapSettingsPanel.includes("i18n:govoplan-core.standard_folder_mappings"), "profile IMAP settings render the shared standard-folder editor");
|
|
assert(mappedImapSettingsPanel.includes("i18n:govoplan-core.detect_folders"), "profile IMAP settings expose folder discovery");
|
|
assert(mappedImapSettingsPanel.includes("i18n:govoplan-core.use_detected_folder_mappings"), "detected standard mappings can be applied together");
|
|
assert(mappedImapSettingsPanel.includes('<option value="Drafts"></option>'), "discovered folder names populate mapping suggestions");
|
|
|
|
const folderLookupError = renderToStaticMarkup(
|
|
<MailServerFolderLookupResultView result={{ ok: false, protocol: "imap", message: "IMAP lookup timed out", folders: [] }} />
|
|
);
|
|
assert(folderLookupError.includes("compact-alert"), "folder lookup errors use the compact shared alert style");
|
|
assert(folderLookupError.includes("warning"), "folder lookup errors are warnings, not page-level danger alerts");
|
|
assert(folderLookupError.includes("IMAP lookup timed out"), "folder lookup error message is rendered");
|
|
|
|
const floatingFolderLookupError = renderToStaticMarkup(
|
|
<MailServerFolderLookupResultView result={{ ok: false, protocol: "imap", message: "IMAP lookup timed out", folders: [] }} floatingFailures />
|
|
);
|
|
assert(floatingFolderLookupError.includes("alert-floating"), "folder lookup errors can be rendered in the floating alert stack");
|
|
|
|
const folderLookupSuccess = renderToStaticMarkup(
|
|
<MailServerFolderLookupResultView
|
|
result={{
|
|
ok: true,
|
|
protocol: "imap",
|
|
message: "Folders loaded",
|
|
detected_sent_folder: "Sent",
|
|
folders: [{ name: "INBOX", flags: [] }, { name: "Sent", flags: ["\\Sent"] }]
|
|
}}
|
|
onUseDetected={noop}
|
|
/>
|
|
);
|
|
assert(folderLookupSuccess.includes("compact-alert"), "folder lookup success uses the compact shared alert style");
|
|
assert(folderLookupSuccess.includes("Folders loaded"), "folder lookup success message is rendered");
|
|
|
|
const smtpServerOnlyPanel = renderToStaticMarkup(
|
|
<MailServerSettingsPanel
|
|
visibleSections={["smtp"]}
|
|
mode="server"
|
|
smtp={{ host: "smtp.example.org", port: 587, username: "sender", password: "", security: "starttls", timeout_seconds: 30 }}
|
|
imap={{ host: "imap.example.org", port: 993, username: "reader", password: "", security: "tls", sent_folder: "auto", timeout_seconds: 30 }}
|
|
onSmtpChange={noop}
|
|
onImapChange={noop}
|
|
/>
|
|
);
|
|
assert(smtpServerOnlyPanel.includes("i18n:govoplan-core.server.cb0cb170"), "focused server panel renders server fields");
|
|
assert(!smtpServerOnlyPanel.includes("i18n:govoplan-core.username.84c29015"), "focused server panel hides credential fields");
|
|
assert(!smtpServerOnlyPanel.includes("i18n:govoplan-core.mail_server_settings_sections.40a163b7"), "single focused section hides section switcher");
|
|
assert(!smtpServerOnlyPanel.includes("imap.example.org"), "focused SMTP server panel does not render IMAP fields");
|
|
|
|
const imapCredentialsOnlyPanel = renderToStaticMarkup(
|
|
<MailServerSettingsPanel
|
|
initialSection="imap"
|
|
visibleSections={["imap"]}
|
|
mode="credentials"
|
|
smtp={{ host: "smtp.example.org", port: 587, username: "sender", password: "", security: "starttls", timeout_seconds: 30 }}
|
|
imap={{ host: "imap.example.org", port: 993, username: "reader", password: "", security: "tls", sent_folder: "auto", timeout_seconds: 30 }}
|
|
onSmtpChange={noop}
|
|
onImapChange={noop}
|
|
imapPasswordSaved
|
|
imapSavedPasswordPlaceholder="Saved IMAP password"
|
|
/>
|
|
);
|
|
assert(imapCredentialsOnlyPanel.includes("i18n:govoplan-core.username.84c29015"), "focused credentials panel renders credential fields");
|
|
assert(!imapCredentialsOnlyPanel.includes("i18n:govoplan-core.server.cb0cb170"), "focused credentials panel hides server fields");
|
|
assert(imapCredentialsOnlyPanel.includes('placeholder="Saved IMAP password"'), "focused credentials panel preserves saved password UX");
|
|
assert(!imapCredentialsOnlyPanel.includes("smtp.example.org"), "focused IMAP credentials panel does not render SMTP fields");
|
|
|
|
const defaultPortSettingsPanel = renderToStaticMarkup(
|
|
<MailServerSettingsPanel
|
|
smtp={{ host: "smtp.example.org", security: "starttls" }}
|
|
imap={{ host: "imap.example.org", security: "tls" }}
|
|
onSmtpChange={noop}
|
|
onImapChange={noop}
|
|
/>
|
|
);
|
|
const defaultImapPortSettingsPanel = renderToStaticMarkup(
|
|
<MailServerSettingsPanel
|
|
initialSection="imap"
|
|
smtp={{ host: "smtp.example.org", security: "starttls" }}
|
|
imap={{ host: "imap.example.org", security: "tls" }}
|
|
onSmtpChange={noop}
|
|
onImapChange={noop}
|
|
/>
|
|
);
|
|
assert(defaultPortSettingsPanel.includes('value="587"'), "SMTP starttls default port is displayed");
|
|
assert(defaultImapPortSettingsPanel.includes('value="993"'), "IMAP TLS default port is displayed");
|
|
|
|
const messageDisplay = renderToStaticMarkup(
|
|
<MessageDisplayPanel
|
|
title="Quarterly update"
|
|
fields={[
|
|
{ label: "From", value: "Sender <sender@example.org>" },
|
|
{ label: "Cc", value: null },
|
|
{ label: "Bcc", value: "Audit <audit@example.org>" }
|
|
]}
|
|
bodyText="Plain body"
|
|
bodyHtml="<p><strong>HTML body</strong></p>"
|
|
preferredBodyMode="html"
|
|
attachments={[
|
|
{ filename: "agenda.pdf", contentType: "application/pdf", sizeBytes: 2048 },
|
|
{
|
|
filename: "secret.pdf",
|
|
contentType: "application/pdf",
|
|
archiveGroup: "archive-1",
|
|
archiveLabel: "recipient-files.zip",
|
|
protected: true,
|
|
protectionNote: 'Password-protected ZIP using local field "zip_password". Encryption: AES.'
|
|
}
|
|
]}
|
|
/>
|
|
);
|
|
assert(messageDisplay.includes("Quarterly update"), "message subject is rendered");
|
|
assert(messageDisplay.includes("Sender <sender@example.org>"), "sender field is rendered");
|
|
assert(!messageDisplay.includes("<dt>Cc</dt>"), "empty optional CC field is hidden");
|
|
assert(messageDisplay.includes("<dt>Bcc</dt>"), "present BCC field is rendered");
|
|
assert(messageDisplay.includes("i18n:govoplan-core.html.9f738ce8"), "HTML body switch is rendered");
|
|
assert(messageDisplay.includes("i18n:govoplan-core.text.c3328c39"), "text body switch is rendered");
|
|
assert(messageDisplay.includes("agenda.pdf"), "direct attachment is rendered");
|
|
assert(messageDisplay.includes("recipient-files.zip"), "ZIP archive attachment group is rendered");
|
|
assert(messageDisplay.includes("i18n:govoplan-core.password_protected.09d9e174"), "ZIP protection badge is rendered");
|
|
assert(messageDisplay.includes("zip_password"), "ZIP protection note is rendered");
|
|
|
|
const safeMessageDocument = buildSafeMessageHtmlDocument(`
|
|
<base href="https://tracking.example/">
|
|
<meta http-equiv="refresh" content="0;url=https://tracking.example/refresh">
|
|
<link rel="stylesheet" href="https://tracking.example/mail.css">
|
|
<style>
|
|
@import "https://tracking.example/import.css";
|
|
.remote { background-image: url(https://tracking.example/background.png); }
|
|
.embedded { background-image: url('cid:background@example.org'); }
|
|
</style>
|
|
<a href="https://tracking.example/click" ping="https://tracking.example/ping">Remote link</a>
|
|
<a href="#local-section">Local link</a>
|
|
<img src="https://tracking.example/pixel.gif" srcset="https://tracking.example/retina.gif 2x" onerror="fetch('https://tracking.example/error')">
|
|
<img src="cid:logo@example.org">
|
|
<img src="data:image/png;base64,iVBORw0KGgo=">
|
|
<img src="data:image/svg+xml;base64,PHN2Zz48L3N2Zz4=">
|
|
<script>fetch("https://tracking.example/script")</script>
|
|
`);
|
|
assert(safeMessageDocument.startsWith("<!doctype html><html><head>"), "message HTML is wrapped in an isolated document");
|
|
assert(safeMessageDocument.includes("default-src 'none'"), "message iframe CSP denies resources by default");
|
|
assert(safeMessageDocument.includes("img-src data: cid:"), "message iframe CSP permits only embedded image sources");
|
|
assert(safeMessageDocument.includes("connect-src 'none'"), "message iframe CSP blocks active network connections");
|
|
assert(safeMessageDocument.includes("form-action 'none'"), "message iframe CSP blocks form submissions");
|
|
assert(safeMessageDocument.includes('src="cid:logo@example.org"'), "CID image references remain available");
|
|
assert(safeMessageDocument.includes('src="data:image/png;base64,iVBORw0KGgo="'), "conservative raster data images remain available");
|
|
assert(safeMessageDocument.includes('href="#local-section"'), "same-document fragment links remain available");
|
|
assert(!safeMessageDocument.includes("tracking.example"), "remote message URLs are neutralized before preview");
|
|
assert(!safeMessageDocument.includes("data:image/svg+xml"), "active SVG data resources are rejected");
|
|
assert(!safeMessageDocument.includes("onerror="), "event-handler attributes are removed");
|
|
assert(!safeMessageDocument.includes("<script"), "script elements are removed");
|
|
|
|
const privateMessageDisplay = renderToStaticMarkup(
|
|
<MessageDisplayPanel
|
|
title="Untrusted message"
|
|
bodyHtml='<img src="https://tracking.example/pixel.gif"><img src="cid:logo@example.org">'
|
|
preferredBodyMode="html"
|
|
/>
|
|
);
|
|
assert(privateMessageDisplay.includes('sandbox=""'), "message HTML remains inside a scriptless iframe sandbox");
|
|
assert(privateMessageDisplay.includes('referrerPolicy="no-referrer"'), "message iframe suppresses referrer data");
|
|
assert(!privateMessageDisplay.includes("tracking.example"), "rendered iframe source contains no remote tracking URL");
|