Zweistufig wie der Werkzeugkatalog: Betreiber pflegt den plattformweiten Standard (/betreiber/email-vorlagen), jeder Mandant kann ihn für sich übersteuern (/verwaltung/email-vorlagen) - ResolveEmailVorlage nutzt die eigene Vorlage falls vorhanden, sonst fällt sie auf den Plattform- Standard zurück. Passwort-Zurücksetzen ist die einzige aktuell existierende E-Mail und nutzt jetzt diese Vorlage statt Hardcoding. Bug beim Live-Verifizieren gefunden: UNIQUE(account_id, typ) verhindert bei NULLABLE account_id keine Duplikate (NULL != NULL in SQL) - jedes Speichern des Plattform-Standards erzeugte eine neue Zeile statt sie zu aktualisieren. Fix: zwei partielle Unique-Indizes statt eines gemeinsamen Constraints, mit Regressionstest abgesichert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
118 lines
4.1 KiB
Go
118 lines
4.1 KiB
Go
package store_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/netcell-it/deklarix/internal/store"
|
|
)
|
|
|
|
func TestEmailVorlagePlattformStandardExistiertNachMigration(t *testing.T) {
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
v, err := s.GetEmailVorlage(ctx, nil, "passwort_zuruecksetzen")
|
|
if err != nil {
|
|
t.Fatalf("GetEmailVorlage (Plattform): %v", err)
|
|
}
|
|
if v.AccountID != nil {
|
|
t.Fatalf("AccountID = %v, want nil (Plattform-Standard)", v.AccountID)
|
|
}
|
|
if v.Betreff == "" || v.Text == "" {
|
|
t.Fatalf("Plattform-Standard unvollständig: %+v", v)
|
|
}
|
|
}
|
|
|
|
func TestResolveEmailVorlageFaelltAufPlattformStandardZurueck(t *testing.T) {
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
accID := testAccountID(t, s)
|
|
|
|
// Ohne eigene Übersteuerung liefert Resolve den Plattform-Standard.
|
|
resolved, err := s.ResolveEmailVorlage(ctx, accID, "passwort_zuruecksetzen")
|
|
if err != nil {
|
|
t.Fatalf("ResolveEmailVorlage (kein eigener Override): %v", err)
|
|
}
|
|
if resolved.AccountID != nil {
|
|
t.Fatalf("AccountID = %v, want nil (geerbt vom Plattform-Standard)", resolved.AccountID)
|
|
}
|
|
|
|
// Eigene Übersteuerung anlegen — Resolve muss jetzt die eigene liefern.
|
|
eigene, err := s.UpsertEmailVorlage(ctx, &accID, "passwort_zuruecksetzen", "Eigener Betreff", "Eigener Text {{link}}")
|
|
if err != nil {
|
|
t.Fatalf("UpsertEmailVorlage: %v", err)
|
|
}
|
|
resolved, err = s.ResolveEmailVorlage(ctx, accID, "passwort_zuruecksetzen")
|
|
if err != nil {
|
|
t.Fatalf("ResolveEmailVorlage (mit Override): %v", err)
|
|
}
|
|
if resolved.ID != eigene.ID || resolved.Betreff != "Eigener Betreff" {
|
|
t.Fatalf("resolved = %+v, want eigene Vorlage", resolved)
|
|
}
|
|
|
|
// Zurücksetzen entfernt die Übersteuerung, Resolve fällt wieder zurück.
|
|
if err := s.DeleteEmailVorlage(ctx, &accID, "passwort_zuruecksetzen"); err != nil {
|
|
t.Fatalf("DeleteEmailVorlage: %v", err)
|
|
}
|
|
resolved, err = s.ResolveEmailVorlage(ctx, accID, "passwort_zuruecksetzen")
|
|
if err != nil {
|
|
t.Fatalf("ResolveEmailVorlage (nach Reset): %v", err)
|
|
}
|
|
if resolved.AccountID != nil {
|
|
t.Fatalf("AccountID nach Reset = %v, want nil (wieder Plattform-Standard)", resolved.AccountID)
|
|
}
|
|
}
|
|
|
|
func TestUpsertPlattformStandardAktualisiertStattZuDuplizieren(t *testing.T) {
|
|
// Regressionstest: SQL behandelt NULL nie als gleich zu NULL, ein
|
|
// naiver UNIQUE(account_id, typ)-Constraint hätte beliebig viele
|
|
// Plattform-Standard-Zeilen je typ zugelassen — jedes erneute
|
|
// Speichern hätte eine neue Zeile erzeugt statt die bestehende zu
|
|
// aktualisieren (genau das ist beim Live-Verifizieren passiert).
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
|
|
first, err := s.UpsertEmailVorlage(ctx, nil, "passwort_zuruecksetzen", "Erster Betreff", "Erster Text {{link}}")
|
|
if err != nil {
|
|
t.Fatalf("UpsertEmailVorlage (1): %v", err)
|
|
}
|
|
second, err := s.UpsertEmailVorlage(ctx, nil, "passwort_zuruecksetzen", "Zweiter Betreff", "Zweiter Text {{link}}")
|
|
if err != nil {
|
|
t.Fatalf("UpsertEmailVorlage (2): %v", err)
|
|
}
|
|
if first.ID != second.ID {
|
|
t.Fatalf("zweites Upsert erzeugte eine neue Zeile (ID %s statt %s) statt zu aktualisieren", second.ID, first.ID)
|
|
}
|
|
|
|
got, err := s.GetEmailVorlage(ctx, nil, "passwort_zuruecksetzen")
|
|
if err != nil {
|
|
t.Fatalf("GetEmailVorlage: %v", err)
|
|
}
|
|
if got.Betreff != "Zweiter Betreff" {
|
|
t.Fatalf("Betreff = %q, want %q (aktueller Stand)", got.Betreff, "Zweiter Betreff")
|
|
}
|
|
}
|
|
|
|
func TestEmailVorlageZweierMandantenIsoliert(t *testing.T) {
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
accA := testAccountID(t, s)
|
|
accB := testAccountID(t, s)
|
|
|
|
if _, err := s.UpsertEmailVorlage(ctx, &accA, "passwort_zuruecksetzen", "Vorlage A", "Text A {{link}}"); err != nil {
|
|
t.Fatalf("UpsertEmailVorlage A: %v", err)
|
|
}
|
|
|
|
resolvedB, err := s.ResolveEmailVorlage(ctx, accB, "passwort_zuruecksetzen")
|
|
if err != nil {
|
|
t.Fatalf("ResolveEmailVorlage B: %v", err)
|
|
}
|
|
if resolvedB.Betreff == "Vorlage A" {
|
|
t.Fatal("Mandant B sieht die Vorlage von Mandant A — Isolation defekt")
|
|
}
|
|
|
|
if _, err := s.GetEmailVorlage(ctx, &accB, "passwort_zuruecksetzen"); !errors.Is(err, store.ErrNotFound) {
|
|
t.Fatalf("GetEmailVorlage B (kein eigener Override) err=%v, want ErrNotFound", err)
|
|
}
|
|
}
|