diff --git a/cmd/pulse-sensor-proxy/config_cmd_test.go b/cmd/pulse-sensor-proxy/config_cmd_test.go new file mode 100644 index 0000000..09e0ee7 --- /dev/null +++ b/cmd/pulse-sensor-proxy/config_cmd_test.go @@ -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) + } +} diff --git a/cmd/pulse-sensor-proxy/http_server_test.go b/cmd/pulse-sensor-proxy/http_server_test.go new file mode 100644 index 0000000..113ca52 --- /dev/null +++ b/cmd/pulse-sensor-proxy/http_server_test.go @@ -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) + } + } +}