Files
breakpilot-lehrer/school-service/internal/notifications/templates_test.go
T
Benjamin Admin 8311b33fb3
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 1m10s
CI / test-go-edu-search (push) Successful in 43s
CI / test-python-klausur (push) Failing after 4m4s
CI / test-python-agent-core (push) Successful in 44s
CI / test-nodejs-website (push) Successful in 51s
Phase 9d: Notification cron + multilingual templates + status badges
Backend (school-service):
  - notification_log table with UNIQUE(event_id, lead_days, audience,
    channel) for idempotent re-runs. Status enum sent/failed/skipped.
  - internal/notifications/templates.go: per-event-type × audience ×
    lead-day-bucket × language templates in 8 languages (de/en/tr/ar/
    uk/ru/pl/fr). Fallback chain (lang→de, eventType→andere) so we
    never miss a render.
  - service.go scans cal_school_event for events whose
    (start_date - runDate) appears in notification_lead_days. For each
    due (audience, channel) tuple it dispatches via POST to the
    Matrix/Email upstreams owned by the colleague's services.
    Empty URL → status='skipped', logged for visibility.
  - dispatcher.go handles the POST, parent-recipient lookup (joins
    parent_account + parent_child + cal_school_event.affected_class_ids),
    and writeLog with the unique constraint dropping duplicate runs.
  - main.go runs a 1-hour ticker; when time.Hour()==6 it invokes the
    scanner for today. Idempotent so transient restarts don't double-
    send.
  - POST /calendar/notifications/run-now for manual trigger + backfill
    (?date=YYYY-MM-DD).
  - GET /calendar/events/:id/notifications returns notification_log
    rows scoped to the owning teacher.
  - MATRIX_SERVICE_URL + EMAIL_SERVICE_URL env vars added (default
    empty = stub mode).

Frontend (studio-v2):
  - NotificationStatus component fetches /events/:id/notifications and
    renders coloured badges per (lead, audience, channel, status).
  - DayDetail mounts NotificationStatus inside each event card when
    notify_parents or notify_students is set.

Tests:
  - 6 new Go unit tests for bucketFor + Render (de/tr/fallback paths)
    + substitute(class_suffix). 89 subtests gesamt.
  - 2 new Playwright tests: badge render with mocked log, hidden when
    notifications are off.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 18:12:39 +02:00

76 lines
2.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package notifications
import (
"strings"
"testing"
)
func TestBucketFor(t *testing.T) {
cases := []struct {
lead int
want string
}{
{-1, "today"}, {0, "today"}, {1, "tomorrow"}, {2, "days"}, {7, "days"},
}
for _, c := range cases {
if got := bucketFor(c.lead); got != c.want {
t.Errorf("bucketFor(%d) = %q, want %q", c.lead, got, c.want)
}
}
}
func TestRender_GermanParentsToday(t *testing.T) {
subject, body := Render("fortbildung", "parents", 0, "de", Vars{
Title: "SCHILF", DatePretty: "10.10.2026", ClassName: "5a",
})
if !strings.Contains(subject, "Heute") || !strings.Contains(subject, "SCHILF") {
t.Errorf("expected today + title in subject, got %q", subject)
}
if !strings.Contains(body, "Liebe Eltern") {
t.Errorf("expected greeting in body, got %q", body)
}
if !strings.Contains(body, "(5a)") {
t.Errorf("expected class suffix, got %q", body)
}
}
func TestRender_TurkishStudentTomorrow(t *testing.T) {
subject, _ := Render("schulfeier", "students", 1, "tr", Vars{
Title: "Yaz şenliği", DatePretty: "12.06.2026",
})
if !strings.Contains(subject, "Yarın") || !strings.Contains(subject, "Yaz şenliği") {
t.Errorf("expected Turkish 'tomorrow' subject, got %q", subject)
}
}
func TestRender_FallbackLanguage(t *testing.T) {
// 'xx' isn't supported → falls back to de.
subject, _ := Render("klassenfahrt", "parents", 7, "xx", Vars{
Title: "Wattenmeer", DatePretty: "15.05.2026", ClassName: "6b",
})
if !strings.Contains(subject, "In 7 Tagen") {
t.Errorf("expected German fallback, got %q", subject)
}
}
func TestRender_FallbackEventType(t *testing.T) {
// 'unknown_type' falls back to 'andere'.
subject, _ := Render("unknown_type", "parents", 0, "de", Vars{
Title: "Sondertermin",
})
if !strings.Contains(subject, "Heute") || !strings.Contains(subject, "Sondertermin") {
t.Errorf("expected today subject after fallback, got %q", subject)
}
}
func TestSubstitute_DropsClassSuffixWhenEmpty(t *testing.T) {
out := substitute("X{{class_suffix}}Y", 0, Vars{ClassName: ""})
if out != "XY" {
t.Errorf("expected XY (no suffix), got %q", out)
}
out = substitute("X{{class_suffix}}Y", 0, Vars{ClassName: "5a"})
if out != "X (5a)Y" {
t.Errorf("expected ' (5a)' suffix, got %q", out)
}
}