package index_test import ( "testing" "time" "github.com/archivmail/internal/index" ) // newXapianIndex creates a temporary Xapian index for testing. func newXapianIndex(t *testing.T) index.Indexer { t.Helper() idx, err := index.New(t.TempDir(), 100, "xapian") if err != nil { t.Skip("xapian not available:", err) } t.Cleanup(func() { idx.Close() }) return idx } func seedDocs(t *testing.T, idx index.Indexer) { t.Helper() docs := []index.MailDocument{ { ID: "aaa111", From: "alice@example.com", To: "bob@example.com", Subject: "Invoice Q1-2026", Body: "Please find attached the invoice for January.", Date: time.Date(2026, 1, 15, 10, 0, 0, 0, time.UTC), Size: 1024, }, { ID: "bbb222", From: "bob@example.com", To: "alice@example.com charlie@example.com", Subject: "Meeting Agenda", Body: "Agenda for the quarterly review meeting.", Date: time.Date(2026, 2, 1, 9, 0, 0, 0, time.UTC), Size: 512, }, { ID: "ccc333", From: "charlie@example.com", To: "alice@example.com", Subject: "Offer with attachment", Body: "Please review the attached offer document.", AttachNames: "offer.pdf", HasAttachment: true, Date: time.Date(2026, 3, 1, 14, 0, 0, 0, time.UTC), Size: 8192, }, } for _, d := range docs { if err := idx.IndexSync(d); err != nil { t.Fatalf("IndexSync %s: %v", d.ID, err) } } } func TestIndexAndSearchFulltext(t *testing.T) { idx := newXapianIndex(t) seedDocs(t, idx) result, err := idx.Search(index.SearchRequest{Query: "invoice", PageSize: 10}) if err != nil { t.Fatalf("Search: %v", err) } if result.Total == 0 { t.Error("expected at least 1 hit for 'invoice'") } if result.Hits[0].ID != "aaa111" { t.Errorf("top hit = %q, want aaa111", result.Hits[0].ID) } } func TestSearchMatchAll(t *testing.T) { idx := newXapianIndex(t) seedDocs(t, idx) result, err := idx.Search(index.SearchRequest{PageSize: 25}) if err != nil { t.Fatalf("Search all: %v", err) } if result.Total != 3 { t.Errorf("expected 3 total hits, got %d", result.Total) } } func TestSearchFromFilter(t *testing.T) { idx := newXapianIndex(t) seedDocs(t, idx) result, err := idx.Search(index.SearchRequest{ From: "alice@example.com", PageSize: 25, }) if err != nil { t.Fatalf("Search from: %v", err) } if result.Total != 1 { t.Errorf("expected 1 hit from alice, got %d", result.Total) } } func TestSearchDateRange(t *testing.T) { idx := newXapianIndex(t) seedDocs(t, idx) from := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) to := time.Date(2026, 2, 1, 23, 59, 59, 0, time.UTC) result, err := idx.Search(index.SearchRequest{ DateFrom: &from, DateTo: &to, PageSize: 25, }) if err != nil { t.Fatalf("Search date range: %v", err) } if result.Total != 2 { t.Errorf("expected 2 hits in Jan-Feb 2026, got %d", result.Total) } } func TestSearchOwnEmail(t *testing.T) { idx := newXapianIndex(t) seedDocs(t, idx) // charlie@example.com sent 1 mail and received 1 mail = should see 2 result, err := idx.Search(index.SearchRequest{ OwnEmail: "charlie@example.com", PageSize: 25, }) if err != nil { t.Fatalf("Search OwnEmail: %v", err) } if result.Total < 1 { t.Errorf("charlie should see at least 1 mail, got %d", result.Total) } } func TestSearchPagination(t *testing.T) { idx := newXapianIndex(t) seedDocs(t, idx) page0, _ := idx.Search(index.SearchRequest{PageSize: 2, Page: 0}) page1, _ := idx.Search(index.SearchRequest{PageSize: 2, Page: 1}) if len(page0.Hits) != 2 { t.Errorf("page 0: expected 2 hits, got %d", len(page0.Hits)) } if len(page1.Hits) != 1 { t.Errorf("page 1: expected 1 hit, got %d", len(page1.Hits)) } // No overlap if page0.Hits[0].ID == page1.Hits[0].ID { t.Error("pagination returned duplicate results") } } func TestDelete(t *testing.T) { idx := newXapianIndex(t) seedDocs(t, idx) if err := idx.Delete("aaa111"); err != nil { t.Fatalf("Delete: %v", err) } result, _ := idx.Search(index.SearchRequest{Query: "invoice", PageSize: 10}) for _, h := range result.Hits { if h.ID == "aaa111" { t.Error("deleted document still in results") } } } func TestUnknownBackend(t *testing.T) { _, err := index.New(t.TempDir(), 10, "elasticsearch") if err == nil { t.Error("expected error for unknown backend") } } func TestXapianNotCompiledError(t *testing.T) { _, err := index.New(t.TempDir(), 10, "xapian") // Without -tags xapian this must return a helpful error if err == nil { t.Log("xapian compiled in — skipping stub error test") } else { t.Logf("xapian stub error (expected): %v", err) } }