package handler import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/shankar0123/certctl/internal/api/middleware" ) func TestHealth_ReturnsOK(t *testing.T) { handler := NewHealthHandler("api-key") req, err := http.NewRequest(http.MethodGet, "/health", nil) if err != nil { t.Fatalf("NewRequest failed: %v", err) } w := httptest.NewRecorder() handler.Health(w, req) if status := w.Code; status != http.StatusOK { t.Errorf("Health handler returned status %d, want %d", status, http.StatusOK) } // Check content type if ct := w.Header().Get("Content-Type"); ct != "application/json" { t.Errorf("Content-Type = %q, want application/json", ct) } // Check response body var result map[string]string if err := json.NewDecoder(w.Body).Decode(&result); err != nil { t.Fatalf("failed to decode response: %v", err) } if result["status"] != "healthy" { t.Errorf("status = %q, want healthy", result["status"]) } } func TestHealth_MethodNotAllowed(t *testing.T) { handler := NewHealthHandler("api-key") req, err := http.NewRequest(http.MethodPost, "/health", nil) if err != nil { t.Fatalf("NewRequest failed: %v", err) } w := httptest.NewRecorder() handler.Health(w, req) if status := w.Code; status != http.StatusMethodNotAllowed { t.Errorf("Health handler returned status %d, want %d", status, http.StatusMethodNotAllowed) } } func TestReady_ReturnsOK(t *testing.T) { handler := NewHealthHandler("api-key") req, err := http.NewRequest(http.MethodGet, "/ready", nil) if err != nil { t.Fatalf("NewRequest failed: %v", err) } w := httptest.NewRecorder() handler.Ready(w, req) if status := w.Code; status != http.StatusOK { t.Errorf("Ready handler returned status %d, want %d", status, http.StatusOK) } // Check content type if ct := w.Header().Get("Content-Type"); ct != "application/json" { t.Errorf("Content-Type = %q, want application/json", ct) } // Check response body var result map[string]string if err := json.NewDecoder(w.Body).Decode(&result); err != nil { t.Fatalf("failed to decode response: %v", err) } if result["status"] != "ready" { t.Errorf("status = %q, want ready", result["status"]) } } func TestReady_MethodNotAllowed(t *testing.T) { handler := NewHealthHandler("api-key") req, err := http.NewRequest(http.MethodDelete, "/ready", nil) if err != nil { t.Fatalf("NewRequest failed: %v", err) } w := httptest.NewRecorder() handler.Ready(w, req) if status := w.Code; status != http.StatusMethodNotAllowed { t.Errorf("Ready handler returned status %d, want %d", status, http.StatusMethodNotAllowed) } } func TestAuthInfo_ReturnsAuthType_APIKey(t *testing.T) { handler := NewHealthHandler("api-key") req, err := http.NewRequest(http.MethodGet, "/api/v1/auth/info", nil) if err != nil { t.Fatalf("NewRequest failed: %v", err) } w := httptest.NewRecorder() handler.AuthInfo(w, req) if status := w.Code; status != http.StatusOK { t.Errorf("AuthInfo handler returned status %d, want %d", status, http.StatusOK) } var result map[string]interface{} if err := json.NewDecoder(w.Body).Decode(&result); err != nil { t.Fatalf("failed to decode response: %v", err) } if result["auth_type"] != "api-key" { t.Errorf("auth_type = %q, want api-key", result["auth_type"]) } if required, ok := result["required"].(bool); !ok || !required { t.Errorf("required = %v, want true", result["required"]) } } func TestAuthInfo_ReturnsAuthType_None(t *testing.T) { handler := NewHealthHandler("none") req, err := http.NewRequest(http.MethodGet, "/api/v1/auth/info", nil) if err != nil { t.Fatalf("NewRequest failed: %v", err) } w := httptest.NewRecorder() handler.AuthInfo(w, req) if status := w.Code; status != http.StatusOK { t.Errorf("AuthInfo handler returned status %d, want %d", status, http.StatusOK) } var result map[string]interface{} if err := json.NewDecoder(w.Body).Decode(&result); err != nil { t.Fatalf("failed to decode response: %v", err) } if result["auth_type"] != "none" { t.Errorf("auth_type = %q, want none", result["auth_type"]) } if required, ok := result["required"].(bool); !ok || required { t.Errorf("required = %v, want false", result["required"]) } } func TestAuthInfo_ReturnsAuthType_JWT(t *testing.T) { handler := NewHealthHandler("jwt") req, err := http.NewRequest(http.MethodGet, "/api/v1/auth/info", nil) if err != nil { t.Fatalf("NewRequest failed: %v", err) } w := httptest.NewRecorder() handler.AuthInfo(w, req) var result map[string]interface{} if err := json.NewDecoder(w.Body).Decode(&result); err != nil { t.Fatalf("failed to decode response: %v", err) } if result["auth_type"] != "jwt" { t.Errorf("auth_type = %q, want jwt", result["auth_type"]) } if required, ok := result["required"].(bool); !ok || !required { t.Errorf("required = %v, want true", result["required"]) } } func TestAuthCheck_ReturnsOK(t *testing.T) { handler := NewHealthHandler("api-key") req, err := http.NewRequest(http.MethodGet, "/api/v1/auth/check", nil) if err != nil { t.Fatalf("NewRequest failed: %v", err) } w := httptest.NewRecorder() handler.AuthCheck(w, req) if status := w.Code; status != http.StatusOK { t.Errorf("AuthCheck handler returned status %d, want %d", status, http.StatusOK) } // Check content type if ct := w.Header().Get("Content-Type"); ct != "application/json" { t.Errorf("Content-Type = %q, want application/json", ct) } // Check response body — mixed-value map (string + bool) post-Phase B.4. var result map[string]any if err := json.NewDecoder(w.Body).Decode(&result); err != nil { t.Fatalf("failed to decode response: %v", err) } if result["status"] != "authenticated" { t.Errorf("status = %q, want authenticated", result["status"]) } } func TestAuthCheck_MethodNotAllowed(t *testing.T) { handler := NewHealthHandler("api-key") req, err := http.NewRequest(http.MethodPost, "/api/v1/auth/check", nil) if err != nil { t.Fatalf("NewRequest failed: %v", err) } w := httptest.NewRecorder() handler.AuthCheck(w, req) // AuthCheck doesn't explicitly check method, so it will return 200 // But let's verify the response is still correct if status := w.Code; status != http.StatusOK { t.Logf("AuthCheck returned status %d (note: method not enforced in handler)", status) } } // --- M-003 (Phase B.4): /auth/check surfaces admin flag + user identity --- // TestAuthCheck_AdminCaller_ReportsAdminTrue confirms that when the auth // middleware sets AdminKey{}=true (i.e., named key was admin-tagged), the // /auth/check endpoint reports admin=true so the GUI can show admin-only // affordances. func TestAuthCheck_AdminCaller_ReportsAdminTrue(t *testing.T) { handler := NewHealthHandler("api-key") req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/check", nil) ctx := context.WithValue(req.Context(), middleware.AdminKey{}, true) ctx = context.WithValue(ctx, middleware.UserKey{}, "ops-admin") req = req.WithContext(ctx) w := httptest.NewRecorder() handler.AuthCheck(w, req) if w.Code != http.StatusOK { t.Fatalf("expected status 200, got %d", w.Code) } var result map[string]any if err := json.NewDecoder(w.Body).Decode(&result); err != nil { t.Fatalf("failed to decode response: %v", err) } if result["status"] != "authenticated" { t.Errorf("status = %q, want authenticated", result["status"]) } admin, ok := result["admin"].(bool) if !ok { t.Fatalf("admin field missing or wrong type: %T", result["admin"]) } if !admin { t.Errorf("admin = false, want true") } if result["user"] != "ops-admin" { t.Errorf("user = %q, want ops-admin", result["user"]) } } // TestAuthCheck_NonAdminCaller_ReportsAdminFalse pins the negative case: the // auth middleware has stored AdminKey{}=false (non-admin named key) — the // endpoint must report admin=false so the GUI hides admin-only affordances. func TestAuthCheck_NonAdminCaller_ReportsAdminFalse(t *testing.T) { handler := NewHealthHandler("api-key") req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/check", nil) ctx := context.WithValue(req.Context(), middleware.AdminKey{}, false) ctx = context.WithValue(ctx, middleware.UserKey{}, "alice") req = req.WithContext(ctx) w := httptest.NewRecorder() handler.AuthCheck(w, req) if w.Code != http.StatusOK { t.Fatalf("expected status 200, got %d", w.Code) } var result map[string]any if err := json.NewDecoder(w.Body).Decode(&result); err != nil { t.Fatalf("failed to decode response: %v", err) } admin, ok := result["admin"].(bool) if !ok { t.Fatalf("admin field missing or wrong type: %T", result["admin"]) } if admin { t.Errorf("admin = true, want false") } if result["user"] != "alice" { t.Errorf("user = %q, want alice", result["user"]) } } // TestAuthCheck_NoAuthContext_DefaultsToEmptyUserAndFalseAdmin covers the // CERTCTL_AUTH_TYPE=none deployment, where the auth middleware doesn't set // any keys. Response must still be well-formed with empty user + admin=false. func TestAuthCheck_NoAuthContext_DefaultsToEmptyUserAndFalseAdmin(t *testing.T) { handler := NewHealthHandler("none") req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/check", nil) w := httptest.NewRecorder() handler.AuthCheck(w, req) if w.Code != http.StatusOK { t.Fatalf("expected status 200, got %d", w.Code) } var result map[string]any if err := json.NewDecoder(w.Body).Decode(&result); err != nil { t.Fatalf("failed to decode response: %v", err) } if result["status"] != "authenticated" { t.Errorf("status = %q, want authenticated", result["status"]) } admin, ok := result["admin"].(bool) if !ok { t.Fatalf("admin field missing or wrong type: %T", result["admin"]) } if admin { t.Errorf("admin = true for no-auth context, want false") } if result["user"] != "" { t.Errorf("user = %q, want empty string", result["user"]) } }