Files
mailarchiv/api_test.go
T
2026-03-12 16:26:32 +01:00

221 lines
5.9 KiB
Go

package api_test
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"log/slog"
"os"
"github.com/mailarchive/config"
"github.com/mailarchive/internal/api"
"github.com/mailarchive/internal/audit"
"github.com/mailarchive/internal/auth"
"github.com/mailarchive/internal/index"
"github.com/mailarchive/internal/storage"
"github.com/mailarchive/internal/userstore"
)
type testEnv struct {
server *api.Server
users *userstore.Store
store *storage.Store
idx index.Indexer
}
func newTestEnv(t *testing.T) *testEnv {
t.Helper()
dir := t.TempDir()
logger := slog.New(slog.NewTextHandler(os.Discard, nil))
store, err := storage.New(filepath.Join(dir, "store"))
if err != nil { t.Fatal(err) }
idx, err := index.New(filepath.Join(dir, "index"), 100, "bleve")
if err != nil { t.Fatal(err) }
users, err := userstore.New(filepath.Join(dir, "users.db"))
if err != nil { t.Fatal(err) }
audlog, err := audit.New(filepath.Join(dir, "audit.db"), dir, logger)
if err != nil { t.Fatal(err) }
// Seed users
users.Create(userstore.CreateUserRequest{Username: "admin", Email: "admin@x.com", Password: "adminpass", Role: userstore.RoleAdmin})
users.Create(userstore.CreateUserRequest{Username: "auditor", Email: "auditor@x.com", Password: "auditorpass", Role: userstore.RoleAuditor})
users.Create(userstore.CreateUserRequest{Username: "user1", Email: "user1@x.com", Password: "userpass", Role: userstore.RoleUser})
authMgr := auth.New(users, nil, "test-secret-must-be-long-enough-32")
cfg := config.APIConfig{Bind: ":18080", Secret: "test-secret-must-be-long-enough-32"}
srv := api.New(cfg, store, idx, authMgr, users, audlog, logger)
t.Cleanup(func() {
idx.Close()
users.Close()
audlog.Close()
})
return &testEnv{server: srv, users: users, store: store, idx: idx}
}
func (e *testEnv) do(t *testing.T, method, path string, body interface{}, token string) *httptest.ResponseRecorder {
t.Helper()
var buf bytes.Buffer
if body != nil {
json.NewEncoder(&buf).Encode(body)
}
req := httptest.NewRequest(method, path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
w := httptest.NewRecorder()
e.server.ServeHTTP(w, req)
return w
}
func (e *testEnv) login(t *testing.T, username, password string) string {
t.Helper()
w := e.do(t, "POST", "/api/auth/login",
map[string]string{"username": username, "password": password}, "")
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)
}
// ---- Tests ----
func TestHealth(t *testing.T) {
env := newTestEnv(t)
w := env.do(t, "GET", "/api/health", nil, "")
if w.Code != 200 {
t.Errorf("health: status %d", w.Code)
}
}
func TestLoginAndMe(t *testing.T) {
env := newTestEnv(t)
token := env.login(t, "admin", "adminpass")
w := env.do(t, "GET", "/api/auth/me", nil, token)
if w.Code != 200 {
t.Fatalf("me: status %d", w.Code)
}
var resp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &resp)
if resp["username"] != "admin" {
t.Errorf("me username = %q", resp["username"])
}
if resp["role"] != "admin" {
t.Errorf("me role = %q", resp["role"])
}
}
func TestLoginWrongCredentials(t *testing.T) {
env := newTestEnv(t)
w := env.do(t, "POST", "/api/auth/login",
map[string]string{"username": "admin", "password": "wrong"}, "")
if w.Code != 401 {
t.Errorf("expected 401, got %d", w.Code)
}
}
func TestUnauthenticatedSearchBlocked(t *testing.T) {
env := newTestEnv(t)
w := env.do(t, "GET", "/api/search?q=test", nil, "")
if w.Code != 401 {
t.Errorf("expected 401, got %d", w.Code)
}
}
func TestLogout(t *testing.T) {
env := newTestEnv(t)
token := env.login(t, "admin", "adminpass")
w := env.do(t, "POST", "/api/auth/logout", nil, token)
if w.Code != 200 {
t.Fatalf("logout: status %d", w.Code)
}
// Token should now be invalid
w2 := env.do(t, "GET", "/api/auth/me", nil, token)
if w2.Code != 401 {
t.Errorf("after logout, me should return 401, got %d", w2.Code)
}
}
func TestAdminUserCRUD(t *testing.T) {
env := newTestEnv(t)
token := env.login(t, "admin", "adminpass")
// List users
w := env.do(t, "GET", "/api/users", nil, token)
if w.Code != 200 {
t.Fatalf("list users: status %d", w.Code)
}
var users []map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &users)
if len(users) != 3 { // admin + auditor + user1
t.Errorf("expected 3 users, got %d", len(users))
}
// Create user
w = env.do(t, "POST", "/api/users",
map[string]string{"username": "newuser", "email": "new@x.com", "password": "pw123", "role": "user"},
token)
if w.Code != 201 {
t.Fatalf("create user: status %d, body: %s", w.Code, w.Body.String())
}
}
func TestNonAdminCannotManageUsers(t *testing.T) {
env := newTestEnv(t)
token := env.login(t, "user1", "userpass")
w := env.do(t, "GET", "/api/users", nil, token)
if w.Code != 403 {
t.Errorf("user role should not list users, got %d", w.Code)
}
}
func TestAuditorCanAccessAuditLog(t *testing.T) {
env := newTestEnv(t)
token := env.login(t, "auditor", "auditorpass")
w := env.do(t, "GET", "/api/audit", nil, token)
if w.Code != 200 {
t.Errorf("auditor should access audit log, got %d", w.Code)
}
}
func TestUserCannotAccessAuditLog(t *testing.T) {
env := newTestEnv(t)
token := env.login(t, "user1", "userpass")
w := env.do(t, "GET", "/api/audit", nil, token)
if w.Code != 403 {
t.Errorf("user role should not access audit log, got %d", w.Code)
}
}
func TestSearchReturnsResults(t *testing.T) {
env := newTestEnv(t)
token := env.login(t, "admin", "adminpass")
w := env.do(t, "GET", "/api/search?q=test", nil, token)
if w.Code != 200 {
t.Fatalf("search: status %d, body: %s", w.Code, w.Body.String())
}
var result map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &result)
if _, ok := result["total"]; !ok {
t.Error("search response missing 'total' field")
}
}