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) } }) }