From c8b115a635e96ce019aa3ad4f7736d79041e505b Mon Sep 17 00:00:00 2001 From: patrick Date: Thu, 12 Mar 2026 16:27:43 +0100 Subject: [PATCH] Dateien nach "/" hochladen --- storage_test.go | 126 ++++++++++++++++++++++++ userstore_test.go | 245 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 371 insertions(+) create mode 100644 storage_test.go create mode 100644 userstore_test.go diff --git a/storage_test.go b/storage_test.go new file mode 100644 index 0000000..e6eb9c3 --- /dev/null +++ b/storage_test.go @@ -0,0 +1,126 @@ +package storage_test + +import ( + "bytes" + "os" + "path/filepath" + "testing" + "time" + + "github.com/mailarchive/internal/storage" +) + +func TestSaveAndLoad(t *testing.T) { + dir := t.TempDir() + store, err := storage.New(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()) + if err != nil { + t.Fatalf("Save: %v", err) + } + if len(id) != 64 { + t.Errorf("expected 64-char SHA256 hex, got %d chars", len(id)) + } + + got, err := store.Load(id) + if err != nil { + t.Fatalf("Load: %v", err) + } + if !bytes.Equal(raw, got) { + t.Errorf("loaded content mismatch") + } +} + +func TestDeduplication(t *testing.T) { + dir := t.TempDir() + store, err := storage.New(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()) + if err != nil { + t.Fatal(err) + } + id2, err := store.Save(raw, time.Now()) + if err != nil { + t.Fatal(err) + } + if id1 != id2 { + t.Errorf("duplicate mail produced different IDs: %s vs %s", id1, id2) + } + + // Only one file should exist + count := 0 + filepath.Walk(filepath.Join(dir, "store"), func(p string, info os.FileInfo, _ error) error { + if !info.IsDir() { count++ } + return nil + }) + if count != 1 { + t.Errorf("expected 1 stored file after dedup, got %d", count) + } +} + +func TestDelete(t *testing.T) { + dir := t.TempDir() + store, err := storage.New(dir) + if err != nil { + t.Fatal(err) + } + + raw := []byte("From: alice@example.com\r\n\r\nDelete me") + id, _ := store.Save(raw, time.Now()) + + if err := store.Delete(id); err != nil { + t.Fatalf("Delete: %v", err) + } + if _, err := store.Load(id); err == nil { + t.Error("Load after Delete should return error") + } +} + +func TestStats(t *testing.T) { + dir := t.TempDir() + store, err := storage.New(dir) + if err != nil { + t.Fatal(err) + } + + mails := [][]byte{ + []byte("From: a@x.com\r\n\r\nMail 1"), + []byte("From: b@x.com\r\n\r\nMail 2"), + []byte("From: c@x.com\r\n\r\nMail 3"), + } + for _, m := range mails { + store.Save(m, time.Now()) + } + + stats, err := store.Stats() + if err != nil { + t.Fatalf("Stats: %v", err) + } + if stats.TotalMails != 3 { + t.Errorf("expected 3 mails, got %d", stats.TotalMails) + } + if stats.TotalBytes <= 0 { + t.Error("expected positive TotalBytes") + } +} + +func TestStorageDirectoryCreation(t *testing.T) { + dir := filepath.Join(t.TempDir(), "nested", "path") + _, err := storage.New(dir) + if err != nil { + t.Fatalf("New with nested path: %v", err) + } + for _, sub := range []string{"store", "attachments", "meta"} { + if _, err := os.Stat(filepath.Join(dir, sub)); os.IsNotExist(err) { + t.Errorf("expected subdirectory %s to be created", sub) + } + } +} diff --git a/userstore_test.go b/userstore_test.go new file mode 100644 index 0000000..fd06947 --- /dev/null +++ b/userstore_test.go @@ -0,0 +1,245 @@ +package userstore_test + +import ( + "path/filepath" + "testing" + "time" + + "github.com/mailarchive/internal/userstore" +) + +func newTestStore(t *testing.T) *userstore.Store { + t.Helper() + s, err := userstore.New(filepath.Join(t.TempDir(), "users.db")) + if err != nil { + t.Fatalf("userstore.New: %v", err) + } + t.Cleanup(func() { s.Close() }) + return s +} + +func TestCreateAndGetUser(t *testing.T) { + s := newTestStore(t) + + u, err := s.Create(userstore.CreateUserRequest{ + Username: "alice", + Email: "alice@example.com", + Password: "secret123", + Role: userstore.RoleAdmin, + }) + if err != nil { + t.Fatalf("Create: %v", err) + } + if u.ID == 0 { + t.Error("expected non-zero ID") + } + if u.Username != "alice" { + t.Errorf("Username = %q", u.Username) + } + if u.Role != userstore.RoleAdmin { + t.Errorf("Role = %q", u.Role) + } + if u.Source != "local" { + t.Errorf("Source = %q, want local", u.Source) + } + + got, err := s.GetByID(u.ID) + if err != nil { + t.Fatalf("GetByID: %v", err) + } + if got.Email != "alice@example.com" { + t.Errorf("Email = %q", got.Email) + } +} + +func TestVerifyPassword(t *testing.T) { + s := newTestStore(t) + _, err := s.Create(userstore.CreateUserRequest{ + Username: "bob", Email: "bob@example.com", + Password: "correcthorse", Role: userstore.RoleUser, + }) + if err != nil { + t.Fatal(err) + } + + // Correct password + u, err := s.VerifyPassword("bob", "correcthorse") + if err != nil { + t.Errorf("VerifyPassword correct: %v", err) + } + if u.Username != "bob" { + t.Errorf("Username = %q", u.Username) + } + + // Wrong password + if _, err := s.VerifyPassword("bob", "wrongpassword"); err == nil { + t.Error("expected error for wrong password") + } + + // Non-existent user + if _, err := s.VerifyPassword("nobody", "x"); err == nil { + t.Error("expected error for unknown user") + } +} + +func TestUpdateUser(t *testing.T) { + s := newTestStore(t) + u, _ := s.Create(userstore.CreateUserRequest{ + Username: "carol", Email: "carol@old.com", + Password: "pw", Role: userstore.RoleUser, + }) + + newEmail := "carol@new.com" + newRole := userstore.RoleAuditor + updated, err := s.Update(u.ID, userstore.UpdateUserRequest{ + Email: &newEmail, + Role: &newRole, + }) + if err != nil { + t.Fatalf("Update: %v", err) + } + if updated.Email != "carol@new.com" { + t.Errorf("Email after update = %q", updated.Email) + } + if updated.Role != userstore.RoleAuditor { + t.Errorf("Role after update = %q", updated.Role) + } +} + +func TestDisableUser(t *testing.T) { + s := newTestStore(t) + u, _ := s.Create(userstore.CreateUserRequest{ + Username: "dave", Email: "dave@x.com", + Password: "pw", Role: userstore.RoleUser, + }) + + active := false + s.Update(u.ID, userstore.UpdateUserRequest{Active: &active}) + + if _, err := s.VerifyPassword("dave", "pw"); err == nil { + t.Error("disabled user should not be able to login") + } +} + +func TestDeleteUser(t *testing.T) { + s := newTestStore(t) + u, _ := s.Create(userstore.CreateUserRequest{ + Username: "eve", Email: "eve@x.com", + Password: "pw", Role: userstore.RoleUser, + }) + + if err := s.Delete(u.ID); err != nil { + t.Fatalf("Delete: %v", err) + } + if _, err := s.GetByID(u.ID); err == nil { + t.Error("GetByID should error after delete") + } + // Delete non-existent should error + if err := s.Delete(u.ID); err == nil { + t.Error("second delete should return error") + } +} + +func TestListUsers(t *testing.T) { + s := newTestStore(t) + users := []userstore.CreateUserRequest{ + {Username: "u1", Email: "u1@x.com", Password: "pw", Role: userstore.RoleUser}, + {Username: "u2", Email: "u2@x.com", Password: "pw", Role: userstore.RoleAdmin}, + {Username: "u3", Email: "u3@x.com", Password: "pw", Role: userstore.RoleAuditor}, + {Username: "u4", Email: "u4@x.com", Password: "pw", Role: userstore.RoleUser}, + } + for _, req := range users { + s.Create(req) + } + + all, err := s.List("") + if err != nil { + t.Fatalf("List all: %v", err) + } + if len(all) != 4 { + t.Errorf("List all: got %d, want 4", len(all)) + } + + admins, _ := s.List(userstore.RoleAdmin) + if len(admins) != 1 { + t.Errorf("List admin: got %d, want 1", len(admins)) + } + + regular, _ := s.List(userstore.RoleUser) + if len(regular) != 2 { + t.Errorf("List user: got %d, want 2", len(regular)) + } +} + +func TestTokenBlacklist(t *testing.T) { + s := newTestStore(t) + + jti := "test-jti-12345" + expires := time.Now().Add(1 * time.Hour) + + if err := s.BlacklistToken(jti, expires); err != nil { + t.Fatalf("BlacklistToken: %v", err) + } + + blacklisted, err := s.IsBlacklisted(jti) + if err != nil { + t.Fatalf("IsBlacklisted: %v", err) + } + if !blacklisted { + t.Error("token should be blacklisted") + } + + // Non-blacklisted token + bl2, _ := s.IsBlacklisted("other-jti") + if bl2 { + t.Error("unknown token should not be blacklisted") + } +} + +func TestCleanExpiredTokens(t *testing.T) { + s := newTestStore(t) + + // Add an already-expired token + s.BlacklistToken("expired-jti", time.Now().Add(-1*time.Hour)) + // Add a valid token + s.BlacklistToken("valid-jti", time.Now().Add(1*time.Hour)) + + if err := s.CleanExpiredTokens(); err != nil { + t.Fatalf("CleanExpiredTokens: %v", err) + } + + bl, _ := s.IsBlacklisted("expired-jti") + if bl { + t.Error("expired token should be cleaned up") + } + bl2, _ := s.IsBlacklisted("valid-jti") + if !bl2 { + t.Error("valid token should still be blacklisted") + } +} + +func TestUpsertLDAPUser(t *testing.T) { + s := newTestStore(t) + + u, err := s.UpsertLDAPUser("ldapuser", "ldap@corp.com", userstore.RoleAuditor) + if err != nil { + t.Fatalf("UpsertLDAPUser: %v", err) + } + if u.Source != "ldap" { + t.Errorf("Source = %q, want ldap", u.Source) + } + + // Second upsert should update, not duplicate + u2, err := s.UpsertLDAPUser("ldapuser", "ldap@corp.com", userstore.RoleAuditor) + if err != nil { + t.Fatalf("UpsertLDAPUser second: %v", err) + } + if u2.ID != u.ID { + t.Error("second upsert should not create a new record") + } + + all, _ := s.List("") + if len(all) != 1 { + t.Errorf("expected 1 user after double upsert, got %d", len(all)) + } +}