From d97005553296cbedfc64033f1a61f04df4bb7456 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Tue, 2 Dec 2025 13:09:48 +0000 Subject: [PATCH] test: Add RequireAuth tests for API package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive tests for the RequireAuth middleware covering: - No auth configured (allows access by design) - API-only mode (rejects requests without token) - API-only mode (accepts valid X-API-Token) - Basic auth with invalid credentials - Basic auth JSON vs plain text error responses - Valid basic auth (allowed) - Proxy auth (allowed) - Proxy auth with invalid secret (rejected) - Bearer token with basic auth configured (allowed) - Invalid Bearer token (rejected) Coverage: RequireAuth 7.1% → 78.6% Coverage: CheckAuth 66.9% → 69.1% Coverage: API package 31.9% → 32.1% --- internal/api/security_test.go | 309 ++++++++++++++++++++++++++++++++++ 1 file changed, 309 insertions(+) diff --git a/internal/api/security_test.go b/internal/api/security_test.go index f77a981..0332ae2 100644 --- a/internal/api/security_test.go +++ b/internal/api/security_test.go @@ -1793,3 +1793,312 @@ func TestRequireAdmin_NoProxyAuthAuthenticatedAllowed(t *testing.T) { t.Errorf("RequireAdmin returned status %d, want %d", w.Code, http.StatusOK) } } + +// RequireAuth tests + +func TestRequireAuth_NoAuthConfiguredAllowsAccess(t *testing.T) { + // When no auth is configured at all, CheckAuth returns true (allows access) + cfg := &config.Config{} + handlerCalled := false + handler := RequireAuth(cfg, func(w http.ResponseWriter, r *http.Request) { + handlerCalled = true + w.WriteHeader(http.StatusOK) + }) + + w := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/api/test", nil) + handler(w, req) + + if !handlerCalled { + t.Error("RequireAuth should call handler when no auth is configured") + } + if w.Code != http.StatusOK { + t.Errorf("RequireAuth returned status %d, want %d", w.Code, http.StatusOK) + } +} + +func TestRequireAuth_APIOnlyModeRejectsNoToken(t *testing.T) { + // When only API tokens are configured, requests without token should be rejected + rawToken := "test-api-token-12345" + record, _ := config.NewAPITokenRecord(rawToken, "test-token", []string{"read"}) + cfg := &config.Config{ + APITokens: []config.APITokenRecord{*record}, + } + + handlerCalled := false + handler := RequireAuth(cfg, func(w http.ResponseWriter, r *http.Request) { + handlerCalled = true + }) + + w := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/api/test", nil) + // No token provided + handler(w, req) + + if handlerCalled { + t.Error("RequireAuth should not call handler without API token in API-only mode") + } + if w.Code != http.StatusUnauthorized { + t.Errorf("RequireAuth returned status %d, want %d", w.Code, http.StatusUnauthorized) + } +} + +func TestRequireAuth_APIOnlyModeAcceptsValidToken(t *testing.T) { + rawToken := "test-api-token-12345" + record, _ := config.NewAPITokenRecord(rawToken, "test-token", []string{"read"}) + cfg := &config.Config{ + APITokens: []config.APITokenRecord{*record}, + } + + handlerCalled := false + handler := RequireAuth(cfg, func(w http.ResponseWriter, r *http.Request) { + handlerCalled = true + w.WriteHeader(http.StatusOK) + }) + + w := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/api/test", nil) + req.Header.Set("X-API-Token", rawToken) + handler(w, req) + + if !handlerCalled { + t.Error("RequireAuth should call handler with valid API token") + } + if w.Code != http.StatusOK { + t.Errorf("RequireAuth returned status %d, want %d", w.Code, http.StatusOK) + } +} + +func TestRequireAuth_InvalidBasicAuthRejectsRequest(t *testing.T) { + hashedPass, _ := auth.HashPassword("password123") + cfg := &config.Config{ + AuthUser: "testuser", + AuthPass: hashedPass, + } + + handlerCalled := false + handler := RequireAuth(cfg, func(w http.ResponseWriter, r *http.Request) { + handlerCalled = true + }) + + w := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/api/test", nil) + req.SetBasicAuth("testuser", "wrongpassword") + handler(w, req) + + if handlerCalled { + t.Error("RequireAuth should not call handler with invalid credentials") + } + if w.Code != http.StatusUnauthorized { + t.Errorf("RequireAuth returned status %d, want %d", w.Code, http.StatusUnauthorized) + } +} + +func TestRequireAuth_InvalidBasicAuthAPIPathReturnsJSON(t *testing.T) { + hashedPass, _ := auth.HashPassword("password123") + cfg := &config.Config{ + AuthUser: "testuser", + AuthPass: hashedPass, + } + + handler := RequireAuth(cfg, func(w http.ResponseWriter, r *http.Request) {}) + + w := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/api/test", nil) + req.SetBasicAuth("testuser", "wrongpassword") + handler(w, req) + + if w.Code != http.StatusUnauthorized { + t.Errorf("RequireAuth returned status %d, want %d", w.Code, http.StatusUnauthorized) + } + if ct := w.Header().Get("Content-Type"); ct != "application/json" { + t.Errorf("RequireAuth Content-Type = %q, want application/json", ct) + } + if body := w.Body.String(); !strings.Contains(body, "Authentication required") { + t.Errorf("RequireAuth body = %q, want to contain 'Authentication required'", body) + } +} + +func TestRequireAuth_InvalidBasicAuthAcceptJSONReturnsJSON(t *testing.T) { + hashedPass, _ := auth.HashPassword("password123") + cfg := &config.Config{ + AuthUser: "testuser", + AuthPass: hashedPass, + } + + handler := RequireAuth(cfg, func(w http.ResponseWriter, r *http.Request) {}) + + w := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/test", nil) + req.Header.Set("Accept", "application/json") + req.SetBasicAuth("testuser", "wrongpassword") + handler(w, req) + + if w.Code != http.StatusUnauthorized { + t.Errorf("RequireAuth returned status %d, want %d", w.Code, http.StatusUnauthorized) + } + if ct := w.Header().Get("Content-Type"); ct != "application/json" { + t.Errorf("RequireAuth Content-Type = %q, want application/json", ct) + } +} + +func TestRequireAuth_InvalidBasicAuthNonAPIReturnsPlainText(t *testing.T) { + hashedPass, _ := auth.HashPassword("password123") + cfg := &config.Config{ + AuthUser: "testuser", + AuthPass: hashedPass, + } + + handler := RequireAuth(cfg, func(w http.ResponseWriter, r *http.Request) {}) + + w := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/test", nil) + req.SetBasicAuth("testuser", "wrongpassword") + handler(w, req) + + if w.Code != http.StatusUnauthorized { + t.Errorf("RequireAuth returned status %d, want %d", w.Code, http.StatusUnauthorized) + } + // Should be plain text error + body := w.Body.String() + if !strings.Contains(body, "Unauthorized") { + t.Errorf("RequireAuth body = %q, want to contain 'Unauthorized'", body) + } +} + +func TestRequireAuth_ValidBasicAuthAllowsAccess(t *testing.T) { + hashedPass, _ := auth.HashPassword("password123") + cfg := &config.Config{ + AuthUser: "testuser", + AuthPass: hashedPass, + } + + handlerCalled := false + handler := RequireAuth(cfg, func(w http.ResponseWriter, r *http.Request) { + handlerCalled = true + w.WriteHeader(http.StatusOK) + }) + + w := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/api/test", nil) + req.SetBasicAuth("testuser", "password123") + handler(w, req) + + if !handlerCalled { + t.Error("RequireAuth should call handler for basic auth authenticated user") + } + if w.Code != http.StatusOK { + t.Errorf("RequireAuth returned status %d, want %d", w.Code, http.StatusOK) + } +} + +func TestRequireAuth_ProxyAuthAllowsAccess(t *testing.T) { + cfg := &config.Config{ + ProxyAuthSecret: "secret123", + ProxyAuthUserHeader: "X-Remote-User", + } + + handlerCalled := false + handler := RequireAuth(cfg, func(w http.ResponseWriter, r *http.Request) { + handlerCalled = true + w.WriteHeader(http.StatusOK) + }) + + w := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/api/test", nil) + req.Header.Set("X-Proxy-Secret", "secret123") + req.Header.Set("X-Remote-User", "proxyuser") + handler(w, req) + + if !handlerCalled { + t.Error("RequireAuth should call handler for proxy authenticated user") + } + if w.Code != http.StatusOK { + t.Errorf("RequireAuth returned status %d, want %d", w.Code, http.StatusOK) + } +} + +func TestRequireAuth_ProxyAuthInvalidSecretRejects(t *testing.T) { + cfg := &config.Config{ + ProxyAuthSecret: "secret123", + ProxyAuthUserHeader: "X-Remote-User", + } + + handlerCalled := false + handler := RequireAuth(cfg, func(w http.ResponseWriter, r *http.Request) { + handlerCalled = true + }) + + w := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/api/test", nil) + req.Header.Set("X-Proxy-Secret", "wrong-secret") + req.Header.Set("X-Remote-User", "proxyuser") + handler(w, req) + + if handlerCalled { + t.Error("RequireAuth should not call handler with invalid proxy secret") + } + if w.Code != http.StatusUnauthorized { + t.Errorf("RequireAuth returned status %d, want %d", w.Code, http.StatusUnauthorized) + } +} + +func TestRequireAuth_BearerTokenAllowsAccess(t *testing.T) { + // Bearer tokens are only checked when basic auth is also configured + // (not in API-only mode) + rawToken := "test-bearer-token-12345" + record, _ := config.NewAPITokenRecord(rawToken, "bearer-token", []string{"read"}) + hashedPass, _ := auth.HashPassword("password123") + cfg := &config.Config{ + AuthUser: "testuser", + AuthPass: hashedPass, + APITokens: []config.APITokenRecord{*record}, + } + + handlerCalled := false + handler := RequireAuth(cfg, func(w http.ResponseWriter, r *http.Request) { + handlerCalled = true + w.WriteHeader(http.StatusOK) + }) + + w := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/api/test", nil) + req.Header.Set("Authorization", "Bearer "+rawToken) + handler(w, req) + + if !handlerCalled { + t.Error("RequireAuth should call handler with valid Bearer token") + } + if w.Code != http.StatusOK { + t.Errorf("RequireAuth returned status %d, want %d", w.Code, http.StatusOK) + } +} + +func TestRequireAuth_InvalidBearerTokenRejects(t *testing.T) { + // Bearer tokens are only checked when basic auth is also configured + rawToken := "test-bearer-token-12345" + record, _ := config.NewAPITokenRecord(rawToken, "bearer-token", []string{"read"}) + hashedPass, _ := auth.HashPassword("password123") + cfg := &config.Config{ + AuthUser: "testuser", + AuthPass: hashedPass, + APITokens: []config.APITokenRecord{*record}, + } + + handlerCalled := false + handler := RequireAuth(cfg, func(w http.ResponseWriter, r *http.Request) { + handlerCalled = true + }) + + w := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/api/test", nil) + req.Header.Set("Authorization", "Bearer invalid-token") + handler(w, req) + + if handlerCalled { + t.Error("RequireAuth should not call handler with invalid Bearer token") + } + if w.Code != http.StatusUnauthorized { + t.Errorf("RequireAuth returned status %d, want %d", w.Code, http.StatusUnauthorized) + } +}