Pulse/internal/api/config_handlers_host_test.go
rcourtman 4f824ab148 style: Apply gofmt to 37 files
Standardize code formatting across test files and monitor.go.
No functional changes.
2025-12-02 17:21:48 +00:00

475 lines
12 KiB
Go

package api
import (
"testing"
)
func TestExtractHostAndPort(t *testing.T) {
tests := []struct {
name string
hostStr string
wantHost string
wantPort string
wantErr bool
}{
// Basic host:port
{
name: "simple host with port",
hostStr: "example.com:8006",
wantHost: "example.com",
wantPort: "8006",
},
{
name: "host without port",
hostStr: "example.com",
wantHost: "example.com",
wantPort: "",
},
// HTTP/HTTPS scheme stripping
{
name: "https scheme stripped",
hostStr: "https://example.com:8006",
wantHost: "example.com",
wantPort: "8006",
},
{
name: "http scheme stripped",
hostStr: "http://example.com:8006",
wantHost: "example.com",
wantPort: "8006",
},
{
name: "https scheme without port",
hostStr: "https://example.com",
wantHost: "example.com",
wantPort: "",
},
{
name: "http scheme without port",
hostStr: "http://example.com",
wantHost: "example.com",
wantPort: "",
},
// Path stripping
{
name: "path stripped from host:port",
hostStr: "example.com:8006/api/v1",
wantHost: "example.com",
wantPort: "8006",
},
{
name: "path stripped from host without port",
hostStr: "example.com/api/v1",
wantHost: "example.com",
wantPort: "",
},
{
name: "full URL with scheme path and port",
hostStr: "https://example.com:8443/some/path",
wantHost: "example.com",
wantPort: "8443",
},
{
name: "trailing slash stripped",
hostStr: "example.com/",
wantHost: "example.com",
wantPort: "",
},
// IPv4 addresses
{
name: "IPv4 with port",
hostStr: "192.168.1.100:8006",
wantHost: "192.168.1.100",
wantPort: "8006",
},
{
name: "IPv4 without port",
hostStr: "192.168.1.100",
wantHost: "192.168.1.100",
wantPort: "",
},
{
name: "IPv4 with scheme and port",
hostStr: "https://192.168.1.100:8006",
wantHost: "192.168.1.100",
wantPort: "8006",
},
// IPv6 addresses with brackets
{
name: "IPv6 with brackets and port",
hostStr: "[2001:db8::1]:8006",
wantHost: "2001:db8::1",
wantPort: "8006",
},
{
name: "IPv6 with brackets no port returns error",
hostStr: "[2001:db8::1]",
wantErr: true, // net.SplitHostPort fails on bracketed IPv6 without port
},
{
name: "IPv6 with scheme brackets and port",
hostStr: "https://[2001:db8::1]:8006",
wantHost: "2001:db8::1",
wantPort: "8006",
},
{
name: "IPv6 loopback with brackets",
hostStr: "[::1]:8006",
wantHost: "::1",
wantPort: "8006",
},
// IPv6 without brackets (no port case)
{
name: "IPv6 without brackets returns as-is",
hostStr: "2001:db8::1",
wantHost: "2001:db8::1",
wantPort: "",
},
{
name: "IPv6 full address without brackets",
hostStr: "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
wantHost: "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
wantPort: "",
},
{
name: "IPv6 loopback without brackets",
hostStr: "::1",
wantHost: "::1",
wantPort: "",
},
// Edge cases
{
name: "empty string",
hostStr: "",
wantHost: "",
wantPort: "",
},
{
name: "only port colon",
hostStr: ":8006",
wantHost: "",
wantPort: "8006",
},
{
name: "localhost with port",
hostStr: "localhost:8006",
wantHost: "localhost",
wantPort: "8006",
},
{
name: "localhost without port",
hostStr: "localhost",
wantHost: "localhost",
wantPort: "",
},
// Error cases
{
name: "malformed brackets",
hostStr: "[2001:db8::1:8006",
wantErr: true,
},
{
name: "brackets without closing with port attempt",
hostStr: "[invalid:8006",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotHost, gotPort, err := extractHostAndPort(tt.hostStr)
if tt.wantErr {
if err == nil {
t.Errorf("extractHostAndPort(%q) expected error, got host=%q port=%q", tt.hostStr, gotHost, gotPort)
}
return
}
if err != nil {
t.Errorf("extractHostAndPort(%q) unexpected error: %v", tt.hostStr, err)
return
}
if gotHost != tt.wantHost {
t.Errorf("extractHostAndPort(%q) host = %q, want %q", tt.hostStr, gotHost, tt.wantHost)
}
if gotPort != tt.wantPort {
t.Errorf("extractHostAndPort(%q) port = %q, want %q", tt.hostStr, gotPort, tt.wantPort)
}
})
}
}
func TestValidateIPAddress(t *testing.T) {
tests := []struct {
name string
ip string
want bool
}{
// Valid IPv4 addresses
{"valid IPv4 localhost", "127.0.0.1", true},
{"valid IPv4 private", "192.168.1.1", true},
{"valid IPv4 public", "8.8.8.8", true},
{"valid IPv4 zeros", "0.0.0.0", true},
{"valid IPv4 broadcast", "255.255.255.255", true},
{"valid IPv4 class A", "10.0.0.1", true},
{"valid IPv4 class B", "172.16.0.1", true},
// Valid IPv6 addresses
{"valid IPv6 loopback", "::1", true},
{"valid IPv6 unspecified", "::", true},
{"valid IPv6 full", "2001:0db8:85a3:0000:0000:8a2e:0370:7334", true},
{"valid IPv6 compressed", "2001:db8:85a3::8a2e:370:7334", true},
{"valid IPv6 link-local", "fe80::1", true},
{"valid IPv6 multicast", "ff02::1", true},
// Invalid addresses
{"invalid empty", "", false},
{"invalid hostname", "localhost", false},
{"invalid domain", "example.com", false},
{"invalid IPv4 with port", "192.168.1.1:8080", false},
{"invalid IPv4 out of range", "256.256.256.256", false},
{"invalid IPv4 too many octets", "192.168.1.1.1", false},
{"invalid IPv4 too few octets", "192.168.1", false},
{"invalid IPv4 negative", "-1.0.0.0", false},
{"invalid IPv4 with letters", "192.168.a.1", false},
{"invalid IPv6 with port", "[::1]:8080", false},
{"invalid IPv6 brackets only", "[::1]", false},
{"invalid random string", "not-an-ip", false},
{"invalid whitespace", " 192.168.1.1 ", false},
{"invalid URL", "https://192.168.1.1", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := validateIPAddress(tt.ip)
if got != tt.want {
t.Errorf("validateIPAddress(%q) = %v, want %v", tt.ip, got, tt.want)
}
})
}
}
func TestValidatePort(t *testing.T) {
tests := []struct {
name string
portStr string
want bool
}{
// Valid ports
{"valid minimum port", "1", true},
{"valid maximum port", "65535", true},
{"valid common HTTP", "80", true},
{"valid HTTPS", "443", true},
{"valid Proxmox PVE", "8006", true},
{"valid Proxmox PBS", "8007", true},
{"valid SSH", "22", true},
{"valid high port", "49152", true},
// Invalid ports
{"invalid zero", "0", false},
{"invalid negative", "-1", false},
{"invalid too high", "65536", false},
{"invalid way too high", "100000", false},
{"invalid empty string", "", false},
{"invalid non-numeric", "abc", false},
{"invalid float", "80.5", false},
{"invalid with spaces", " 80 ", false},
{"invalid mixed", "80abc", false},
{"invalid hex", "0x50", false},
{"invalid with leading zeros that parse ok", "0080", true}, // strconv.Atoi handles leading zeros
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := validatePort(tt.portStr)
if got != tt.want {
t.Errorf("validatePort(%q) = %v, want %v", tt.portStr, got, tt.want)
}
})
}
}
func TestDefaultPortForNodeType(t *testing.T) {
tests := []struct {
name string
nodeType string
want string
}{
{"PVE node", "pve", "8006"},
{"PMG node", "pmg", "8006"},
{"PBS node", "pbs", "8007"},
{"docker node returns empty", "docker", ""},
{"unknown type returns empty", "unknown", ""},
{"empty type returns empty", "", ""},
{"uppercase PVE returns empty", "PVE", ""}, // case sensitive
{"mixed case returns empty", "Pve", ""}, // case sensitive
{"similar but wrong returns empty", "pvee", ""}, // exact match only
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := defaultPortForNodeType(tt.nodeType)
if got != tt.want {
t.Errorf("defaultPortForNodeType(%q) = %q, want %q", tt.nodeType, got, tt.want)
}
})
}
}
func TestNormalizeNodeHost(t *testing.T) {
tests := []struct {
name string
rawHost string
nodeType string
want string
wantErr bool
}{
{
name: "adds default port to explicit https without port",
rawHost: "https://example.com",
nodeType: "pve",
want: "https://example.com:8006",
},
{
name: "adds default port for bare pve host",
rawHost: "pve.lan",
nodeType: "pve",
want: "https://pve.lan:8006",
},
{
name: "adds default port for bare pbs host",
rawHost: "pbs.lan",
nodeType: "pbs",
want: "https://pbs.lan:8007",
},
{
name: "preserves custom port",
rawHost: "https://example.com:8443",
nodeType: "pve",
want: "https://example.com:8443",
},
{
name: "supports ipv6 without scheme",
rawHost: "2001:db8::1",
nodeType: "pmg",
want: "https://[2001:db8::1]:8006",
},
{
name: "drops path segments",
rawHost: "https://example.com/api",
nodeType: "pve",
want: "https://example.com:8006",
},
{
name: "adds default port to explicit http scheme",
rawHost: "http://example.com",
nodeType: "pve",
want: "http://example.com:8006",
},
// Error cases
{
name: "empty host returns error",
rawHost: "",
nodeType: "pve",
wantErr: true,
},
{
name: "whitespace-only host returns error",
rawHost: " ",
nodeType: "pve",
wantErr: true,
},
{
name: "control character in host returns error",
rawHost: "\x00invalid",
nodeType: "pve",
wantErr: true,
},
// Unknown node type (no default port added)
{
name: "unknown node type keeps host without port",
rawHost: "https://example.com",
nodeType: "unknown",
want: "https://example.com",
},
{
name: "docker node type keeps host without port",
rawHost: "https://docker.lan",
nodeType: "docker",
want: "https://docker.lan",
},
// IPv4 addresses
{
name: "IPv4 address gets default port",
rawHost: "192.168.1.100",
nodeType: "pve",
want: "https://192.168.1.100:8006",
},
{
name: "IPv4 with custom port preserved",
rawHost: "192.168.1.100:9000",
nodeType: "pve",
want: "https://192.168.1.100:9000",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := normalizeNodeHost(tt.rawHost, tt.nodeType)
if tt.wantErr {
if err == nil {
t.Fatalf("normalizeNodeHost(%q, %q) expected error, got %q", tt.rawHost, tt.nodeType, got)
}
return
}
if err != nil {
t.Fatalf("normalizeNodeHost(%q, %q) returned error: %v", tt.rawHost, tt.nodeType, err)
}
if got != tt.want {
t.Fatalf("normalizeNodeHost(%q, %q) = %q, want %q", tt.rawHost, tt.nodeType, got, tt.want)
}
})
}
}
func TestGenerateNodeID(t *testing.T) {
tests := []struct {
name string
nodeType string
index int
want string
}{
// Standard node types
{"pve node index 0", "pve", 0, "pve-0"},
{"pve node index 1", "pve", 1, "pve-1"},
{"pbs node index 0", "pbs", 0, "pbs-0"},
{"pmg node index 5", "pmg", 5, "pmg-5"},
{"docker node index 2", "docker", 2, "docker-2"},
// Edge cases
{"empty node type", "", 0, "-0"},
{"negative index", "pve", -1, "pve--1"},
{"large index", "pve", 999, "pve-999"},
{"very large index", "pve", 2147483647, "pve-2147483647"},
// Custom/unknown node types
{"custom type", "custom-type", 3, "custom-type-3"},
{"type with spaces", "my node", 1, "my node-1"},
{"type with special chars", "node_v2", 0, "node_v2-0"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := generateNodeID(tt.nodeType, tt.index)
if got != tt.want {
t.Errorf("generateNodeID(%q, %d) = %q, want %q", tt.nodeType, tt.index, got, tt.want)
}
})
}
}