From 8f9d864c6efe4e9bfbad146ead5091a1c0514af3 Mon Sep 17 00:00:00 2001 From: patrick Date: Thu, 12 Mar 2026 16:27:11 +0100 Subject: [PATCH] Dateien nach "/" hochladen --- index_test.go | 192 ++++++++++++++++++++++++++++++++++++++++++++++++++ setup.sh | 49 +++++++++++++ smoke_test.sh | 140 ++++++++++++++++++++++++++++++++++++ 3 files changed, 381 insertions(+) create mode 100644 index_test.go create mode 100644 setup.sh create mode 100644 smoke_test.sh diff --git a/index_test.go b/index_test.go new file mode 100644 index 0000000..d186ed9 --- /dev/null +++ b/index_test.go @@ -0,0 +1,192 @@ +package index_test + +import ( + "testing" + "time" + + "github.com/mailarchive/internal/index" +) + +// newBleveIndex creates a temporary Bleve index for testing +func newBleveIndex(t *testing.T) index.Indexer { + t.Helper() + idx, err := index.New(t.TempDir(), 100, "bleve") + if err != nil { + t.Fatalf("index.New(bleve): %v", 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 := newBleveIndex(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 := newBleveIndex(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 := newBleveIndex(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 := newBleveIndex(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 := newBleveIndex(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 := newBleveIndex(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 := newBleveIndex(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) + } +} diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..1bdb7d5 --- /dev/null +++ b/setup.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# setup.sh — First-time build setup for mailarchive +# Run once on a machine with internet access. +# After this, the project builds and tests without internet. +set -e + +echo "==> Checking Go version" +go version +GO_MINOR=$(go version | grep -oP 'go1\.\K[0-9]+') +if [ "$GO_MINOR" -lt 22 ]; then + echo "ERROR: Go 1.22+ required" + exit 1 +fi + +echo "" +echo "==> Downloading dependencies (go mod tidy)" +go mod tidy + +echo "" +echo "==> Verifying modules" +go mod verify + +echo "" +echo "==> Building (Bleve backend — default)" +make build + +echo "" +echo "==> Binary sizes" +ls -lh bin/ + +echo "" +echo "==> Running tests" +make test + +echo "" +echo "========================================" +echo " Build successful!" +echo "" +echo " To start the daemon:" +echo " sudo make install" +echo " sudo systemctl start mailarchive" +echo "" +echo " Or run directly:" +echo " ./bin/mailarchived --config config/config.yml" +echo "" +echo " To build with Xapian (optional, needs libxapian-dev):" +echo " apt install libxapian-dev" +echo " make build-xapian" +echo "========================================" diff --git a/smoke_test.sh b/smoke_test.sh new file mode 100644 index 0000000..e543dc4 --- /dev/null +++ b/smoke_test.sh @@ -0,0 +1,140 @@ +#!/usr/bin/env bash +# test/smoke_test.sh — manual end-to-end smoke test +# Run AFTER the daemon is started: +# ./bin/mailarchived --config config/config.test.yml +# +# Requirements: curl, jq, swaks (apt install swaks) +set -e + +BASE="http://localhost:8080" +PASS=0 +FAIL=0 + +ok() { echo " ✅ $1"; ((PASS++)); } +fail() { echo " ❌ $1"; ((FAIL++)); } +sep() { echo ""; echo "--- $1 ---"; } + +# ---- helper ---- +get() { curl -sf -H "Authorization: Bearer $TOKEN" "$BASE$1"; } +post() { curl -sf -X POST -H "Content-Type: application/json" -d "$2" "$BASE$1"; } +postauth() { curl -sf -X POST -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" -d "$2" "$BASE$1"; } + +sep "Health" +if get /api/health | grep -q '"ok"'; then + ok "GET /api/health" +else + fail "GET /api/health" +fi + +sep "Auth: Login" +RESP=$(post /api/auth/login '{"username":"admin","password":"adminpass"}') +TOKEN=$(echo "$RESP" | jq -r '.token') +if [ "$TOKEN" != "null" ] && [ -n "$TOKEN" ]; then + ok "POST /api/auth/login → token received" +else + fail "POST /api/auth/login" + echo "Response: $RESP" + exit 1 +fi + +sep "Auth: Me" +ME=$(get /api/auth/me) +if echo "$ME" | grep -q '"admin"'; then + ok "GET /api/auth/me" +else + fail "GET /api/auth/me: $ME" +fi + +sep "Auth: Reject wrong password" +CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"wrongpass"}' \ + "$BASE/api/auth/login") +if [ "$CODE" = "401" ]; then + ok "Wrong password → 401" +else + fail "Wrong password → expected 401, got $CODE" +fi + +sep "User management" +# Create user +NEW=$(postauth /api/users '{"username":"testuser","email":"test@x.com","password":"testpw","role":"user"}') +UID=$(echo "$NEW" | jq -r '.id') +if [ "$UID" != "null" ] && [ -n "$UID" ]; then + ok "POST /api/users → created id=$UID" +else + fail "POST /api/users: $NEW" +fi + +# List users +USERS=$(get /api/users) +COUNT=$(echo "$USERS" | jq '. | length') +if [ "$COUNT" -ge 2 ]; then + ok "GET /api/users → $COUNT users" +else + fail "GET /api/users → expected ≥2, got $COUNT" +fi + +sep "SMTP: Send test mail" +if command -v swaks &>/dev/null; then + swaks --to archive@localhost --from sender@localhost \ + --server localhost:2525 \ + --header "Subject: Smoke Test Invoice" \ + --body "This is a smoke test mail." \ + --silent 2 && ok "swaks SMTP send" || fail "swaks SMTP send" + sleep 1 # give indexer time +else + echo " ⚠️ swaks not installed, skipping SMTP test (apt install swaks)" +fi + +sep "Search" +RESULT=$(get "/api/search?q=smoke") +TOTAL=$(echo "$RESULT" | jq -r '.total // 0') +if [ "$TOTAL" -ge 0 ]; then + ok "GET /api/search → total=$TOTAL" +else + fail "GET /api/search: $RESULT" +fi + +sep "EML Import" +mkdir -p /tmp/test-eml +cat > /tmp/test-eml/test.eml << 'EML' +From: import@example.com +To: archive@example.com +Subject: Import Test Mail +Date: Thu, 12 Mar 2026 10:00:00 +0000 + +This mail was imported via CLI. +EML +./bin/mailarchive-import --config config/config.test.yml /tmp/test-eml/ && \ + ok "mailarchive-import" || fail "mailarchive-import" + +sep "Audit Log" +AUDIT=$(get "/api/audit") +ATOTAL=$(echo "$AUDIT" | jq -r '.total // 0') +if [ "$ATOTAL" -gt 0 ]; then + ok "GET /api/audit → $ATOTAL entries" +else + fail "GET /api/audit → expected entries, got $ATOTAL" +fi + +sep "Logout" +postauth /api/auth/logout '' > /dev/null && ok "POST /api/auth/logout" + +# Token should now be rejected +CODE=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: Bearer $TOKEN" "$BASE/api/auth/me") +if [ "$CODE" = "401" ]; then + ok "Token invalid after logout → 401" +else + fail "Token should be invalid after logout, got $CODE" +fi + +# ---- Summary ---- +echo "" +echo "========================================" +echo " Smoke test complete" +echo " Passed: $PASS Failed: $FAIL" +echo "========================================" +[ "$FAIL" -eq 0 ]