Add unit tests for pulse-sensor-proxy utility functions
- hashIPToUID: 11 test cases covering IP hashing for rate limiting (determinism, range bounds, collision detection, boundary values) - extractNodesFromYAML: 17 test cases covering YAML node list parsing (map format, list format, mixed types, edge cases) First test files for config_cmd.go and http_server.go utilities.
This commit is contained in:
parent
39d02d62a3
commit
af80f303a5
2 changed files with 303 additions and 0 deletions
160
cmd/pulse-sensor-proxy/config_cmd_test.go
Normal file
160
cmd/pulse-sensor-proxy/config_cmd_test.go
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExtractNodesFromYAML(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected []string
|
||||||
|
}{
|
||||||
|
// Map format with allowed_nodes key
|
||||||
|
{
|
||||||
|
name: "map format with allowed_nodes key",
|
||||||
|
input: `allowed_nodes:
|
||||||
|
- node1
|
||||||
|
- node2
|
||||||
|
- node3`,
|
||||||
|
expected: []string{"node1", "node2", "node3"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "map format with other keys",
|
||||||
|
input: `metrics_address: 127.0.0.1:9127
|
||||||
|
allowed_nodes:
|
||||||
|
- host1
|
||||||
|
- host2
|
||||||
|
ssh_timeout: 30s`,
|
||||||
|
expected: []string{"host1", "host2"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "map format empty allowed_nodes",
|
||||||
|
input: `allowed_nodes: []
|
||||||
|
other_key: value`,
|
||||||
|
expected: nil,
|
||||||
|
},
|
||||||
|
|
||||||
|
// List format (bare YAML list)
|
||||||
|
{
|
||||||
|
name: "list format bare",
|
||||||
|
input: `- node1
|
||||||
|
- node2
|
||||||
|
- node3`,
|
||||||
|
expected: []string{"node1", "node2", "node3"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list format single item",
|
||||||
|
input: `- single-node`,
|
||||||
|
expected: []string{"single-node"},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Empty and edge cases
|
||||||
|
{
|
||||||
|
name: "empty input",
|
||||||
|
input: ``,
|
||||||
|
expected: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "only whitespace",
|
||||||
|
input: ` `,
|
||||||
|
expected: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty strings filtered",
|
||||||
|
input: `allowed_nodes:
|
||||||
|
- node1
|
||||||
|
- ""
|
||||||
|
- node2`,
|
||||||
|
expected: []string{"node1", "node2"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "null value in list",
|
||||||
|
input: `allowed_nodes:
|
||||||
|
- node1
|
||||||
|
- ~
|
||||||
|
- node2`,
|
||||||
|
expected: []string{"node1", "node2"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid YAML",
|
||||||
|
input: `{{{invalid`,
|
||||||
|
expected: nil,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Mixed content
|
||||||
|
{
|
||||||
|
name: "map format with comments",
|
||||||
|
input: `# This is a comment
|
||||||
|
allowed_nodes:
|
||||||
|
- node1
|
||||||
|
# inline comment
|
||||||
|
- node2`,
|
||||||
|
expected: []string{"node1", "node2"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "map format allowed_nodes not a list",
|
||||||
|
input: `allowed_nodes: not-a-list`,
|
||||||
|
expected: nil,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Different YAML structures
|
||||||
|
{
|
||||||
|
name: "nested structure ignored",
|
||||||
|
input: `config:
|
||||||
|
allowed_nodes:
|
||||||
|
- nested-node`,
|
||||||
|
expected: nil, // only top-level allowed_nodes is recognized
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "just null",
|
||||||
|
input: `~`,
|
||||||
|
expected: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := extractNodesFromYAML([]byte(tc.input))
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(result, tc.expected) {
|
||||||
|
t.Errorf("extractNodesFromYAML() = %v, want %v", result, tc.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractNodesFromYAML_WithNumericValues(t *testing.T) {
|
||||||
|
// YAML can have non-string values in lists
|
||||||
|
input := `allowed_nodes:
|
||||||
|
- node1
|
||||||
|
- 12345
|
||||||
|
- true
|
||||||
|
- node2`
|
||||||
|
|
||||||
|
result := extractNodesFromYAML([]byte(input))
|
||||||
|
|
||||||
|
// Only string values should be extracted
|
||||||
|
expected := []string{"node1", "node2"}
|
||||||
|
if !reflect.DeepEqual(result, expected) {
|
||||||
|
t.Errorf("extractNodesFromYAML() = %v, want %v", result, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractNodesFromYAML_ListWithMixedTypes(t *testing.T) {
|
||||||
|
// Bare list with mixed types
|
||||||
|
input := `- host1
|
||||||
|
- 42
|
||||||
|
- host2
|
||||||
|
- null
|
||||||
|
- host3`
|
||||||
|
|
||||||
|
result := extractNodesFromYAML([]byte(input))
|
||||||
|
|
||||||
|
// Only non-empty strings should be extracted
|
||||||
|
expected := []string{"host1", "host2", "host3"}
|
||||||
|
if !reflect.DeepEqual(result, expected) {
|
||||||
|
t.Errorf("extractNodesFromYAML() = %v, want %v", result, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
143
cmd/pulse-sensor-proxy/http_server_test.go
Normal file
143
cmd/pulse-sensor-proxy/http_server_test.go
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHashIPToUID(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ip string
|
||||||
|
wantMin uint32
|
||||||
|
wantMax uint32
|
||||||
|
wantSame bool // if true, verify determinism by checking same IP gives same result
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "IPv4 localhost",
|
||||||
|
ip: "127.0.0.1",
|
||||||
|
wantMin: 100000,
|
||||||
|
wantMax: 999999,
|
||||||
|
wantSame: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv4 standard",
|
||||||
|
ip: "192.168.1.100",
|
||||||
|
wantMin: 100000,
|
||||||
|
wantMax: 999999,
|
||||||
|
wantSame: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv4 another address",
|
||||||
|
ip: "10.0.0.1",
|
||||||
|
wantMin: 100000,
|
||||||
|
wantMax: 999999,
|
||||||
|
wantSame: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 localhost",
|
||||||
|
ip: "::1",
|
||||||
|
wantMin: 100000,
|
||||||
|
wantMax: 999999,
|
||||||
|
wantSame: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 full address",
|
||||||
|
ip: "2001:db8::1",
|
||||||
|
wantMin: 100000,
|
||||||
|
wantMax: 999999,
|
||||||
|
wantSame: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty string",
|
||||||
|
ip: "",
|
||||||
|
wantMin: 100000,
|
||||||
|
wantMax: 999999,
|
||||||
|
wantSame: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single character",
|
||||||
|
ip: "a",
|
||||||
|
wantMin: 100000,
|
||||||
|
wantMax: 999999,
|
||||||
|
wantSame: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "long string",
|
||||||
|
ip: "this-is-a-very-long-hostname-that-might-be-used.example.com",
|
||||||
|
wantMin: 100000,
|
||||||
|
wantMax: 999999,
|
||||||
|
wantSame: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := hashIPToUID(tc.ip)
|
||||||
|
|
||||||
|
// Check range
|
||||||
|
if result < tc.wantMin || result > tc.wantMax {
|
||||||
|
t.Errorf("hashIPToUID(%q) = %d, want in range [%d, %d]",
|
||||||
|
tc.ip, result, tc.wantMin, tc.wantMax)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check determinism
|
||||||
|
if tc.wantSame {
|
||||||
|
result2 := hashIPToUID(tc.ip)
|
||||||
|
if result != result2 {
|
||||||
|
t.Errorf("hashIPToUID(%q) not deterministic: got %d then %d",
|
||||||
|
tc.ip, result, result2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHashIPToUID_DifferentInputsProduceDifferentHashes(t *testing.T) {
|
||||||
|
ips := []string{
|
||||||
|
"127.0.0.1",
|
||||||
|
"192.168.1.1",
|
||||||
|
"192.168.1.2",
|
||||||
|
"10.0.0.1",
|
||||||
|
"::1",
|
||||||
|
"2001:db8::1",
|
||||||
|
}
|
||||||
|
|
||||||
|
hashes := make(map[uint32]string)
|
||||||
|
collisions := 0
|
||||||
|
|
||||||
|
for _, ip := range ips {
|
||||||
|
hash := hashIPToUID(ip)
|
||||||
|
if existing, found := hashes[hash]; found {
|
||||||
|
// Collision found - not necessarily an error but worth noting
|
||||||
|
collisions++
|
||||||
|
t.Logf("Hash collision: %q and %q both produce %d", ip, existing, hash)
|
||||||
|
}
|
||||||
|
hashes[hash] = ip
|
||||||
|
}
|
||||||
|
|
||||||
|
// With only 6 inputs and 900000 possible outputs, collisions should be rare
|
||||||
|
if collisions > 1 {
|
||||||
|
t.Errorf("Too many collisions (%d) for %d inputs", collisions, len(ips))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHashIPToUID_BoundaryValues(t *testing.T) {
|
||||||
|
// Test that the function correctly produces values in the expected range
|
||||||
|
// even for edge cases
|
||||||
|
|
||||||
|
tests := []string{
|
||||||
|
"", // empty
|
||||||
|
"\x00", // null byte
|
||||||
|
"\xff\xff\xff", // high bytes
|
||||||
|
"0.0.0.0",
|
||||||
|
"255.255.255.255",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ip := range tests {
|
||||||
|
result := hashIPToUID(ip)
|
||||||
|
if result < 100000 || result > 999999 {
|
||||||
|
t.Errorf("hashIPToUID(%q) = %d, out of expected range [100000, 999999]",
|
||||||
|
ip, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue