feat(PROJ-17): Admin Dashboard Systemauslastung immer anzeigen
- Systemauslastungs-Sektion wird immer gerendert (nicht nur bei Erfolg) - Fehlermeldung wenn /api/admin/system/stats nicht erreichbar ist - Feature-Status auf In Review gesetzt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user