From a32aa606005cb4ae1f46c2780cac2af42ec70162 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 30 Nov 2025 07:27:59 +0000 Subject: [PATCH] Add unit tests for config utility functions (config) Test coverage for IsPasswordHashed, IsValidDiscoveryEnvironment, and splitAndTrim functions. 63 test cases covering bcrypt hash validation, discovery environment validation, and comma-separated string parsing. --- internal/config/config_utils_test.go | 167 +++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 internal/config/config_utils_test.go diff --git a/internal/config/config_utils_test.go b/internal/config/config_utils_test.go new file mode 100644 index 0000000..09b15cf --- /dev/null +++ b/internal/config/config_utils_test.go @@ -0,0 +1,167 @@ +package config + +import ( + "strings" + "testing" +) + +func TestIsPasswordHashed(t *testing.T) { + // Generate a valid bcrypt hash for testing (60 chars, starts with $2) + validBcryptHash := "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy" + + tests := []struct { + name string + password string + expected bool + }{ + // Valid bcrypt hashes (60 chars, $2a/$2b/$2y prefix) + {"valid bcrypt 2a", validBcryptHash, true}, + {"valid bcrypt 2b", "$2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy", true}, + {"valid bcrypt 2y", "$2y$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy", true}, + {"valid bcrypt different cost", "$2a$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.BFB8hMSWy6s/FO", true}, + + // Invalid: wrong prefix + {"no $2 prefix", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456", false}, + {"plain password", "mysecretpassword", false}, + {"$1 prefix (MD5)", "$1$saltsalt$hashed", false}, + {"$5 prefix (SHA-256)", "$5$rounds=5000$saltsalt$hash", false}, + {"$6 prefix (SHA-512)", "$6$rounds=5000$saltsalt$hash", false}, + + // Invalid: wrong length + {"too short", "$2a$10$abc", false}, + {"59 chars (truncated)", "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhW", false}, + {"61 chars (too long)", "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWyX", false}, + {"55 chars (truncated, logs warning)", "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL1", false}, + + // Edge cases + {"empty string", "", false}, + {"just $2", "$2", false}, + {"$2a$", "$2a$", false}, + {"$2a$10$", "$2a$10$", false}, + {"whitespace", " ", false}, + {"$2 but short", "$2a$10$short", false}, + + // Real-world edge cases + {"hash with newline", validBcryptHash + "\n", false}, + {"hash with trailing space", validBcryptHash + " ", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsPasswordHashed(tt.password) + if result != tt.expected { + t.Errorf("IsPasswordHashed(%q) = %v, want %v", tt.password, result, tt.expected) + } + }) + } +} + +func TestIsValidDiscoveryEnvironment(t *testing.T) { + tests := []struct { + name string + value string + expected bool + }{ + // Valid values (case insensitive) + {"empty string (auto)", "", true}, + {"auto lowercase", "auto", true}, + {"AUTO uppercase", "AUTO", true}, + {"Auto mixed", "Auto", true}, + {"native", "native", true}, + {"NATIVE", "NATIVE", true}, + {"docker_host", "docker_host", true}, + {"DOCKER_HOST", "DOCKER_HOST", true}, + {"docker_bridge", "docker_bridge", true}, + {"DOCKER_BRIDGE", "DOCKER_BRIDGE", true}, + {"lxc_privileged", "lxc_privileged", true}, + {"LXC_PRIVILEGED", "LXC_PRIVILEGED", true}, + {"lxc_unprivileged", "lxc_unprivileged", true}, + {"LXC_UNPRIVILEGED", "LXC_UNPRIVILEGED", true}, + + // Valid with whitespace trimming + {"auto with leading space", " auto", true}, + {"auto with trailing space", "auto ", true}, + {"auto with both spaces", " auto ", true}, + {"native with tabs", "\tnative\t", true}, + + // Invalid values + {"unknown value", "unknown", false}, + {"typo dockerhost", "dockerhost", false}, + {"typo docker-host", "docker-host", false}, + {"kubernetes", "kubernetes", false}, + {"vm", "vm", false}, + {"baremetal", "baremetal", false}, + {"container", "container", false}, + {"partial match docker", "docker", false}, + {"partial match lxc", "lxc", false}, + {"underscore only", "_", false}, + {"random string", "xyzabc", false}, + {"numeric", "123", false}, + {"special chars", "docker@host", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsValidDiscoveryEnvironment(tt.value) + if result != tt.expected { + t.Errorf("IsValidDiscoveryEnvironment(%q) = %v, want %v", tt.value, result, tt.expected) + } + }) + } +} + +func TestSplitAndTrim(t *testing.T) { + tests := []struct { + name string + input string + expected []string + }{ + // Basic splitting + {"single value", "one", []string{"one"}}, + {"two values", "one,two", []string{"one", "two"}}, + {"three values", "one,two,three", []string{"one", "two", "three"}}, + + // Whitespace handling + {"value with leading space", " one", []string{"one"}}, + {"value with trailing space", "one ", []string{"one"}}, + {"values with spaces around comma", "one , two", []string{"one", "two"}}, + {"values with spaces", " one , two , three ", []string{"one", "two", "three"}}, + {"tabs and spaces", "\tone\t,\ttwo\t", []string{"one", "two"}}, + + // Empty handling + {"empty string", "", []string{}}, + {"only comma", ",", []string{}}, + {"multiple commas", ",,,", []string{}}, + {"comma with spaces", " , , ", []string{}}, + {"empty between values", "one,,two", []string{"one", "two"}}, + {"empty at start", ",one,two", []string{"one", "two"}}, + {"empty at end", "one,two,", []string{"one", "two"}}, + + // Real-world examples + {"CIDR list", "10.0.0.0/8, 192.168.0.0/16", []string{"10.0.0.0/8", "192.168.0.0/16"}}, + {"hostnames", "node1.local, node2.local, node3.local", []string{"node1.local", "node2.local", "node3.local"}}, + {"mixed content", "http://a.com, https://b.com", []string{"http://a.com", "https://b.com"}}, + + // Edge cases + {"single space", " ", []string{}}, + {"value is just spaces", " ", []string{}}, + {"comma surrounded by spaces value", "one, ,two", []string{"one", "two"}}, + {"long value", strings.Repeat("a", 100), []string{strings.Repeat("a", 100)}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := splitAndTrim(tt.input) + if len(result) != len(tt.expected) { + t.Errorf("splitAndTrim(%q) returned %d items, want %d", tt.input, len(result), len(tt.expected)) + t.Errorf("got: %v, want: %v", result, tt.expected) + return + } + for i, v := range result { + if v != tt.expected[i] { + t.Errorf("splitAndTrim(%q)[%d] = %q, want %q", tt.input, i, v, tt.expected[i]) + } + } + }) + } +}