Dateien nach "/" hochladen
This commit is contained in:
+192
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 "========================================"
|
||||||
+140
@@ -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 ]
|
||||||
Reference in New Issue
Block a user