Files
uopi/internal/server/helpers_test.go
T
Martino Ferrari c0f7e662be Testing
2026-06-24 01:39:15 +02:00

224 lines
7.2 KiB
Go

package server
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/uopi/uopi/internal/access"
)
// -------------------------------------------------------------------------- //
// accessMiddleware //
// -------------------------------------------------------------------------- //
// okHandler records that it ran and returns 200.
func okHandler(ran *bool) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
*ran = true
w.WriteHeader(http.StatusOK)
})
}
func TestAccessMiddlewareWriteUserPassesMutation(t *testing.T) {
// A configured policy with an operator (write) user.
policy := access.New("", []access.GroupSpec{
{Name: "ops", Members: map[string]access.Role{"alice": access.RoleOperator}},
})
var ran bool
h := accessMiddleware(policy, testUserHeader, okHandler(&ran))
req := httptest.NewRequest(http.MethodPost, "/api/v1/interfaces", nil)
req.Header.Set(testUserHeader, "alice")
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusOK || !ran {
t.Errorf("write user POST: code=%d ran=%v, want 200/true", rec.Code, ran)
}
}
func TestAccessMiddlewareReadUserBlockedFromMutation(t *testing.T) {
// Configured policy → an unlisted user is a read-only viewer.
policy := access.New("", []access.GroupSpec{
{Name: "ops", Members: map[string]access.Role{"alice": access.RoleOperator}},
})
var ran bool
h := accessMiddleware(policy, testUserHeader, okHandler(&ran))
req := httptest.NewRequest(http.MethodPost, "/api/v1/interfaces", nil)
req.Header.Set(testUserHeader, "bob") // viewer
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden {
t.Errorf("read-only POST: code=%d, want 403", rec.Code)
}
if ran {
t.Error("handler must not run for a forbidden mutation")
}
}
func TestAccessMiddlewareReadUserAllowedToGet(t *testing.T) {
policy := access.New("", []access.GroupSpec{
{Name: "ops", Members: map[string]access.Role{"alice": access.RoleOperator}},
})
var ran bool
h := accessMiddleware(policy, testUserHeader, okHandler(&ran))
req := httptest.NewRequest(http.MethodGet, "/api/v1/interfaces", nil)
req.Header.Set(testUserHeader, "bob") // viewer
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusOK || !ran {
t.Errorf("read-only GET: code=%d ran=%v, want 200/true", rec.Code, ran)
}
}
func TestAccessMiddlewareMeAlwaysReachable(t *testing.T) {
policy := access.New("", []access.GroupSpec{
{Name: "ops", Members: map[string]access.Role{"alice": access.RoleOperator}},
})
var ran bool
h := accessMiddleware(policy, testUserHeader, okHandler(&ran))
// Even a mutating method on /me is allowed through (identity discovery).
req := httptest.NewRequest(http.MethodPost, "/api/v1/me", nil)
req.Header.Set(testUserHeader, "bob")
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusOK || !ran {
t.Errorf("/me must always be reachable: code=%d ran=%v", rec.Code, ran)
}
}
func TestAccessMiddlewareStoresUserOnContext(t *testing.T) {
policy := access.New("", nil) // unconfigured → everyone write
var got string
h := accessMiddleware(policy, testUserHeader, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
got = access.UserFrom(r.Context())
}))
req := httptest.NewRequest(http.MethodGet, "/api/v1/foo", nil)
req.Header.Set(testUserHeader, "carol")
h.ServeHTTP(httptest.NewRecorder(), req)
if got != "carol" {
t.Errorf("context user = %q, want carol", got)
}
}
// -------------------------------------------------------------------------- //
// httpsRedirectHandler //
// -------------------------------------------------------------------------- //
func TestHTTPSRedirectHandler(t *testing.T) {
cases := []struct {
name string
tlsAddr string
host string
target string
want string
}{
{"default port omitted", ":443", "example.com", "/panels?x=1", "https://example.com/panels?x=1"},
{"custom port preserved", ":8443", "host.local:80", "/a", "https://host.local:8443/a"},
{"root path", "0.0.0.0:443", "h", "/", "https://h/"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
h := httpsRedirectHandler(c.tlsAddr)
req := httptest.NewRequest(http.MethodGet, c.target, nil)
req.Host = c.host
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusMovedPermanently {
t.Fatalf("code = %d, want 301", rec.Code)
}
if loc := rec.Header().Get("Location"); loc != c.want {
t.Errorf("Location = %q, want %q", loc, c.want)
}
})
}
}
// -------------------------------------------------------------------------- //
// parseDialogTarget //
// -------------------------------------------------------------------------- //
func TestParseDialogTarget(t *testing.T) {
cases := []struct {
in string
ds, name string
ok bool
}{
{"epics:SR:CURRENT", "epics", "SR:CURRENT", true}, // splits on first ':' only
{"bareName", "srv", "bareName", true}, // defaults to srv
{" spaced ", "srv", "spaced", true},
{"", "", "", false},
{" ", "", "", false},
}
for _, c := range cases {
ds, name, ok := parseDialogTarget(c.in)
if ds != c.ds || name != c.name || ok != c.ok {
t.Errorf("parseDialogTarget(%q) = (%q,%q,%v), want (%q,%q,%v)",
c.in, ds, name, ok, c.ds, c.name, c.ok)
}
}
}
// -------------------------------------------------------------------------- //
// formatAuditValue //
// -------------------------------------------------------------------------- //
func TestFormatAuditValue(t *testing.T) {
cases := []struct {
in any
want string
}{
{"hello", "hello"},
{float64(3.5), "3.5"},
{float64(42), "42"},
{true, "true"},
{false, "false"},
{nil, ""},
{[]any{1.0, 2.0}, "[1,2]"}, // default → JSON
{map[string]any{"a": 1.0}, `{"a":1}`},
}
for _, c := range cases {
if got := formatAuditValue(c.in); got != c.want {
t.Errorf("formatAuditValue(%v) = %q, want %q", c.in, got, c.want)
}
}
}
// -------------------------------------------------------------------------- //
// clientIP //
// -------------------------------------------------------------------------- //
func TestClientIP(t *testing.T) {
t.Run("X-Forwarded-For first hop", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("X-Forwarded-For", "203.0.113.7, 10.0.0.1")
if got := clientIP(req); got != "203.0.113.7" {
t.Errorf("got %q, want 203.0.113.7", got)
}
})
t.Run("X-Real-IP", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("X-Real-IP", "198.51.100.4")
if got := clientIP(req); got != "198.51.100.4" {
t.Errorf("got %q, want 198.51.100.4", got)
}
})
t.Run("RemoteAddr fallback", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.RemoteAddr = "192.0.2.9:54321"
if got := clientIP(req); got != "192.0.2.9" {
t.Errorf("got %q, want 192.0.2.9", got)
}
})
}