Files
pulse/pkg/securityutil/securityutil_websocket_origin_branchcov0716_test.go
T
rcourtman df4e02ef89 Add Go branch-coverage tests for models config securityutil and aicontracts helpers
Adds table-driven branch-coverage unit tests for previously uncovered pure
functions across internal/models, internal/config, pkg/securityutil and
pkg/aicontracts. New test files only, with no source changes.

Covers model collection normalizers, reflect-based metric deep-copy,
removed-host agent bookkeeping, report-schedule and entitlement-billing
normalization, effective AI control and Patrol autonomy resolution,
websocket-origin host normalization, and command-safety classifiers. 61
TestBranchCov functions in 8 files, all vet and gofmt clean.
2026-07-16 14:20:13 +01:00

210 lines
9.8 KiB
Go

package securityutil
import (
"strings"
"testing"
)
// These tests target the websocket-origin helpers in websocket_origin.go:
// NormalizeWebSocketOriginHost, SameHostWebSocketOrigin,
// HTTPOriginForWebSocketBaseURL, and HTTPOriginForWebSocketBaseURLWithOptions.
// They exercise both sides of each conditional, the switch arms, error paths,
// and boundary/empty inputs against the package's real exported functions.
func TestBranchCovNormalizeWebSocketOriginHost(t *testing.T) {
tests := []struct {
name string
host string
want string
}{
// Branch: empty after TrimSpace -> return "".
{name: "empty string", host: "", want: ""},
{name: "whitespace only", host: " ", want: ""},
{name: "tab newline only", host: "\t\n", want: ""},
// Branch: SplitHostPort error (no port) -> return lowercased+trimmed host.
{name: "plain host lowercased", host: "Example.COM", want: "example.com"},
{name: "plain host trimmed and lowercased", host: " Host.Example.COM ", want: "host.example.com"},
// Branch: port == "80" -> strip, return host only.
{name: "port 80 stripped", host: "example.com:80", want: "example.com"},
{name: "uppercase host port 80 stripped", host: "API.Example.COM:80", want: "api.example.com"},
// Branch: port == "443" -> strip, return host only.
{name: "port 443 stripped", host: "example.com:443", want: "example.com"},
// Branch: other port -> JoinHostPort(host, port).
{name: "nonstandard port kept", host: "example.com:8080", want: "example.com:8080"},
{name: "uppercase host nonstandard port", host: "API.Host:9000", want: "api.host:9000"},
// Boundary: "080" is not "80", so the port is preserved verbatim.
{name: "non canonical port string kept", host: "example.com:080", want: "example.com:080"},
// IPv6: JoinHostPort re-wraps in brackets.
{name: "ipv6 nonstandard port joined", host: "[::1]:8080", want: "[::1]:8080"},
{name: "ipv6 bracketed without port kept", host: "[::1]", want: "[::1]"},
// IPv6 with standard port: SplitHostPort returns the bare host without
// brackets, so the returned value loses its IPv6 bracketing. This is the
// actual current behavior (see report for the same-origin impact).
{name: "ipv6 port 80 loses brackets", host: "[::1]:80", want: "::1"},
{name: "ipv6 port 443 loses brackets", host: "[fe80::1]:443", want: "fe80::1"},
// Boundary: empty host with a port. SplitHostPort(":8080") succeeds with
// an empty host, so JoinHostPort reproduces ":8080"; ":80" strips to "".
{name: "empty host with nonstandard port", host: ":8080", want: ":8080"},
{name: "empty host with port 80 collapses to empty", host: ":80", want: ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := NormalizeWebSocketOriginHost(tt.host)
if got != tt.want {
t.Fatalf("NormalizeWebSocketOriginHost(%q) = %q, want %q", tt.host, got, tt.want)
}
})
}
}
func TestBranchCovSameHostWebSocketOrigin(t *testing.T) {
tests := []struct {
name string
origin string
requestHost string
want bool
}{
// Branch: url.Parse error -> false. "%zz" is an invalid escape that
// url.Parse rejects (same input used elsewhere for parse-error coverage).
{name: "parse error invalid escape", origin: "http://host/%zz", requestHost: "host", want: false},
// Branch: parsed.Host == "" -> false.
{name: "empty origin", origin: "", requestHost: "example.com", want: false},
{name: "whitespace only origin", origin: " ", requestHost: "example.com", want: false},
{name: "scheme but empty host", origin: "http://", requestHost: "example.com", want: false},
// Branch: scheme not http/https -> false. Note ws/wss origins are rejected.
{name: "ftp scheme rejected", origin: "ftp://example.com", requestHost: "example.com", want: false},
{name: "ws scheme rejected", origin: "ws://localhost", requestHost: "localhost", want: false},
{name: "wss scheme rejected", origin: "wss://localhost", requestHost: "localhost", want: false},
{name: "file scheme rejected", origin: "file:///etc/passwd", requestHost: "localhost", want: false},
// Branch: http/https scheme, equal normalized hosts -> true.
{name: "https same host", origin: "https://example.com", requestHost: "example.com", want: true},
{name: "http same host", origin: "http://example.com", requestHost: "example.com", want: true},
{name: "scheme differs but host equal", origin: "https://example.com", requestHost: "example.com", want: true},
{name: "origin trimmed before parse", origin: " https://example.com ", requestHost: "example.com", want: true},
{name: "uppercase origin host matches", origin: "https://API.Example.COM", requestHost: "api.example.com", want: true},
// Integration: standard ports normalize away on both sides -> match.
{name: "origin port 80 equals bare host", origin: "http://example.com:80", requestHost: "example.com", want: true},
{name: "both standard port 443 match", origin: "https://example.com:443", requestHost: "example.com:443", want: true},
// Branch: normalized hosts differ -> false.
{name: "nonstandard port mismatch", origin: "http://example.com:8080", requestHost: "example.com", want: false},
{name: "host mismatch", origin: "https://example.com", requestHost: "evil.com", want: false},
{name: "both same nonstandard port match", origin: "http://example.com:9000", requestHost: "example.com:9000", want: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := SameHostWebSocketOrigin(tt.origin, tt.requestHost)
if got != tt.want {
t.Fatalf("SameHostWebSocketOrigin(%q, %q) = %v, want %v", tt.origin, tt.requestHost, got, tt.want)
}
})
}
}
func TestBranchCovHTTPOriginForWebSocketBaseURL(t *testing.T) {
tests := []struct {
name string
raw string
wantErr string
want string
}{
// Delegating entrypoint: wss -> https.
{name: "wss to https", raw: "wss://example.com", want: "https://example.com"},
// ws over loopback -> http.
{name: "ws loopback to http", raw: "ws://localhost:8080", want: "http://localhost:8080"},
// https is upgraded to wss by the normalizer, then mapped to https.
{name: "https round trips to https", raw: "https://example.com", want: "https://example.com"},
// Error path delegated from the zero-option validator.
{name: "empty required", raw: " ", wantErr: "Pulse URL is required"},
{name: "non loopback http rejected", raw: "http://example.com", wantErr: "must use https/wss unless host is loopback"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := HTTPOriginForWebSocketBaseURL(tt.raw)
if tt.wantErr != "" {
if err == nil {
t.Fatalf("HTTPOriginForWebSocketBaseURL(%q) = %q, want error containing %q", tt.raw, got, tt.wantErr)
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("HTTPOriginForWebSocketBaseURL(%q) err = %q, want substring %q", tt.raw, err.Error(), tt.wantErr)
}
return
}
if err != nil {
t.Fatalf("HTTPOriginForWebSocketBaseURL(%q) unexpected error: %v", tt.raw, err)
}
if got != tt.want {
t.Fatalf("HTTPOriginForWebSocketBaseURL(%q) = %q, want %q", tt.raw, got, tt.want)
}
})
}
}
func TestBranchCovHTTPOriginForWebSocketBaseURLWithOptions(t *testing.T) {
tests := []struct {
name string
raw string
opts PulseURLValidationOptions
wantErr string
want string
}{
// switch arm "ws" -> "http".
{name: "ws maps to http", raw: "ws://localhost:8080", opts: PulseURLValidationOptions{}, want: "http://localhost:8080"},
// switch arm "wss" -> "https".
{name: "wss maps to https", raw: "wss://example.com", opts: PulseURLValidationOptions{}, want: "https://example.com"},
// https is normalized to wss upstream, then mapped back to https.
{name: "https normalized to wss then https", raw: "https://api.example.com", opts: PulseURLValidationOptions{}, want: "https://api.example.com"},
// The normalizer preserves a non-root path; this function must clear
// Path/RawPath/RawQuery/Fragment so only the bare origin remains.
{name: "path stripped to origin", raw: "wss://example.com/a/b", opts: PulseURLValidationOptions{}, want: "https://example.com"},
{name: "loopback ws path stripped to origin", raw: "ws://127.0.0.1/agent/feed", opts: PulseURLValidationOptions{}, want: "http://127.0.0.1"},
// opts flow through to the validator: AllowLocalNetworkHTTP permits
// plaintext to a private IPv4, which then maps ws -> http.
{name: "opts allow local network plaintext", raw: "http://192.168.1.5:9000", opts: PulseURLValidationOptions{AllowLocalNetworkHTTP: true}, want: "http://192.168.1.5:9000"},
// Error path: upstream normalization failure is returned as-is.
{name: "empty required", raw: "", opts: PulseURLValidationOptions{}, wantErr: "Pulse URL is required"},
{name: "missing scheme websocket message", raw: "example.com", opts: PulseURLValidationOptions{}, wantErr: "must include scheme"},
{name: "non loopback http without opts rejected", raw: "http://example.com", opts: PulseURLValidationOptions{}, wantErr: "must use https/wss unless host is loopback"},
{name: "unsupported scheme rejected upstream", raw: "ftp://host", opts: PulseURLValidationOptions{}, wantErr: "unsupported scheme"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := HTTPOriginForWebSocketBaseURLWithOptions(tt.raw, tt.opts)
if tt.wantErr != "" {
if err == nil {
t.Fatalf("HTTPOriginForWebSocketBaseURLWithOptions(%q) = %q, want error containing %q", tt.raw, got, tt.wantErr)
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("HTTPOriginForWebSocketBaseURLWithOptions(%q) err = %q, want substring %q", tt.raw, err.Error(), tt.wantErr)
}
return
}
if err != nil {
t.Fatalf("HTTPOriginForWebSocketBaseURLWithOptions(%q) unexpected error: %v", tt.raw, err)
}
if got != tt.want {
t.Fatalf("HTTPOriginForWebSocketBaseURLWithOptions(%q) = %q, want %q", tt.raw, got, tt.want)
}
})
}
}