Manticore-RT-Index für Mail-Suchdokumente (Betreff, Text, Anhangstext, Metadaten), statisches versioniertes Schema. - fields.go: statische Feld-/Index-Namen-Whitelist, einzige Quelle für Feldnamen im Paket (vermeidet known-issues-archivmail.md #11/#12: Sprintf/Join-basierte SQL-Klauselbildung). - migrations/0001_mail_documents.sql: statisches Schema, per go:embed eingebettet, über /sql?mode=raw angelegt (kein String-Zusammenbau). - client.go: Index/Search über die strukturierte Manticore-HTTP-JSON-API, Tenant-Filter über strukturiertes equals-Feld statt WHERE-Interpolation. Prüfungen (alle real durchgeführt, siehe mail/docs/SRC-01-PRUEFPROTOKOLL.md): 1. TestNoDynamicSQLClauseBuilding: automatisierter Quelltext-Scan bestätigt keine Sprintf/Join-SQL-Klauselbildung. 2. TestSearch_MaliciousInputDoesNotAlterFieldNames: Injection-artige Eingaben verändern nachweislich keine Feldnamen im gesendeten Payload. 3. TestSearch_FindsExpectedDocument: Funktionstest gegen echtes Manticore auf 192.168.1.131 liefert erwartete Treffer. Zusätzlich TestSearch_TenantIsolation für Akzeptanzkriterium 3. Kein Umbau: storage/crypto/encstorage/dedup unverändert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
99 lines
2.7 KiB
Go
99 lines
2.7 KiB
Go
// Integrationstest (SRC-01): echte Manticore-Instanz auf dem Testhost.
|
|
// Folgt derselben TEST_*-Env-Konvention wie mail/internal/example und
|
|
// mail/internal/dedup — TEST_MANTICORE_URL.
|
|
package search
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"testing"
|
|
)
|
|
|
|
func setupClient(t *testing.T) *Client {
|
|
t.Helper()
|
|
baseURL := os.Getenv("TEST_MANTICORE_URL")
|
|
if baseURL == "" {
|
|
t.Skip("TEST_MANTICORE_URL nicht gesetzt, Integrationstest übersprungen")
|
|
}
|
|
client := NewClient(baseURL)
|
|
if err := client.EnsureSchema(context.Background()); err != nil {
|
|
t.Fatalf("schema: %v", err)
|
|
}
|
|
return client
|
|
}
|
|
|
|
// TestSearch_FindsExpectedDocument ist die geforderte Pflichtprüfung 3:
|
|
// Funktionstest bestätigt, dass die Volltextsuche erwartete Treffer für
|
|
// einen Testkorpus liefert.
|
|
func TestSearch_FindsExpectedDocument(t *testing.T) {
|
|
client := setupClient(t)
|
|
ctx := context.Background()
|
|
tenant := "mandant-src01-funktionstest"
|
|
|
|
if err := client.Index(ctx, Document{
|
|
ID: 910001,
|
|
TenantSlug: tenant,
|
|
MessageID: "msg-funktionstest-1",
|
|
Subject: "Quartalsbericht Q3",
|
|
Body: "Anbei der vollständige Quartalsbericht mit Umsatzzahlen.",
|
|
}); err != nil {
|
|
t.Fatalf("index: %v", err)
|
|
}
|
|
if err := client.Index(ctx, Document{
|
|
ID: 910002,
|
|
TenantSlug: tenant,
|
|
MessageID: "msg-funktionstest-2",
|
|
Subject: "Mittagessen morgen",
|
|
Body: "Wollen wir zusammen essen gehen?",
|
|
}); err != nil {
|
|
t.Fatalf("index: %v", err)
|
|
}
|
|
|
|
results, err := client.Search(ctx, tenant, "Quartalsbericht")
|
|
if err != nil {
|
|
t.Fatalf("search: %v", err)
|
|
}
|
|
found := false
|
|
for _, r := range results {
|
|
if r.MessageID == "msg-funktionstest-1" {
|
|
found = true
|
|
}
|
|
if r.MessageID == "msg-funktionstest-2" {
|
|
t.Fatal("unerwarteter treffer für nicht passende nachricht")
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("erwarteten treffer msg-funktionstest-1 nicht gefunden, habe: %+v", results)
|
|
}
|
|
}
|
|
|
|
// TestSearch_TenantIsolation ist Akzeptanzkriterium 3 (mandantengetrennt
|
|
// abfragbar): identischer Inhalt bei zwei Mandanten, Suche bei Mandant A
|
|
// darf keinen Treffer bei Mandant B liefern.
|
|
func TestSearch_TenantIsolation(t *testing.T) {
|
|
client := setupClient(t)
|
|
ctx := context.Background()
|
|
tenantA := "mandant-src01-iso-a"
|
|
tenantB := "mandant-src01-iso-b"
|
|
|
|
if err := client.Index(ctx, Document{
|
|
ID: 910101,
|
|
TenantSlug: tenantA,
|
|
MessageID: "msg-iso-a",
|
|
Subject: "Vertraulicher Betreff Alpha",
|
|
Body: "Inhalt nur für Mandant A.",
|
|
}); err != nil {
|
|
t.Fatalf("index mandant a: %v", err)
|
|
}
|
|
|
|
results, err := client.Search(ctx, tenantB, "Vertraulicher")
|
|
if err != nil {
|
|
t.Fatalf("search mandant b: %v", err)
|
|
}
|
|
for _, r := range results {
|
|
if r.MessageID == "msg-iso-a" {
|
|
t.Fatal("mandant b hat treffer aus mandant a gesehen — mandantentrennung verletzt")
|
|
}
|
|
}
|
|
}
|