Files
nexarch/mail/internal/search/client_test.go
T
sysopsandClaude Sonnet 5 9748307f12 SRC-03: such-api-mit-ranking
Such-API mit Ranking (Relevanz, Datum, Anhangstreffer), mandantengetrennt,
mit Grundoperatoren (Phrase, Ausschluss).

- client.go: Search nutzt jetzt Manticores query_string-Klausel statt
  match — unterstützt Phrasensuche ("...") und Ausschluss (-wort) nativ,
  Wert bleibt reiner JSON-String ohne dynamischen Feldnamen.
- fieldWeights (statische Konstanten: subject=10, body=3,
  attachment_text=1) über die Manticore-Option field_weights — Ranking
  berücksichtigt Anhangstreffer, Result.Score macht es nachvollziehbar.
- Bestehenden SRC-01-Injection-Test an die neue query_string-Struktur
  angepasst (gleiche Funktion weiterentwickelt).

Prüfungen (alle real durchgeführt, siehe mail/docs/SRC-03-PRUEFPROTOKOLL.md):
1. TestSearch_TenantIsolation (SRC-01, weiterhin gültig).
2. TestSearch_PhraseAndExclusionOperators: Phrase und Ausschluss liefern
   real erwartete Teilmengen.
3. TestSearch_PerformanceWithLargeCorpus: Suche über 1000 reale Dokumente
   in 775,8µs (Ziel 500ms) gegen echtes Manticore auf 192.168.1.131.
Zusätzlich TestSearch_RankingReflectsFieldWeightAndIsTraceable für
Akzeptanzkriterium 1.

Kein Umbau: dedup/indexworker/storage/crypto/encstorage unverändert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
2026-08-31 10:05:27 +02:00

73 lines
2.4 KiB
Go

package search
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
// TestSearch_MaliciousInputDoesNotAlterFieldNames ist die geforderte
// Pflichtprüfung 2: eine Abfrage mit manipulierten Eingabewerten
// (SQL-/Injection-artige Zeichen in tenantSlug und queryText) darf keine
// Spalten-/Tabellennamen in der an Manticore gesendeten Anfrage verändern
// — Werte landen ausschließlich als JSON-String-Werte, niemals als
// Feld-/Tabellenname.
func TestSearch_MaliciousInputDoesNotAlterFieldNames(t *testing.T) {
var captured map[string]any
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := json.NewDecoder(r.Body).Decode(&captured); err != nil {
t.Fatal(err)
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"hits":{"hits":[]}}`))
}))
defer srv.Close()
client := NewClient(srv.URL)
maliciousTenant := `acme"; DROP TABLE mail_documents; --`
maliciousQuery := `x' OR '1'='1`
if _, err := client.Search(context.Background(), maliciousTenant, maliciousQuery); err != nil {
t.Fatalf("search: %v", err)
}
query, ok := captured["query"].(map[string]any)
if !ok {
t.Fatal("erwartetes 'query'-Objekt fehlt in gesendetem Payload")
}
boolQuery, ok := query["bool"].(map[string]any)
if !ok {
t.Fatal("erwartetes 'bool'-Objekt fehlt")
}
must, ok := boolQuery["must"].([]any)
if !ok || len(must) != 2 {
t.Fatal("erwartete 'must'-Liste mit 2 Klauseln fehlt")
}
equalsClause, ok := must[0].(map[string]any)["equals"].(map[string]any)
if !ok {
t.Fatal("erwartete 'equals'-Klausel fehlt")
}
// Feldname bleibt statisch "tenant_slug" — nur der Wert enthält die
// böswillige Eingabe, unverändert als String.
if _, hasStaticField := equalsClause[FieldTenantSlug]; !hasStaticField {
t.Fatalf("erwartetes statisches Feld %q nicht gefunden, habe: %v", FieldTenantSlug, equalsClause)
}
if equalsClause[FieldTenantSlug] != maliciousTenant {
t.Fatalf("wert wurde verändert: %v", equalsClause[FieldTenantSlug])
}
// query_string hat keinerlei dynamischen Feldnamen — der Klausel-Wert
// ist unmittelbar der übergebene String, keine map mit datenabhängigem
// Schlüssel möglich.
queryStringClause, ok := must[1].(map[string]any)["query_string"]
if !ok {
t.Fatal("erwartete 'query_string'-Klausel fehlt")
}
if queryStringClause != maliciousQuery {
t.Fatalf("suchwert wurde verändert: %v", queryStringClause)
}
}