Facetten- und Filter-API für Suche (Absender, Zeitraum, Postfach, Anhangstyp, Tag), mandantengetrennt, mit UND-Verknüpfung mehrerer Filter. - migrations/0002..0005: vier nummerierte ALTER-Migrationen für die neuen Facettenfelder (Manticore erlaubt nur eine Spalte je ALTER-Anweisung), EnsureSchema wendet sie idempotent nach. - fields.go: FacetFields-Whitelist, einzige zulässige Facettendimensionen. - facets.go: Client.Facets nutzt Manticores strukturierte aggs.terms/ aggs.range-API, kein dynamischer SQL-Klauselbau. Filter kombinieren als zusätzliche equals-Klauseln in derselben bool.must-Liste wie der Tenant-Filter. Zeitraum-Facette über feste Buckets via aggs.range. Prüfungen (alle real durchgeführt, siehe mail/docs/SRC-05-PRUEFPROTOKOLL.md): 1. TestFacets_CountsMatchActualHits: Facettenzahlen stimmen real mit der tatsächlichen Treffermenge überein. 2. TestFacets_ThreeFiltersCombineWithAND: 3 kombinierte Filter schränken 4 Dokumente real auf genau 1 verbleibenden Treffer ein. 3. TestFacets_TenantSeparation: Facetten eines Mandanten enthalten real keine Werte eines anderen. Kein Umbau: Search/Delete/Index-Verhalten aus SRC-01/SRC-03 unverändert, dedup/indexworker/storage/crypto/encstorage unverändert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
108 lines
4.6 KiB
Go
108 lines
4.6 KiB
Go
// Integrationstest (SRC-05): echte Manticore-Instanz, TEST_MANTICORE_URL
|
|
// (gleiche Konvention wie integration_test.go/ranking_test.go).
|
|
package search
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
)
|
|
|
|
func indexFacetDoc(t *testing.T, client *Client, ctx context.Context, tenant string, doc Document) {
|
|
t.Helper()
|
|
doc.TenantSlug = tenant
|
|
doc.ID = DocumentID(tenant, doc.MessageID)
|
|
if err := client.Index(ctx, doc); err != nil {
|
|
t.Fatalf("index %s: %v", doc.MessageID, err)
|
|
}
|
|
}
|
|
|
|
// TestFacets_CountsMatchActualHits ist die geforderte Pflichtprüfung 1:
|
|
// Facettenzahlen stimmen mit tatsächlicher Treffermenge überein.
|
|
func TestFacets_CountsMatchActualHits(t *testing.T) {
|
|
client := setupClient(t)
|
|
ctx := context.Background()
|
|
tenant := "mandant-src05-zahlen"
|
|
|
|
indexFacetDoc(t, client, ctx, tenant, Document{MessageID: "msg-fz-1", Subject: "a", Sender: "alice@example.com", Mailbox: "inbox", AttachmentType: "pdf"})
|
|
indexFacetDoc(t, client, ctx, tenant, Document{MessageID: "msg-fz-2", Subject: "b", Sender: "alice@example.com", Mailbox: "inbox", AttachmentType: "docx"})
|
|
indexFacetDoc(t, client, ctx, tenant, Document{MessageID: "msg-fz-3", Subject: "c", Sender: "bob@example.com", Mailbox: "archiv", AttachmentType: "pdf"})
|
|
|
|
result, err := client.Facets(ctx, tenant, "", nil)
|
|
if err != nil {
|
|
t.Fatalf("facets: %v", err)
|
|
}
|
|
|
|
senderCounts := toCountMap(result.Values[FieldSender])
|
|
if senderCounts["alice@example.com"] != 2 {
|
|
t.Fatalf("erwartete 2 treffer für alice@example.com, habe %d (%+v)", senderCounts["alice@example.com"], result.Values[FieldSender])
|
|
}
|
|
if senderCounts["bob@example.com"] != 1 {
|
|
t.Fatalf("erwartete 1 treffer für bob@example.com, habe %d", senderCounts["bob@example.com"])
|
|
}
|
|
|
|
attachmentCounts := toCountMap(result.Values[FieldAttachmentType])
|
|
if attachmentCounts["pdf"] != 2 {
|
|
t.Fatalf("erwartete 2 treffer für pdf, habe %d", attachmentCounts["pdf"])
|
|
}
|
|
}
|
|
|
|
// TestFacets_ThreeFiltersCombineWithAND ist die geforderte Pflichtprüfung
|
|
// 2: Kombination von drei Filtern liefert korrekt eingeschränkte Treffer.
|
|
func TestFacets_ThreeFiltersCombineWithAND(t *testing.T) {
|
|
client := setupClient(t)
|
|
ctx := context.Background()
|
|
tenant := "mandant-src05-kombi"
|
|
|
|
indexFacetDoc(t, client, ctx, tenant, Document{MessageID: "msg-kombi-treffer", Subject: "x", Sender: "alice@example.com", Mailbox: "inbox", AttachmentType: "pdf", Tag: "wichtig"})
|
|
indexFacetDoc(t, client, ctx, tenant, Document{MessageID: "msg-kombi-falscher-sender", Subject: "x", Sender: "bob@example.com", Mailbox: "inbox", AttachmentType: "pdf", Tag: "wichtig"})
|
|
indexFacetDoc(t, client, ctx, tenant, Document{MessageID: "msg-kombi-falsche-mailbox", Subject: "x", Sender: "alice@example.com", Mailbox: "archiv", AttachmentType: "pdf", Tag: "wichtig"})
|
|
indexFacetDoc(t, client, ctx, tenant, Document{MessageID: "msg-kombi-falscher-typ", Subject: "x", Sender: "alice@example.com", Mailbox: "inbox", AttachmentType: "docx", Tag: "wichtig"})
|
|
|
|
filters := []FacetFilter{
|
|
{Field: FieldSender, Value: "alice@example.com"},
|
|
{Field: FieldMailbox, Value: "inbox"},
|
|
{Field: FieldAttachmentType, Value: "pdf"},
|
|
}
|
|
result, err := client.Facets(ctx, tenant, "", filters)
|
|
if err != nil {
|
|
t.Fatalf("facets: %v", err)
|
|
}
|
|
|
|
tagCounts := toCountMap(result.Values[FieldTag])
|
|
if tagCounts["wichtig"] != 1 {
|
|
t.Fatalf("erwartete genau 1 verbleibenden treffer nach 3 UND-verknüpften filtern, habe %d (%+v)", tagCounts["wichtig"], result.Values[FieldTag])
|
|
}
|
|
}
|
|
|
|
// TestFacets_TenantSeparation ist die geforderte Pflichtprüfung 3:
|
|
// Facetten eines Mandanten enthalten keine Werte eines anderen.
|
|
func TestFacets_TenantSeparation(t *testing.T) {
|
|
client := setupClient(t)
|
|
ctx := context.Background()
|
|
tenantA := "mandant-src05-facetten-a"
|
|
tenantB := "mandant-src05-facetten-b"
|
|
|
|
indexFacetDoc(t, client, ctx, tenantA, Document{MessageID: "msg-fa-1", Subject: "a", Sender: "nur-a@example.com", Mailbox: "inbox", AttachmentType: "pdf"})
|
|
indexFacetDoc(t, client, ctx, tenantB, Document{MessageID: "msg-fb-1", Subject: "b", Sender: "nur-b@example.com", Mailbox: "inbox", AttachmentType: "pdf"})
|
|
|
|
resultB, err := client.Facets(ctx, tenantB, "", nil)
|
|
if err != nil {
|
|
t.Fatalf("facets mandant b: %v", err)
|
|
}
|
|
senderCountsB := toCountMap(resultB.Values[FieldSender])
|
|
if _, present := senderCountsB["nur-a@example.com"]; present {
|
|
t.Fatalf("mandant b sieht facettenwert von mandant a: %+v", resultB.Values[FieldSender])
|
|
}
|
|
if senderCountsB["nur-b@example.com"] != 1 {
|
|
t.Fatalf("erwartete eigenen facettenwert bei mandant b, habe: %+v", resultB.Values[FieldSender])
|
|
}
|
|
}
|
|
|
|
func toCountMap(values []FacetValue) map[string]int64 {
|
|
m := make(map[string]int64, len(values))
|
|
for _, v := range values {
|
|
m[v.Value] = v.Count
|
|
}
|
|
return m
|
|
}
|