mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-02-18 00:17:39 +01:00
Tests WebSocket upgrade with POST method in demo mode, ensuring the websocket check branch is exercised after the GET/HEAD/OPTIONS check. Coverage: 90% to 100%.
146 lines
5.5 KiB
Go
146 lines
5.5 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
)
|
|
|
|
func TestDemoModeMiddleware(t *testing.T) {
|
|
// Create a simple handler that records if it was called
|
|
handlerCalled := false
|
|
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
handlerCalled = true
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("OK"))
|
|
})
|
|
|
|
tests := []struct {
|
|
name string
|
|
demoMode bool
|
|
method string
|
|
path string
|
|
upgradeHeader string
|
|
wantCalled bool
|
|
wantStatus int
|
|
wantDemoHeader bool
|
|
}{
|
|
// Demo mode disabled - all requests pass through
|
|
{"demo off GET", false, http.MethodGet, "/api/users", "", true, http.StatusOK, false},
|
|
{"demo off POST", false, http.MethodPost, "/api/users", "", true, http.StatusOK, false},
|
|
{"demo off PUT", false, http.MethodPut, "/api/users/1", "", true, http.StatusOK, false},
|
|
{"demo off DELETE", false, http.MethodDelete, "/api/users/1", "", true, http.StatusOK, false},
|
|
{"demo off PATCH", false, http.MethodPatch, "/api/users/1", "", true, http.StatusOK, false},
|
|
|
|
// Demo mode enabled - read-only methods allowed
|
|
{"demo on GET", true, http.MethodGet, "/api/users", "", true, http.StatusOK, true},
|
|
{"demo on HEAD", true, http.MethodHead, "/api/users", "", true, http.StatusOK, true},
|
|
{"demo on OPTIONS", true, http.MethodOptions, "/api/users", "", true, http.StatusOK, true},
|
|
|
|
// Demo mode enabled - WebSocket upgrades allowed
|
|
{"demo on websocket GET", true, http.MethodGet, "/api/ws", "websocket", true, http.StatusOK, true},
|
|
{"demo on websocket case insensitive", true, http.MethodGet, "/api/ws", "WebSocket", true, http.StatusOK, true},
|
|
{"demo on websocket uppercase", true, http.MethodGet, "/api/ws", "WEBSOCKET", true, http.StatusOK, true},
|
|
// WebSocket upgrade with POST method (tests websocket branch after GET/HEAD/OPTIONS check)
|
|
{"demo on websocket POST", true, http.MethodPost, "/api/ws", "websocket", true, http.StatusOK, true},
|
|
|
|
// Demo mode enabled - auth endpoints allowed (POST)
|
|
{"demo on login", true, http.MethodPost, "/api/login", "", true, http.StatusOK, true},
|
|
{"demo on oidc login", true, http.MethodPost, "/api/oidc/login", "", true, http.StatusOK, true},
|
|
{"demo on oidc callback", true, http.MethodPost, "/api/oidc/callback", "", true, http.StatusOK, true},
|
|
{"demo on logout", true, http.MethodPost, "/api/logout", "", true, http.StatusOK, true},
|
|
|
|
// Demo mode enabled - modification requests blocked
|
|
{"demo on POST", true, http.MethodPost, "/api/users", "", false, http.StatusForbidden, true},
|
|
{"demo on PUT", true, http.MethodPut, "/api/users/1", "", false, http.StatusForbidden, true},
|
|
{"demo on DELETE", true, http.MethodDelete, "/api/users/1", "", false, http.StatusForbidden, true},
|
|
{"demo on PATCH", true, http.MethodPatch, "/api/users/1", "", false, http.StatusForbidden, true},
|
|
|
|
// Demo mode enabled - partial path matches should not be allowed
|
|
{"demo on login prefix", true, http.MethodPost, "/api/login/extra", "", false, http.StatusForbidden, true},
|
|
{"demo on oidc prefix", true, http.MethodPost, "/api/oidc/loginx", "", false, http.StatusForbidden, true},
|
|
|
|
// Demo mode enabled - POST to other paths blocked
|
|
{"demo on POST settings", true, http.MethodPost, "/api/settings", "", false, http.StatusForbidden, true},
|
|
{"demo on POST config", true, http.MethodPost, "/api/config", "", false, http.StatusForbidden, true},
|
|
{"demo on DELETE docker host", true, http.MethodDelete, "/api/docker-hosts/1", "", false, http.StatusForbidden, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
handlerCalled = false
|
|
|
|
cfg := &config.Config{
|
|
DemoMode: tt.demoMode,
|
|
}
|
|
|
|
middleware := DemoModeMiddleware(cfg, nextHandler)
|
|
|
|
req := httptest.NewRequest(tt.method, tt.path, nil)
|
|
if tt.upgradeHeader != "" {
|
|
req.Header.Set("Upgrade", tt.upgradeHeader)
|
|
}
|
|
|
|
rr := httptest.NewRecorder()
|
|
middleware.ServeHTTP(rr, req)
|
|
|
|
// Check if handler was called
|
|
if handlerCalled != tt.wantCalled {
|
|
t.Errorf("handler called = %v, want %v", handlerCalled, tt.wantCalled)
|
|
}
|
|
|
|
// Check status code
|
|
if rr.Code != tt.wantStatus {
|
|
t.Errorf("status = %d, want %d", rr.Code, tt.wantStatus)
|
|
}
|
|
|
|
// Check X-Demo-Mode header
|
|
demoHeader := rr.Header().Get("X-Demo-Mode")
|
|
hasDemoHeader := demoHeader == "true"
|
|
if hasDemoHeader != tt.wantDemoHeader {
|
|
t.Errorf("X-Demo-Mode header present = %v, want %v", hasDemoHeader, tt.wantDemoHeader)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDemoModeMiddleware_BlockedResponse(t *testing.T) {
|
|
// Verify the error response format when a request is blocked
|
|
cfg := &config.Config{
|
|
DemoMode: true,
|
|
}
|
|
|
|
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
t.Error("handler should not be called for blocked request")
|
|
})
|
|
|
|
middleware := DemoModeMiddleware(cfg, nextHandler)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/users", nil)
|
|
rr := httptest.NewRecorder()
|
|
middleware.ServeHTTP(rr, req)
|
|
|
|
// Check Content-Type header
|
|
contentType := rr.Header().Get("Content-Type")
|
|
if contentType != "application/json" {
|
|
t.Errorf("Content-Type = %q, want %q", contentType, "application/json")
|
|
}
|
|
|
|
// Parse and verify response body
|
|
var response map[string]string
|
|
if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil {
|
|
t.Fatalf("failed to parse response body: %v", err)
|
|
}
|
|
|
|
if response["error"] != "Demo mode enabled" {
|
|
t.Errorf("error = %q, want %q", response["error"], "Demo mode enabled")
|
|
}
|
|
|
|
if response["message"] == "" {
|
|
t.Error("message should not be empty")
|
|
}
|
|
}
|