Replace ad-hoc lime/forest-green system with the official 4-color HLC palette. Lime + midnight are the primary pair; cyan + cream supporting. Tokens - :root now exposes --hlc-lime, --hlc-midnight, --hlc-cyan, --hlc-cream plus channel-token siblings (--hlc-*-rgb) so tints can be expressed as rgb(var(--hlc-*-rgb) / a) without hex literals. - --color-bg → cream, --color-text/--color-hero-bg → midnight, --color-accent → lime, --color-accent-dark → midnight (foreground on lime; passes WCAG AA where #fff failed). - New --sidebar-* tokens for the dark sidebar surface. Sweep (frontend/src/styles/global.css) - Replaced every hard-coded #c6f41c / #65a30d / #84cc16 / #b8e616 / #4d7c0f / #1a2e1a / #1a1a2e / #1a2e05 with the matching var(...). - rgba(101,163,13,a) and rgba(198,244,28,a) collapsed to rgb(var(--hlc-lime-rgb) / a). - text-on-lime now uses var(--color-accent-dark) instead of #fff; btn-danger keeps white on red. Sidebar reskin (cronus's audit, F-30) - Background: midnight; text: cream (muted via cream-channel alpha); active/hover: lime. Border + hover use cream-channel alphas so no rgba hex creep on the dark surface. Brand assets - manifest.json theme_color → lime, background_color → cream. - icon.svg / icon-maskable.svg base recoloured to lime + midnight glyph. - 32× <meta name="theme-color"> across pages updated to #BFF355. - Email templates (base.html, invitation.html) lime accent updated; mail_service_test.go expectation tracks the new hex. Deferred / out of scope - PNG icons under public/icons/ are baked artefacts; regen left to the next deploy. - Categorical chip colours (office tints, traffic-light red/amber/green, termin-type hues) are functional, not brand, and deliberately untouched. - Dark mode is not in scope. Verified - bun run build clean. - go build ./... clean; mail render tests pass. - Visual sweep at 1280×900 against frontend/dist via Playwright on /, /login, /dashboard, /projects, /agenda, /team, /fristenrechner, /glossary — sidebar midnight + lime active, cream page bg, white cards, midnight text on lime CTAs. Supersedes audit findings F-14, F-30, F-31.
154 lines
4.8 KiB
Go
154 lines
4.8 KiB
Go
package services
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestHTMLToText covers the HTML→plain-text fallback. Users with text-only
|
|
// clients still need to read reminder/invite mails, and some spam filters
|
|
// downrank multipart/alternative when the text part is empty or identical
|
|
// to the HTML.
|
|
func TestHTMLToText(t *testing.T) {
|
|
in := `<html><head><style>b{color:red}</style></head><body>` +
|
|
`<h1>Deadline überfällig</h1><p>Hallo <b>Welt</b></p>` +
|
|
`<p>Zweite Zeile — ok.</p><script>alert(1)</script></body></html>`
|
|
got := htmlToText(in)
|
|
if !strings.Contains(got, "Deadline überfällig") {
|
|
t.Errorf("expected decoded umlauts in %q", got)
|
|
}
|
|
if strings.Contains(got, "alert(1)") {
|
|
t.Errorf("script content leaked into text body: %q", got)
|
|
}
|
|
if strings.Contains(got, "<b>") {
|
|
t.Errorf("raw tag remained in text body: %q", got)
|
|
}
|
|
if !strings.Contains(got, "—") {
|
|
t.Errorf("expected em-dash decoded, got %q", got)
|
|
}
|
|
}
|
|
|
|
// TestRenderTemplateDeadlineReminder verifies that the template bundle wires
|
|
// base.html + the content template together and fills in user-facing fields.
|
|
// A typo in deadline_reminder.html would fail here before any SMTP I/O.
|
|
func TestRenderTemplateDeadlineReminder(t *testing.T) {
|
|
svc, err := NewMailService()
|
|
if err != nil {
|
|
t.Fatalf("NewMailService: %v", err)
|
|
}
|
|
html, err := svc.RenderTemplate(TemplateData{
|
|
Subject: "[Paliad] Deadline morgen: X",
|
|
Lang: "de",
|
|
Name: "deadline_reminder",
|
|
Data: map[string]any{
|
|
"Kind": "tomorrow",
|
|
"Title": "Schriftsatz einreichen",
|
|
"DueDate": "2026-04-21",
|
|
"ProjectReference": "2026/0042",
|
|
"ProjectTitle": "Mustermann ./. Musterfrau",
|
|
"DeadlineURL": "https://paliad.de/deadlines/123",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("RenderTemplate: %v", err)
|
|
}
|
|
for _, want := range []string{
|
|
"Paliad", "Schriftsatz einreichen", "2026-04-21", "2026/0042",
|
|
"Mustermann ./. Musterfrau", "https://paliad.de/deadlines/123",
|
|
"morgen", "#BFF355",
|
|
} {
|
|
if !strings.Contains(html, want) {
|
|
t.Errorf("rendered html missing %q", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestRenderTemplateInvitation covers the invitation template so a typo in
|
|
// invitation.html would fail CI.
|
|
func TestRenderTemplateInvitation(t *testing.T) {
|
|
svc, err := NewMailService()
|
|
if err != nil {
|
|
t.Fatalf("NewMailService: %v", err)
|
|
}
|
|
html, err := svc.RenderTemplate(TemplateData{
|
|
Subject: "[Paliad] Anna Schmidt lädt Sie ein",
|
|
Lang: "en",
|
|
Name: "invitation",
|
|
Data: map[string]any{
|
|
"InviterName": "Anna Schmidt",
|
|
"InviterEmail": "anna@hlc.com",
|
|
"ToEmail": "colleague@hlc.com",
|
|
"Message": "Have a look at Paliad.",
|
|
"RegisterURL": "https://paliad.de/login",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("RenderTemplate: %v", err)
|
|
}
|
|
for _, want := range []string{
|
|
"Anna Schmidt", "invites you", "Have a look at Paliad.",
|
|
"https://paliad.de/login", "colleague@hlc.com",
|
|
} {
|
|
if !strings.Contains(html, want) {
|
|
t.Errorf("rendered html missing %q", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestRenderTemplateDeadlineWeekly confirms the weekly summary iterates over
|
|
// its .Items slice and applies the overdue flag.
|
|
func TestRenderTemplateDeadlineWeekly(t *testing.T) {
|
|
svc, err := NewMailService()
|
|
if err != nil {
|
|
t.Fatalf("NewMailService: %v", err)
|
|
}
|
|
html, err := svc.RenderTemplate(TemplateData{
|
|
Subject: "[Paliad] Wochenübersicht",
|
|
Lang: "de",
|
|
Name: "deadline_weekly",
|
|
Data: map[string]any{
|
|
"Count": 2,
|
|
"DeadlinesURL": "https://paliad.de/deadlines",
|
|
"Items": []map[string]any{
|
|
{"DueDate": "2026-04-20", "Title": "Heute f.", "ProjectReference": "2026/0001", "URL": "https://paliad.de/deadlines/a", "Overdue": true},
|
|
{"DueDate": "2026-04-24", "Title": "Später f.", "ProjectReference": "2026/0002", "URL": "https://paliad.de/deadlines/b", "Overdue": false},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("RenderTemplate: %v", err)
|
|
}
|
|
for _, want := range []string{
|
|
"Heute f.", "Später f.", "2026/0001", "2026/0002",
|
|
"https://paliad.de/deadlines/a", "https://paliad.de/deadlines/b",
|
|
} {
|
|
if !strings.Contains(html, want) {
|
|
t.Errorf("rendered html missing %q", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestBuildMIMEHasBothParts ensures the multipart/alternative structure
|
|
// carries both the text and HTML parts — an earlier refactor dropped one
|
|
// part by mistake, caught by this.
|
|
func TestBuildMIMEHasBothParts(t *testing.T) {
|
|
msg := buildMIME("mail@paliad.de", "Paliad", "to@example.com",
|
|
"Test", "<p>HTML</p>", "TEXT")
|
|
body := string(msg)
|
|
if !strings.Contains(body, "Content-Type: text/plain") {
|
|
t.Error("missing text/plain part")
|
|
}
|
|
if !strings.Contains(body, "Content-Type: text/html") {
|
|
t.Error("missing text/html part")
|
|
}
|
|
if !strings.Contains(body, "multipart/alternative") {
|
|
t.Error("not multipart/alternative")
|
|
}
|
|
if !strings.Contains(body, "TEXT") {
|
|
t.Error("text body missing")
|
|
}
|
|
if !strings.Contains(body, "<p>HTML</p>") {
|
|
t.Error("html body missing")
|
|
}
|
|
}
|