fields.go: neuer Typ FacetField mit vier geschlossenen Konstanten (FacetFieldSender/Mailbox/AttachmentType/Tag). IsValid() entscheidet über ein erschöpfendes switch/case statt eine []string-Liste zu durchsuchen — genau der aus known-issues-archivmail.md #12 und known-issues-archivdms.md #10 bekannte Fehler (dynamische Tabellen-/ Feldnamen nur durch eine fragile Whitelist-Funktion abgesichert) wird damit strukturell vermieden: ein vergessener Listeneintrag kann nichts mehr durchlassen, weil es keine durchsuchte Liste mehr gibt. ParseFacetField ist die einzige vorgesehene Konstruktionsstelle für FacetField aus einer externen Zeichenkette. facets.go: FacetFilter.Field ist jetzt FacetField statt string, buildFilteredMust prüft f.Field.IsValid() statt Listenmitgliedschaft (isFacetField entfernt, es gibt keine Liste mehr, die die Entscheidung trifft). Alle Pflichtprüfungen mit echten Nachweisen: unbekannte/erfundene Facettenfelder werden abgelehnt, alle vier realen Facettenfelder funktionieren weiterhin, ein FacetField-Wert per direkter Typkonvertierung (nicht über ParseFacetField) wird trotzdem zuverlässig abgelehnt (Akzeptanzkriterium 2: Whitelist ist nicht die einzige Absicherung), automatisiertes Code-Review bestätigt kein fmt.Sprintf in facets.go/fields.go. Entscheidung dokumentiert: Mail-eigene Implementierung, keine geteilte Utility mit dem DMS-Board (Prüfprotokoll). Keine Regression, insbesondere mail/internal/savedsearch (Konsument von FacetFilter) unverändert grün — go build/go vet/golangci-lint clean, gesamtes Mail-Modul regressionsfrei getestet.
118 lines
4.7 KiB
Go
118 lines
4.7 KiB
Go
// SRC-11: Feld-Whitelist-Query-Builder für Suchindex-Zugriff. Reine
|
|
// Unit-Tests (kein Manticore nötig) — buildFilteredMust und FacetField
|
|
// sind pure Funktionen/Typen.
|
|
package search
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestBuildFilteredMust_RejectsUnknownField ist die geforderte
|
|
// Pflichtprüfung 1 (SRC-11): Versuch, ein nicht in der Whitelist
|
|
// enthaltenes Feld anzufragen, wird abgewiesen statt stillschweigend
|
|
// ignoriert.
|
|
func TestBuildFilteredMust_RejectsUnknownField(t *testing.T) {
|
|
// FacetField(...) simuliert genau den Fall, den Akzeptanzkriterium 2
|
|
// verlangt: ein Wert, der NICHT über die vorgesehene
|
|
// ParseFacetField-Konstruktion entstanden ist (z. B. aus einem
|
|
// veralteten Datenbankeintrag nach Entfernen eines Feldes) — muss
|
|
// trotzdem abgelehnt werden.
|
|
unknown := FacetField("tenant_slug") // existiert als Suchfeld, ist aber KEIN Facettenfeld
|
|
_, err := buildFilteredMust("mandant-x", "", []FacetFilter{{Field: unknown, Value: "x"}})
|
|
if err == nil {
|
|
t.Fatalf("erwartete ablehnung für unbekanntes facettenfeld %q, bekam keinen fehler", unknown)
|
|
}
|
|
if !strings.Contains(err.Error(), string(unknown)) {
|
|
t.Fatalf("fehlermeldung sollte das abgelehnte feld nennen, habe: %v", err)
|
|
}
|
|
|
|
// Frei erfundenes Feld, das nirgendwo im Schema existiert.
|
|
madeUp := FacetField("'; DROP TABLE mail_documents; --")
|
|
_, err = buildFilteredMust("mandant-x", "", []FacetFilter{{Field: madeUp, Value: "x"}})
|
|
if err == nil {
|
|
t.Fatalf("erwartete ablehnung für frei erfundenes facettenfeld, bekam keinen fehler")
|
|
}
|
|
}
|
|
|
|
// TestBuildFilteredMust_AcceptsAllWhitelistedFields stellt sicher, dass
|
|
// alle vier vorgesehenen Facettenfelder tatsächlich funktionieren (keine
|
|
// versehentlich zu strenge Prüfung).
|
|
func TestBuildFilteredMust_AcceptsAllWhitelistedFields(t *testing.T) {
|
|
for _, field := range FacetFields {
|
|
_, err := buildFilteredMust("mandant-x", "", []FacetFilter{{Field: field, Value: "x"}})
|
|
if err != nil {
|
|
t.Fatalf("feld %q hätte akzeptiert werden müssen: %v", field, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestFacetField_ClosedSetEvenViaDirectTypeConversion ist die geforderte
|
|
// Pflichtprüfung/Akzeptanzkriterium 2: die Whitelist ist nicht die
|
|
// einzige Absicherung. Selbst ein FacetField-Wert, der NICHT über
|
|
// ParseFacetField entstanden ist (direkte Typkonvertierung, z. B. durch
|
|
// künftigen Code, der die vorgesehene Konstruktion umgeht), wird von
|
|
// IsValid() zuverlässig abgelehnt — die Prüfung hängt an einem
|
|
// erschöpfenden switch/case auf den vier Konstanten, nicht an einer
|
|
// durchsuchbaren Liste, die vergessen werden könnte.
|
|
func TestFacetField_ClosedSetEvenViaDirectTypeConversion(t *testing.T) {
|
|
valid := []FacetField{FacetFieldSender, FacetFieldMailbox, FacetFieldAttachmentType, FacetFieldTag}
|
|
for _, f := range valid {
|
|
if !f.IsValid() {
|
|
t.Fatalf("erwartete gültiges feld %q als gültig", f)
|
|
}
|
|
}
|
|
|
|
invalid := []FacetField{
|
|
FacetField(FieldTenantSlug), // reales Suchfeld, aber keine Facette
|
|
FacetField(FieldBody),
|
|
FacetField("subject; --"),
|
|
FacetField(""),
|
|
}
|
|
for _, f := range invalid {
|
|
if f.IsValid() {
|
|
t.Fatalf("feld %q hätte als ungültig erkannt werden müssen", f)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestParseFacetField_OnlyAcceptsKnownStrings deckt die einzige
|
|
// vorgesehene Konstruktionsstelle für FacetField aus einer externen
|
|
// Zeichenkette ab.
|
|
func TestParseFacetField_OnlyAcceptsKnownStrings(t *testing.T) {
|
|
if _, ok := ParseFacetField("sender"); !ok {
|
|
t.Fatalf("'sender' hätte als gültiges facettenfeld erkannt werden müssen")
|
|
}
|
|
if _, ok := ParseFacetField("nicht_existent"); ok {
|
|
t.Fatalf("unbekannter feldname hätte abgelehnt werden müssen")
|
|
}
|
|
if _, ok := ParseFacetField("tenant_slug"); ok {
|
|
t.Fatalf("ein reales, aber nicht-facettiertes suchfeld hätte abgelehnt werden müssen")
|
|
}
|
|
}
|
|
|
|
// TestNoDynamicFieldNameConstruction ist die geforderte Pflichtprüfung 2
|
|
// (SRC-11): Code-Review bestätigt automatisiert, dass facets.go und
|
|
// fields.go keinen dynamischen Spalten-/Tabellennamen per
|
|
// String-Zusammenbau (fmt.Sprintf/+) erzeugen — Feldnamen kommen
|
|
// ausschließlich aus den FacetField-Konstanten bzw. den statischen
|
|
// FieldXxx-Konstanten dieses Pakets.
|
|
func TestNoDynamicFieldNameConstruction(t *testing.T) {
|
|
for _, file := range []string{"facets.go", "fields.go"} {
|
|
src, err := os.ReadFile(file)
|
|
if err != nil {
|
|
t.Fatalf("%s lesen: %v", file, err)
|
|
}
|
|
for _, line := range strings.Split(string(src), "\n") {
|
|
trimmed := strings.TrimSpace(line)
|
|
if strings.HasPrefix(trimmed, "//") {
|
|
continue // Kommentarzeilen dürfen den Begriff zur Erklärung nennen
|
|
}
|
|
if strings.Contains(line, "fmt.Sprintf") {
|
|
t.Fatalf("%s darf kein fmt.Sprintf im Code verwenden (dynamische Feldnamenbildung verboten, SRC-11 Akzeptanzkriterium 1): %q", file, trimmed)
|
|
}
|
|
}
|
|
}
|
|
}
|