// 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) } } } }