fix(PROJ-74): Test-/Vet-Signatur-Drift beheben, go build/vet/test wieder grün
archivmail-export nutzte noch die alte storage.New(path string)-Signatur statt storage.Config (fehlender Keyfile/Compress hätte Rohbytes statt Klartext-EML exportiert). Toten Self-Assignment-Code in storage.go entfernt. Testdateien (storage, audit, api, userstore, auth) an aktuelle Signaturen angeglichen; auth-Tests liefen bisher gegen einen SQLite-Pfad statt Postgres- DSN und wurden auf das TEST_DATABASE_URL-Schema-Isolationsmuster der übrigen Pakete umgestellt. api_test.go las den Login-Token noch aus dem JSON-Body statt aus dem httpOnly-Cookie (Auth-Contract-Drift). TestParseMissingDate an tatsächliches Verhalten angepasst: der Parser lässt das Datum bewusst als Zero-Value, der time.Now()-Fallback sitzt in der Storage-Schicht — damit bleibt nachvollziehbar ob ein Datum aus der Mail stammt oder vom Archiv gesetzt wurde (GoBD). Verifiziert auf 192.168.1.132: go build/vet/test ./... komplett grün, kein Skip (Postgres + Manticore erreichbar). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019j28kGcaJAhBnrYX34hGdt
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
88cdc3eb3e
commit
075afa005a
@@ -4,8 +4,8 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -33,9 +33,9 @@ type testEnv struct {
|
||||
func newTestEnv(t *testing.T) *testEnv {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
logger := slog.New(slog.NewTextHandler(os.Discard, nil))
|
||||
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
|
||||
store, err := storage.New(filepath.Join(dir, "store"))
|
||||
store, err := storage.New(storage.Config{Dir: filepath.Join(dir, "store")})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -133,9 +133,13 @@ func (e *testEnv) login(t *testing.T, username, password string) string {
|
||||
if w.Code != 200 {
|
||||
t.Fatalf("login %s: status %d, body: %s", username, w.Code, w.Body.String())
|
||||
}
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
return resp["token"].(string)
|
||||
for _, c := range w.Result().Cookies() {
|
||||
if c.Name == "archivmail_session" {
|
||||
return c.Value
|
||||
}
|
||||
}
|
||||
t.Fatalf("login %s: no session cookie in response, body: %s", username, w.Body.String())
|
||||
return ""
|
||||
}
|
||||
|
||||
// ---- Tests ----
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -75,7 +76,7 @@ func newTestAudit(t *testing.T) *audit.Logger {
|
||||
}
|
||||
schemaDSN := dsn + sep + "search_path=" + schema
|
||||
|
||||
logger := slog.New(slog.NewTextHandler(os.Discard, nil))
|
||||
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
l, err := audit.New(schemaDSN, t.TempDir(), logger)
|
||||
if err != nil {
|
||||
t.Fatalf("audit.New: %v", err)
|
||||
@@ -218,7 +219,7 @@ func TestQueryPagination(t *testing.T) {
|
||||
// DELETE on audit_log through the normal application connection.
|
||||
func TestImmutableTrigger(t *testing.T) {
|
||||
dsn, schemaDSN := testDSN(t)
|
||||
logger := slog.New(slog.NewTextHandler(os.Discard, nil))
|
||||
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
l, err := audit.New(schemaDSN, t.TempDir(), logger)
|
||||
if err != nil {
|
||||
t.Fatalf("audit.New: %v", err)
|
||||
@@ -255,7 +256,7 @@ func TestImmutableTrigger(t *testing.T) {
|
||||
func TestFileLogging(t *testing.T) {
|
||||
_, schemaDSN := testDSN(t)
|
||||
logPath := filepath.Join(t.TempDir(), "audit.log")
|
||||
logger := slog.New(slog.NewTextHandler(os.Discard, nil))
|
||||
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
l, err := audit.New(schemaDSN, logPath, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("audit.New: %v", err)
|
||||
@@ -297,7 +298,7 @@ func TestFileLoggingUnwritableContinues(t *testing.T) {
|
||||
_, schemaDSN := testDSN(t)
|
||||
// A directory cannot be opened for writing → file logging disabled.
|
||||
logPath := t.TempDir()
|
||||
logger := slog.New(slog.NewTextHandler(os.Discard, nil))
|
||||
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
l, err := audit.New(schemaDSN, logPath, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("audit.New should not fail on unwritable path: %v", err)
|
||||
|
||||
@@ -1,20 +1,61 @@
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
||||
"archivmail/internal/auth"
|
||||
"archivmail/internal/userstore"
|
||||
)
|
||||
|
||||
// newTestAuth builds an auth.Manager on top of a schema-isolated PostgreSQL
|
||||
// userstore. userstore.New expects a PostgreSQL DSN (pgxpool) — the former
|
||||
// SQLite file path ("users.db") made pgx fail with "cannot parse ...".
|
||||
func newTestAuth(t *testing.T) (*auth.Manager, *userstore.Store) {
|
||||
t.Helper()
|
||||
store, err := userstore.New(filepath.Join(t.TempDir(), "users.db"))
|
||||
dsn := os.Getenv("TEST_DATABASE_URL")
|
||||
if dsn == "" {
|
||||
t.Skip("TEST_DATABASE_URL not set — skipping (needs PostgreSQL)")
|
||||
}
|
||||
|
||||
schema := "authtest_" + strings.ToLower(strings.ReplaceAll(t.Name(), "/", "_"))
|
||||
if len(schema) > 63 {
|
||||
schema = schema[:63]
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
conn, err := pgx.Connect(ctx, dsn)
|
||||
if err != nil {
|
||||
t.Fatalf("connect: %v", err)
|
||||
}
|
||||
if _, err := conn.Exec(ctx, "CREATE SCHEMA IF NOT EXISTS "+schema); err != nil {
|
||||
conn.Close(ctx)
|
||||
t.Fatalf("create schema: %v", err)
|
||||
}
|
||||
conn.Close(ctx)
|
||||
|
||||
sep := "?"
|
||||
if strings.Contains(dsn, "?") {
|
||||
sep = "&"
|
||||
}
|
||||
schemaDSN := dsn + sep + "search_path=" + schema
|
||||
|
||||
store, err := userstore.New(schemaDSN)
|
||||
if err != nil {
|
||||
t.Fatalf("userstore.New: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { store.Close() })
|
||||
t.Cleanup(func() {
|
||||
store.Close()
|
||||
conn2, _ := pgx.Connect(context.Background(), dsn)
|
||||
if conn2 != nil {
|
||||
conn2.Exec(context.Background(), "DROP SCHEMA "+schema+" CASCADE")
|
||||
conn2.Close(context.Background())
|
||||
}
|
||||
})
|
||||
|
||||
// Seed a test user
|
||||
store.Create(userstore.CreateUserRequest{
|
||||
|
||||
@@ -503,10 +503,9 @@ func (s *Store) Save(ctx context.Context, raw []byte, _ time.Time, tenantID *int
|
||||
}
|
||||
|
||||
if parseErr == nil {
|
||||
// PROJ-38: resolve thread before inserting
|
||||
if pm.InReplyTo != "" || len(pm.References) > 0 {
|
||||
pm.MessageID = pm.MessageID // no-op; thread resolved inside insertMeta
|
||||
}
|
||||
// PROJ-38: resolve thread before inserting.
|
||||
// In-Reply-To/References werden innerhalb von resolveThreadID
|
||||
// ausgewertet, hier ist keine Vorverarbeitung nötig.
|
||||
threadID := s.resolveThreadID(ctx, pm)
|
||||
if err := s.insertMeta(ctx, id, pm, len(raw), tenantID, storageID, threadID); err != nil {
|
||||
// Race: another goroutine inserted via Message-ID UNIQUE conflict.
|
||||
|
||||
@@ -2,6 +2,7 @@ package storage_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -12,13 +13,13 @@ import (
|
||||
|
||||
func TestSaveAndLoad(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
store, err := storage.New(dir)
|
||||
store, err := storage.New(storage.Config{Dir: dir})
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
|
||||
raw := []byte("From: alice@example.com\r\nSubject: Test\r\n\r\nHello World")
|
||||
id, err := store.Save(raw, time.Now())
|
||||
id, err := store.Save(context.Background(), raw, time.Now(), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Save: %v", err)
|
||||
}
|
||||
@@ -37,17 +38,17 @@ func TestSaveAndLoad(t *testing.T) {
|
||||
|
||||
func TestDeduplication(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
store, err := storage.New(dir)
|
||||
store, err := storage.New(storage.Config{Dir: dir})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
raw := []byte("From: alice@example.com\r\n\r\nDuplicate test")
|
||||
id1, err := store.Save(raw, time.Now())
|
||||
id1, err := store.Save(context.Background(), raw, time.Now(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id2, err := store.Save(raw, time.Now())
|
||||
id2, err := store.Save(context.Background(), raw, time.Now(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -68,13 +69,13 @@ func TestDeduplication(t *testing.T) {
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
store, err := storage.New(dir)
|
||||
store, err := storage.New(storage.Config{Dir: dir})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
raw := []byte("From: alice@example.com\r\n\r\nDelete me")
|
||||
id, _ := store.Save(raw, time.Now())
|
||||
id, _ := store.Save(context.Background(), raw, time.Now(), nil)
|
||||
|
||||
if err := store.Delete(id); err != nil {
|
||||
t.Fatalf("Delete: %v", err)
|
||||
@@ -86,7 +87,7 @@ func TestDelete(t *testing.T) {
|
||||
|
||||
func TestStats(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
store, err := storage.New(dir)
|
||||
store, err := storage.New(storage.Config{Dir: dir})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -97,7 +98,7 @@ func TestStats(t *testing.T) {
|
||||
[]byte("From: c@x.com\r\n\r\nMail 3"),
|
||||
}
|
||||
for _, m := range mails {
|
||||
store.Save(m, time.Now())
|
||||
store.Save(context.Background(), m, time.Now(), nil)
|
||||
}
|
||||
|
||||
stats, err := store.Stats()
|
||||
@@ -114,7 +115,7 @@ func TestStats(t *testing.T) {
|
||||
|
||||
func TestStorageDirectoryCreation(t *testing.T) {
|
||||
dir := filepath.Join(t.TempDir(), "nested", "path")
|
||||
_, err := storage.New(dir)
|
||||
_, err := storage.New(storage.Config{Dir: dir})
|
||||
if err != nil {
|
||||
t.Fatalf("New with nested path: %v", err)
|
||||
}
|
||||
|
||||
@@ -323,7 +323,7 @@ func TestCleanExpiredTokens(t *testing.T) {
|
||||
func TestUpsertLDAPUser(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
|
||||
u, err := s.UpsertLDAPUser("ldapuser", "ldap@corp.com", userstore.RoleAuditor)
|
||||
u, err := s.UpsertLDAPUser("ldapuser", "ldap@corp.com", userstore.RoleAuditor, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertLDAPUser: %v", err)
|
||||
}
|
||||
@@ -332,7 +332,7 @@ func TestUpsertLDAPUser(t *testing.T) {
|
||||
}
|
||||
|
||||
// Second upsert should update, not duplicate
|
||||
u2, err := s.UpsertLDAPUser("ldapuser", "ldap@corp.com", userstore.RoleAuditor)
|
||||
u2, err := s.UpsertLDAPUser("ldapuser", "ldap@corp.com", userstore.RoleAuditor, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertLDAPUser second: %v", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user