Add unit tests for normalizeUnifiedAgentArch and getClientIP (api)
Tests for unified_agent.go: - normalizeUnifiedAgentArch: 51 test cases covering all supported architectures (linux/darwin/windows), case insensitivity, whitespace handling, and invalid inputs Tests for updates.go: - getClientIP: 18 test cases covering X-Forwarded-For priority, X-Real-IP fallback, RemoteAddr extraction, IPv4/IPv6 support, nil header safety, and case insensitivity
This commit is contained in:
parent
04d4b6606a
commit
930e003519
2 changed files with 318 additions and 0 deletions
92
internal/api/unified_agent_test.go
Normal file
92
internal/api/unified_agent_test.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
package api
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNormalizeUnifiedAgentArch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
// Linux AMD64 variants
|
||||
{name: "linux-amd64 canonical", input: "linux-amd64", expected: "linux-amd64"},
|
||||
{name: "amd64 short", input: "amd64", expected: "linux-amd64"},
|
||||
{name: "x86_64 uname style", input: "x86_64", expected: "linux-amd64"},
|
||||
|
||||
// Linux ARM64 variants
|
||||
{name: "linux-arm64 canonical", input: "linux-arm64", expected: "linux-arm64"},
|
||||
{name: "arm64 short", input: "arm64", expected: "linux-arm64"},
|
||||
{name: "aarch64 uname style", input: "aarch64", expected: "linux-arm64"},
|
||||
|
||||
// Linux ARMv7 variants
|
||||
{name: "linux-armv7 canonical", input: "linux-armv7", expected: "linux-armv7"},
|
||||
{name: "armv7 short", input: "armv7", expected: "linux-armv7"},
|
||||
{name: "armv7l uname style", input: "armv7l", expected: "linux-armv7"},
|
||||
{name: "armhf debian style", input: "armhf", expected: "linux-armv7"},
|
||||
|
||||
// Linux ARMv6 variants
|
||||
{name: "linux-armv6 canonical", input: "linux-armv6", expected: "linux-armv6"},
|
||||
{name: "armv6 short", input: "armv6", expected: "linux-armv6"},
|
||||
|
||||
// Linux 386 variants
|
||||
{name: "linux-386 canonical", input: "linux-386", expected: "linux-386"},
|
||||
{name: "386 short", input: "386", expected: "linux-386"},
|
||||
{name: "i386 style", input: "i386", expected: "linux-386"},
|
||||
{name: "i686 style", input: "i686", expected: "linux-386"},
|
||||
|
||||
// macOS variants
|
||||
{name: "darwin-amd64 canonical", input: "darwin-amd64", expected: "darwin-amd64"},
|
||||
{name: "macos-amd64 alias", input: "macos-amd64", expected: "darwin-amd64"},
|
||||
{name: "darwin-arm64 canonical", input: "darwin-arm64", expected: "darwin-arm64"},
|
||||
{name: "macos-arm64 alias", input: "macos-arm64", expected: "darwin-arm64"},
|
||||
|
||||
// Windows variants
|
||||
{name: "windows-amd64 canonical", input: "windows-amd64", expected: "windows-amd64"},
|
||||
{name: "windows-arm64 canonical", input: "windows-arm64", expected: "windows-arm64"},
|
||||
{name: "windows-386 canonical", input: "windows-386", expected: "windows-386"},
|
||||
|
||||
// Case insensitivity
|
||||
{name: "uppercase AMD64", input: "AMD64", expected: "linux-amd64"},
|
||||
{name: "mixed case Linux-AMD64", input: "Linux-AMD64", expected: "linux-amd64"},
|
||||
{name: "uppercase X86_64", input: "X86_64", expected: "linux-amd64"},
|
||||
{name: "mixed case AARCH64", input: "AARCH64", expected: "linux-arm64"},
|
||||
{name: "uppercase ARMHF", input: "ARMHF", expected: "linux-armv7"},
|
||||
{name: "uppercase DARWIN-ARM64", input: "DARWIN-ARM64", expected: "darwin-arm64"},
|
||||
{name: "uppercase WINDOWS-AMD64", input: "WINDOWS-AMD64", expected: "windows-amd64"},
|
||||
|
||||
// Whitespace handling
|
||||
{name: "leading space", input: " amd64", expected: "linux-amd64"},
|
||||
{name: "trailing space", input: "amd64 ", expected: "linux-amd64"},
|
||||
{name: "both spaces", input: " arm64 ", expected: "linux-arm64"},
|
||||
{name: "tab chars", input: "\tamd64\t", expected: "linux-amd64"},
|
||||
|
||||
// Invalid/unknown architectures
|
||||
{name: "empty string", input: "", expected: ""},
|
||||
{name: "unknown arch", input: "sparc", expected: ""},
|
||||
{name: "invalid value", input: "not-a-real-arch", expected: ""},
|
||||
{name: "partial match", input: "amd", expected: ""},
|
||||
{name: "numeric only", input: "64", expected: ""},
|
||||
{name: "special characters", input: "amd64!", expected: ""},
|
||||
{name: "linux alone", input: "linux", expected: ""},
|
||||
{name: "darwin alone", input: "darwin", expected: ""},
|
||||
{name: "windows alone", input: "windows", expected: ""},
|
||||
{name: "riscv64 unsupported", input: "riscv64", expected: ""},
|
||||
{name: "mips unsupported", input: "mips", expected: ""},
|
||||
{name: "ppc64 unsupported", input: "ppc64", expected: ""},
|
||||
|
||||
// Edge cases for similar strings
|
||||
{name: "armv8 not matched", input: "armv8", expected: ""},
|
||||
{name: "arm not matched", input: "arm", expected: ""},
|
||||
{name: "x86 not matched", input: "x86", expected: ""},
|
||||
{name: "x64 not matched", input: "x64", expected: ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := normalizeUnifiedAgentArch(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("normalizeUnifiedAgentArch(%q) = %q, want %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
226
internal/api/updates_test.go
Normal file
226
internal/api/updates_test.go
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetClientIP(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
xff string // X-Forwarded-For header
|
||||
xri string // X-Real-IP header
|
||||
remoteAddr string // Request.RemoteAddr
|
||||
expectedIP string
|
||||
}{
|
||||
// X-Forwarded-For takes priority
|
||||
{
|
||||
name: "XFF with valid IPv4",
|
||||
xff: "192.168.1.100",
|
||||
xri: "",
|
||||
remoteAddr: "10.0.0.1:12345",
|
||||
expectedIP: "192.168.1.100",
|
||||
},
|
||||
{
|
||||
name: "XFF with valid IPv6",
|
||||
xff: "2001:db8::1",
|
||||
xri: "",
|
||||
remoteAddr: "10.0.0.1:12345",
|
||||
expectedIP: "2001:db8::1",
|
||||
},
|
||||
{
|
||||
name: "XFF with IPv6 loopback",
|
||||
xff: "::1",
|
||||
xri: "",
|
||||
remoteAddr: "10.0.0.1:12345",
|
||||
expectedIP: "::1",
|
||||
},
|
||||
|
||||
// X-Real-IP fallback when XFF not valid
|
||||
{
|
||||
name: "XRI with valid IPv4",
|
||||
xff: "",
|
||||
xri: "172.16.0.50",
|
||||
remoteAddr: "10.0.0.1:12345",
|
||||
expectedIP: "172.16.0.50",
|
||||
},
|
||||
{
|
||||
name: "XRI with valid IPv6",
|
||||
xff: "",
|
||||
xri: "fe80::1",
|
||||
remoteAddr: "10.0.0.1:12345",
|
||||
expectedIP: "fe80::1",
|
||||
},
|
||||
{
|
||||
name: "XRI preferred when XFF invalid",
|
||||
xff: "invalid-ip",
|
||||
xri: "192.168.1.1",
|
||||
remoteAddr: "10.0.0.1:12345",
|
||||
expectedIP: "192.168.1.1",
|
||||
},
|
||||
|
||||
// RemoteAddr fallback
|
||||
{
|
||||
name: "RemoteAddr with port",
|
||||
xff: "",
|
||||
xri: "",
|
||||
remoteAddr: "10.0.0.1:12345",
|
||||
expectedIP: "10.0.0.1",
|
||||
},
|
||||
{
|
||||
name: "RemoteAddr IPv6 with port",
|
||||
xff: "",
|
||||
xri: "",
|
||||
remoteAddr: "[::1]:12345",
|
||||
expectedIP: "::1",
|
||||
},
|
||||
{
|
||||
name: "RemoteAddr without port",
|
||||
xff: "",
|
||||
xri: "",
|
||||
remoteAddr: "10.0.0.1",
|
||||
expectedIP: "10.0.0.1",
|
||||
},
|
||||
|
||||
// Invalid headers fall through
|
||||
{
|
||||
name: "XFF invalid falls to XRI",
|
||||
xff: "not-an-ip",
|
||||
xri: "192.168.1.1",
|
||||
remoteAddr: "10.0.0.1:12345",
|
||||
expectedIP: "192.168.1.1",
|
||||
},
|
||||
{
|
||||
name: "Both headers invalid falls to RemoteAddr",
|
||||
xff: "not-an-ip",
|
||||
xri: "also-not-an-ip",
|
||||
remoteAddr: "10.0.0.1:12345",
|
||||
expectedIP: "10.0.0.1",
|
||||
},
|
||||
|
||||
// Edge cases
|
||||
{
|
||||
name: "Empty XFF ignored",
|
||||
xff: "",
|
||||
xri: "192.168.1.1",
|
||||
remoteAddr: "10.0.0.1:12345",
|
||||
expectedIP: "192.168.1.1",
|
||||
},
|
||||
{
|
||||
name: "All empty uses RemoteAddr",
|
||||
xff: "",
|
||||
xri: "",
|
||||
remoteAddr: "127.0.0.1:8080",
|
||||
expectedIP: "127.0.0.1",
|
||||
},
|
||||
{
|
||||
name: "Loopback IPv4",
|
||||
xff: "127.0.0.1",
|
||||
xri: "",
|
||||
remoteAddr: "10.0.0.1:12345",
|
||||
expectedIP: "127.0.0.1",
|
||||
},
|
||||
|
||||
// Note: The current implementation has a bug with multiple IPs in XFF
|
||||
// It tries to parse the entire string as a single IP, which fails
|
||||
// This test documents current behavior, not ideal behavior
|
||||
{
|
||||
name: "XFF with multiple IPs - current behavior",
|
||||
xff: "192.168.1.100, 10.0.0.1",
|
||||
xri: "172.16.0.1",
|
||||
remoteAddr: "10.0.0.1:12345",
|
||||
expectedIP: "172.16.0.1", // Falls through because "192.168.1.100, 10.0.0.1" is not a valid single IP
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req := &http.Request{
|
||||
Header: make(http.Header),
|
||||
RemoteAddr: tt.remoteAddr,
|
||||
}
|
||||
|
||||
if tt.xff != "" {
|
||||
req.Header.Set("X-Forwarded-For", tt.xff)
|
||||
}
|
||||
if tt.xri != "" {
|
||||
req.Header.Set("X-Real-IP", tt.xri)
|
||||
}
|
||||
|
||||
result := getClientIP(req)
|
||||
if result != tt.expectedIP {
|
||||
t.Errorf("getClientIP() = %q, want %q", result, tt.expectedIP)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetClientIP_NilHeaders(t *testing.T) {
|
||||
// Test with a request that has nil headers (edge case)
|
||||
req := &http.Request{
|
||||
Header: nil,
|
||||
RemoteAddr: "10.0.0.1:12345",
|
||||
}
|
||||
|
||||
// This will panic if headers aren't handled correctly
|
||||
// The function should gracefully handle nil headers
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("getClientIP panicked with nil headers: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
// Note: With nil headers, Header.Get() will panic
|
||||
// This test documents that the function expects non-nil headers
|
||||
// If this test panics, it's documenting current behavior
|
||||
_ = getClientIP(req)
|
||||
}
|
||||
|
||||
func TestGetClientIP_HeaderCaseSensitivity(t *testing.T) {
|
||||
// HTTP headers are case-insensitive per RFC 7230
|
||||
// http.Header.Get handles this automatically
|
||||
tests := []struct {
|
||||
name string
|
||||
headerKey string
|
||||
headerVal string
|
||||
remoteAddr string
|
||||
expectedIP string
|
||||
}{
|
||||
{
|
||||
name: "lowercase x-forwarded-for",
|
||||
headerKey: "x-forwarded-for",
|
||||
headerVal: "192.168.1.100",
|
||||
remoteAddr: "10.0.0.1:12345",
|
||||
expectedIP: "192.168.1.100",
|
||||
},
|
||||
{
|
||||
name: "lowercase x-real-ip",
|
||||
headerKey: "x-real-ip",
|
||||
headerVal: "192.168.1.100",
|
||||
remoteAddr: "10.0.0.1:12345",
|
||||
expectedIP: "192.168.1.100",
|
||||
},
|
||||
{
|
||||
name: "mixed case X-Forwarded-FOR",
|
||||
headerKey: "X-Forwarded-FOR",
|
||||
headerVal: "192.168.1.100",
|
||||
remoteAddr: "10.0.0.1:12345",
|
||||
expectedIP: "192.168.1.100",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req := &http.Request{
|
||||
Header: make(http.Header),
|
||||
RemoteAddr: tt.remoteAddr,
|
||||
}
|
||||
req.Header.Set(tt.headerKey, tt.headerVal)
|
||||
|
||||
result := getClientIP(req)
|
||||
if result != tt.expectedIP {
|
||||
t.Errorf("getClientIP() = %q, want %q", result, tt.expectedIP)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue