package server import ( "context" "encoding/json" "net/http" "net/http/httptest" "path/filepath" "strings" "testing" "log/slog" "github.com/go-chi/chi/v5" "git.wntrmute.dev/kyle/metacrypt/internal/barrier" "git.wntrmute.dev/kyle/metacrypt/internal/config" "git.wntrmute.dev/kyle/metacrypt/internal/crypto" "git.wntrmute.dev/kyle/metacrypt/internal/db" "git.wntrmute.dev/kyle/metacrypt/internal/engine" "git.wntrmute.dev/kyle/metacrypt/internal/policy" "git.wntrmute.dev/kyle/metacrypt/internal/seal" // auth is used indirectly via the server "git.wntrmute.dev/kyle/metacrypt/internal/auth" ) func setupTestServer(t *testing.T) (*Server, *seal.Manager, chi.Router) { t.Helper() dir := t.TempDir() database, err := db.Open(filepath.Join(dir, "test.db")) if err != nil { t.Fatalf("open db: %v", err) } t.Cleanup(func() { database.Close() }) db.Migrate(database) b := barrier.NewAESGCMBarrier(database) sealMgr := seal.NewManager(database, b, slog.Default()) sealMgr.CheckInitialized() // Auth requires MCIAS client which we can't create in tests easily, // so we pass nil and avoid auth-dependent routes in these tests. authenticator := auth.NewAuthenticator(nil, slog.Default()) policyEngine := policy.NewEngine(b) engineRegistry := engine.NewRegistry(b, slog.Default()) cfg := &config.Config{ Server: config.ServerConfig{ ListenAddr: ":0", TLSCert: "cert.pem", TLSKey: "key.pem", }, Database: config.DatabaseConfig{Path: filepath.Join(dir, "test.db")}, MCIAS: config.MCIASConfig{ServerURL: "https://mcias.test"}, Seal: config.SealConfig{ Argon2Time: 1, Argon2Memory: 64 * 1024, Argon2Threads: 1, }, } logger := slog.Default() srv := New(cfg, sealMgr, authenticator, policyEngine, engineRegistry, logger, "test") r := chi.NewRouter() srv.registerRoutes(r) return srv, sealMgr, r } func TestStatusEndpoint(t *testing.T) { _, _, mux := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/v1/status", nil) w := httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("status code: got %d, want %d", w.Code, http.StatusOK) } var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) if resp["state"] != "uninitialized" { t.Errorf("state: got %q, want %q", resp["state"], "uninitialized") } } func TestInitEndpoint(t *testing.T) { _, _, mux := setupTestServer(t) body := `{"password":"test-password"}` req := httptest.NewRequest(http.MethodPost, "/v1/init", strings.NewReader(body)) w := httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("status code: got %d, want %d. Body: %s", w.Code, http.StatusOK, w.Body.String()) } var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) if resp["state"] != "unsealed" { t.Errorf("state: got %q, want %q", resp["state"], "unsealed") } // Second init should fail. req2 := httptest.NewRequest(http.MethodPost, "/v1/init", strings.NewReader(body)) w2 := httptest.NewRecorder() mux.ServeHTTP(w2, req2) if w2.Code != http.StatusConflict { t.Errorf("double init: got %d, want %d", w2.Code, http.StatusConflict) } } func TestUnsealEndpoint(t *testing.T) { _, sealMgr, mux := setupTestServer(t) // Initialize first. params := crypto.Argon2Params{Time: 1, Memory: 64 * 1024, Threads: 1} sealMgr.Initialize(context.Background(), []byte("password"), params) sealMgr.Seal() // Unseal with wrong password. body := `{"password":"wrong"}` req := httptest.NewRequest(http.MethodPost, "/v1/unseal", strings.NewReader(body)) w := httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Errorf("wrong password: got %d, want %d", w.Code, http.StatusUnauthorized) } // Unseal with correct password. body = `{"password":"password"}` req = httptest.NewRequest(http.MethodPost, "/v1/unseal", strings.NewReader(body)) w = httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("correct password: got %d, want %d. Body: %s", w.Code, http.StatusOK, w.Body.String()) } } func TestStatusMethodNotAllowed(t *testing.T) { _, _, mux := setupTestServer(t) req := httptest.NewRequest(http.MethodPost, "/v1/status", nil) w := httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusMethodNotAllowed { t.Errorf("POST /v1/status: got %d, want %d", w.Code, http.StatusMethodNotAllowed) } } func TestRootNotFound(t *testing.T) { _, _, mux := setupTestServer(t) // The vault server no longer serves a web UI at /; that is handled by metacrypt-web. req := httptest.NewRequest(http.MethodGet, "/", nil) w := httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Errorf("root: got %d, want %d", w.Code, http.StatusNotFound) } } func TestTokenInfoFromContext(t *testing.T) { ctx := context.Background() if info := TokenInfoFromContext(ctx); info != nil { t.Error("expected nil from empty context") } info := &auth.TokenInfo{Username: "test", IsAdmin: true} ctx = context.WithValue(ctx, tokenInfoKey, info) got := TokenInfoFromContext(ctx) if got == nil || got.Username != "test" { t.Error("expected token info from context") } }