test: Add RequireAdmin tests for API package
Add comprehensive tests for the RequireAdmin middleware covering: - No auth configured (allows access by design) - API-only mode (rejects requests without token) - Basic auth with invalid credentials - Proxy auth with admin role (allowed) - Proxy auth with non-admin role (forbidden) - Proxy auth with invalid secret (unauthorized) - Proxy auth without role header (defaults to admin) - Proxy auth with custom role separator - Proxy auth with spaces in roles (trimmed) - Basic auth authenticated users (allowed as admin) - JSON vs plain text error responses based on path/Accept header Also improves CheckProxyAuth coverage as a side effect. Coverage: RequireAdmin 20.8% → 87.5% Coverage: CheckProxyAuth 0.0% → 89.3% Coverage: API package 30.9% → 31.9%
This commit is contained in:
parent
71e15886ed
commit
1073c511b3
1 changed files with 425 additions and 0 deletions
|
|
@ -7,6 +7,9 @@ import (
|
|||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/auth"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||
)
|
||||
|
||||
// fixedTimeForTest returns a fixed time for deterministic testing
|
||||
|
|
@ -1368,3 +1371,425 @@ func TestCheckCSRF_UnsafeMethods(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAdmin_NoAuthConfiguredAllowsAccess(t *testing.T) {
|
||||
// When no auth is configured at all, CheckAuth returns true (allows access)
|
||||
cfg := &config.Config{}
|
||||
handlerCalled := false
|
||||
handler := RequireAdmin(cfg, func(w http.ResponseWriter, r *http.Request) {
|
||||
handlerCalled = true
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/api/admin/test", nil)
|
||||
handler(w, req)
|
||||
|
||||
if !handlerCalled {
|
||||
t.Error("RequireAdmin should call handler when no auth is configured")
|
||||
}
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("RequireAdmin returned status %d, want %d", w.Code, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAdmin_APIOnlyModeRejectsNoToken(t *testing.T) {
|
||||
// When only API tokens are configured, requests without token should be rejected
|
||||
rawToken := "test-admin-token-12345"
|
||||
record, _ := config.NewAPITokenRecord(rawToken, "admin-token", []string{"admin"})
|
||||
cfg := &config.Config{
|
||||
APITokens: []config.APITokenRecord{*record},
|
||||
}
|
||||
|
||||
handlerCalled := false
|
||||
handler := RequireAdmin(cfg, func(w http.ResponseWriter, r *http.Request) {
|
||||
handlerCalled = true
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/api/admin/test", nil)
|
||||
// No token provided
|
||||
handler(w, req)
|
||||
|
||||
if handlerCalled {
|
||||
t.Error("RequireAdmin should not call handler without API token in API-only mode")
|
||||
}
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("RequireAdmin returned status %d, want %d", w.Code, http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAdmin_InvalidBasicAuthRejectsRequest(t *testing.T) {
|
||||
// When basic auth is configured, invalid credentials should be rejected
|
||||
hashedPass, _ := auth.HashPassword("password123")
|
||||
cfg := &config.Config{
|
||||
AuthUser: "testuser",
|
||||
AuthPass: hashedPass,
|
||||
}
|
||||
|
||||
handlerCalled := false
|
||||
handler := RequireAdmin(cfg, func(w http.ResponseWriter, r *http.Request) {
|
||||
handlerCalled = true
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/api/admin/test", nil)
|
||||
req.SetBasicAuth("testuser", "wrongpassword")
|
||||
handler(w, req)
|
||||
|
||||
if handlerCalled {
|
||||
t.Error("RequireAdmin should not call handler with invalid credentials")
|
||||
}
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("RequireAdmin returned status %d, want %d", w.Code, http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAdmin_InvalidBasicAuthAPIPathReturnsJSON(t *testing.T) {
|
||||
hashedPass, _ := auth.HashPassword("password123")
|
||||
cfg := &config.Config{
|
||||
AuthUser: "testuser",
|
||||
AuthPass: hashedPass,
|
||||
}
|
||||
|
||||
handler := RequireAdmin(cfg, func(w http.ResponseWriter, r *http.Request) {})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/api/admin/test", nil)
|
||||
req.SetBasicAuth("testuser", "wrongpassword")
|
||||
handler(w, req)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("RequireAdmin returned status %d, want %d", w.Code, http.StatusUnauthorized)
|
||||
}
|
||||
if ct := w.Header().Get("Content-Type"); ct != "application/json" {
|
||||
t.Errorf("RequireAdmin Content-Type = %q, want application/json", ct)
|
||||
}
|
||||
if body := w.Body.String(); !strings.Contains(body, "Authentication required") {
|
||||
t.Errorf("RequireAdmin body = %q, want to contain 'Authentication required'", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAdmin_InvalidBasicAuthAcceptJSONReturnsJSON(t *testing.T) {
|
||||
hashedPass, _ := auth.HashPassword("password123")
|
||||
cfg := &config.Config{
|
||||
AuthUser: "testuser",
|
||||
AuthPass: hashedPass,
|
||||
}
|
||||
|
||||
handler := RequireAdmin(cfg, func(w http.ResponseWriter, r *http.Request) {})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/admin/test", nil)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.SetBasicAuth("testuser", "wrongpassword")
|
||||
handler(w, req)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("RequireAdmin returned status %d, want %d", w.Code, http.StatusUnauthorized)
|
||||
}
|
||||
if ct := w.Header().Get("Content-Type"); ct != "application/json" {
|
||||
t.Errorf("RequireAdmin Content-Type = %q, want application/json", ct)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAdmin_InvalidBasicAuthNonAPIReturnsPlainText(t *testing.T) {
|
||||
hashedPass, _ := auth.HashPassword("password123")
|
||||
cfg := &config.Config{
|
||||
AuthUser: "testuser",
|
||||
AuthPass: hashedPass,
|
||||
}
|
||||
|
||||
handler := RequireAdmin(cfg, func(w http.ResponseWriter, r *http.Request) {})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/admin/test", nil)
|
||||
req.SetBasicAuth("testuser", "wrongpassword")
|
||||
handler(w, req)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("RequireAdmin 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("RequireAdmin body = %q, want to contain 'Unauthorized'", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAdmin_ProxyAuthAdminAllowed(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
ProxyAuthSecret: "secret123",
|
||||
ProxyAuthUserHeader: "X-Remote-User",
|
||||
ProxyAuthRoleHeader: "X-Remote-Roles",
|
||||
ProxyAuthAdminRole: "admin",
|
||||
}
|
||||
|
||||
handlerCalled := false
|
||||
handler := RequireAdmin(cfg, func(w http.ResponseWriter, r *http.Request) {
|
||||
handlerCalled = true
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/api/admin/test", nil)
|
||||
req.Header.Set("X-Proxy-Secret", "secret123")
|
||||
req.Header.Set("X-Remote-User", "admin-user")
|
||||
req.Header.Set("X-Remote-Roles", "admin|user")
|
||||
handler(w, req)
|
||||
|
||||
if !handlerCalled {
|
||||
t.Error("RequireAdmin should call handler for authenticated admin proxy user")
|
||||
}
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("RequireAdmin returned status %d, want %d", w.Code, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAdmin_ProxyAuthNonAdminForbidden(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
ProxyAuthSecret: "secret123",
|
||||
ProxyAuthUserHeader: "X-Remote-User",
|
||||
ProxyAuthRoleHeader: "X-Remote-Roles",
|
||||
ProxyAuthAdminRole: "admin",
|
||||
}
|
||||
|
||||
handlerCalled := false
|
||||
handler := RequireAdmin(cfg, func(w http.ResponseWriter, r *http.Request) {
|
||||
handlerCalled = true
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/api/admin/test", nil)
|
||||
req.Header.Set("X-Proxy-Secret", "secret123")
|
||||
req.Header.Set("X-Remote-User", "regular-user")
|
||||
req.Header.Set("X-Remote-Roles", "user|viewer") // No admin role
|
||||
handler(w, req)
|
||||
|
||||
if handlerCalled {
|
||||
t.Error("RequireAdmin should not call handler for non-admin proxy user")
|
||||
}
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Errorf("RequireAdmin returned status %d, want %d", w.Code, http.StatusForbidden)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAdmin_ProxyAuthNonAdminAPIPathReturnsJSON(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
ProxyAuthSecret: "secret123",
|
||||
ProxyAuthUserHeader: "X-Remote-User",
|
||||
ProxyAuthRoleHeader: "X-Remote-Roles",
|
||||
ProxyAuthAdminRole: "admin",
|
||||
}
|
||||
|
||||
handler := RequireAdmin(cfg, func(w http.ResponseWriter, r *http.Request) {})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/api/admin/test", nil)
|
||||
req.Header.Set("X-Proxy-Secret", "secret123")
|
||||
req.Header.Set("X-Remote-User", "regular-user")
|
||||
req.Header.Set("X-Remote-Roles", "user")
|
||||
handler(w, req)
|
||||
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Errorf("RequireAdmin returned status %d, want %d", w.Code, http.StatusForbidden)
|
||||
}
|
||||
if ct := w.Header().Get("Content-Type"); ct != "application/json" {
|
||||
t.Errorf("RequireAdmin Content-Type = %q, want application/json", ct)
|
||||
}
|
||||
if body := w.Body.String(); !strings.Contains(body, "Admin privileges required") {
|
||||
t.Errorf("RequireAdmin body = %q, want to contain 'Admin privileges required'", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAdmin_ProxyAuthNonAdminAcceptJSONReturnsJSON(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
ProxyAuthSecret: "secret123",
|
||||
ProxyAuthUserHeader: "X-Remote-User",
|
||||
ProxyAuthRoleHeader: "X-Remote-Roles",
|
||||
ProxyAuthAdminRole: "admin",
|
||||
}
|
||||
|
||||
handler := RequireAdmin(cfg, func(w http.ResponseWriter, r *http.Request) {})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/admin/test", nil)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("X-Proxy-Secret", "secret123")
|
||||
req.Header.Set("X-Remote-User", "regular-user")
|
||||
req.Header.Set("X-Remote-Roles", "user")
|
||||
handler(w, req)
|
||||
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Errorf("RequireAdmin returned status %d, want %d", w.Code, http.StatusForbidden)
|
||||
}
|
||||
if ct := w.Header().Get("Content-Type"); ct != "application/json" {
|
||||
t.Errorf("RequireAdmin Content-Type = %q, want application/json", ct)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAdmin_ProxyAuthNonAdminNonAPIReturnsPlainText(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
ProxyAuthSecret: "secret123",
|
||||
ProxyAuthUserHeader: "X-Remote-User",
|
||||
ProxyAuthRoleHeader: "X-Remote-Roles",
|
||||
ProxyAuthAdminRole: "admin",
|
||||
}
|
||||
|
||||
handler := RequireAdmin(cfg, func(w http.ResponseWriter, r *http.Request) {})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/admin/test", nil)
|
||||
req.Header.Set("X-Proxy-Secret", "secret123")
|
||||
req.Header.Set("X-Remote-User", "regular-user")
|
||||
req.Header.Set("X-Remote-Roles", "user")
|
||||
handler(w, req)
|
||||
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Errorf("RequireAdmin returned status %d, want %d", w.Code, http.StatusForbidden)
|
||||
}
|
||||
// Should be plain text error
|
||||
body := w.Body.String()
|
||||
if !strings.Contains(body, "Admin privileges required") {
|
||||
t.Errorf("RequireAdmin body = %q, want to contain 'Admin privileges required'", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAdmin_ProxyAuthNoRoleHeaderDefaultsToAdmin(t *testing.T) {
|
||||
// When ProxyAuthRoleHeader is not configured, all authenticated users are admins
|
||||
cfg := &config.Config{
|
||||
ProxyAuthSecret: "secret123",
|
||||
ProxyAuthUserHeader: "X-Remote-User",
|
||||
// No ProxyAuthRoleHeader set
|
||||
}
|
||||
|
||||
handlerCalled := false
|
||||
handler := RequireAdmin(cfg, func(w http.ResponseWriter, r *http.Request) {
|
||||
handlerCalled = true
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/api/admin/test", nil)
|
||||
req.Header.Set("X-Proxy-Secret", "secret123")
|
||||
req.Header.Set("X-Remote-User", "any-user")
|
||||
handler(w, req)
|
||||
|
||||
if !handlerCalled {
|
||||
t.Error("RequireAdmin should call handler when no role checking is configured")
|
||||
}
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("RequireAdmin returned status %d, want %d", w.Code, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAdmin_ProxyAuthInvalidSecretUnauthorized(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
ProxyAuthSecret: "secret123",
|
||||
ProxyAuthUserHeader: "X-Remote-User",
|
||||
}
|
||||
|
||||
handlerCalled := false
|
||||
handler := RequireAdmin(cfg, func(w http.ResponseWriter, r *http.Request) {
|
||||
handlerCalled = true
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/api/admin/test", nil)
|
||||
req.Header.Set("X-Proxy-Secret", "wrong-secret")
|
||||
req.Header.Set("X-Remote-User", "admin-user")
|
||||
handler(w, req)
|
||||
|
||||
if handlerCalled {
|
||||
t.Error("RequireAdmin should not call handler with invalid proxy secret")
|
||||
}
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("RequireAdmin returned status %d, want %d", w.Code, http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAdmin_ProxyAuthCustomRoleSeparator(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
ProxyAuthSecret: "secret123",
|
||||
ProxyAuthUserHeader: "X-Remote-User",
|
||||
ProxyAuthRoleHeader: "X-Remote-Roles",
|
||||
ProxyAuthAdminRole: "administrator",
|
||||
ProxyAuthRoleSeparator: ",",
|
||||
}
|
||||
|
||||
handlerCalled := false
|
||||
handler := RequireAdmin(cfg, func(w http.ResponseWriter, r *http.Request) {
|
||||
handlerCalled = true
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/api/admin/test", nil)
|
||||
req.Header.Set("X-Proxy-Secret", "secret123")
|
||||
req.Header.Set("X-Remote-User", "admin-user")
|
||||
req.Header.Set("X-Remote-Roles", "user,administrator,viewer") // Comma separated
|
||||
handler(w, req)
|
||||
|
||||
if !handlerCalled {
|
||||
t.Error("RequireAdmin should call handler for admin with custom role separator")
|
||||
}
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("RequireAdmin returned status %d, want %d", w.Code, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAdmin_ProxyAuthTrimSpacesInRoles(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
ProxyAuthSecret: "secret123",
|
||||
ProxyAuthUserHeader: "X-Remote-User",
|
||||
ProxyAuthRoleHeader: "X-Remote-Roles",
|
||||
ProxyAuthAdminRole: "admin",
|
||||
}
|
||||
|
||||
handlerCalled := false
|
||||
handler := RequireAdmin(cfg, func(w http.ResponseWriter, r *http.Request) {
|
||||
handlerCalled = true
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/api/admin/test", nil)
|
||||
req.Header.Set("X-Proxy-Secret", "secret123")
|
||||
req.Header.Set("X-Remote-User", "admin-user")
|
||||
req.Header.Set("X-Remote-Roles", "user| admin |viewer") // Spaces around admin
|
||||
handler(w, req)
|
||||
|
||||
if !handlerCalled {
|
||||
t.Error("RequireAdmin should call handler when role matches after trimming spaces")
|
||||
}
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("RequireAdmin returned status %d, want %d", w.Code, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAdmin_NoProxyAuthAuthenticatedAllowed(t *testing.T) {
|
||||
// When proxy auth is not configured, authenticated users are considered admins
|
||||
hashedPass, _ := auth.HashPassword("password123")
|
||||
cfg := &config.Config{
|
||||
AuthUser: "testuser",
|
||||
AuthPass: hashedPass,
|
||||
}
|
||||
|
||||
handlerCalled := false
|
||||
handler := RequireAdmin(cfg, func(w http.ResponseWriter, r *http.Request) {
|
||||
handlerCalled = true
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/api/admin/test", nil)
|
||||
req.SetBasicAuth("testuser", "password123")
|
||||
handler(w, req)
|
||||
|
||||
if !handlerCalled {
|
||||
t.Error("RequireAdmin should call handler for basic auth authenticated user")
|
||||
}
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("RequireAdmin returned status %d, want %d", w.Code, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue