From 2eb9e61f0ea2f06dbd054aa1783870ec689175fe Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 26 Nov 2025 13:56:30 +0000 Subject: [PATCH] test: add unit tests for utils package - Test ID generation (uniqueness, format) - Test JSON response writing (various types, headers) - Test boolean parsing (truthy/falsy values) - Test environment variable trimming - Test data directory resolution - Test large payload handling --- internal/utils/utils_test.go | 282 +++++++++++++++++++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 internal/utils/utils_test.go diff --git a/internal/utils/utils_test.go b/internal/utils/utils_test.go new file mode 100644 index 000000000..0f80a1281 --- /dev/null +++ b/internal/utils/utils_test.go @@ -0,0 +1,282 @@ +package utils + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" +) + +func TestGenerateID(t *testing.T) { + tests := []struct { + prefix string + }{ + {"test"}, + {"alert"}, + {"node"}, + {""}, + } + + for _, tc := range tests { + t.Run(tc.prefix, func(t *testing.T) { + id := GenerateID(tc.prefix) + + // Should start with prefix + if tc.prefix != "" && !strings.HasPrefix(id, tc.prefix+"-") { + t.Errorf("GenerateID(%q) = %q, should start with %q-", tc.prefix, id, tc.prefix) + } + + // Should be non-empty + if id == "" { + t.Error("GenerateID() returned empty string") + } + }) + } + + // IDs should be unique + id1 := GenerateID("test") + id2 := GenerateID("test") + if id1 == id2 { + t.Error("GenerateID() returned duplicate IDs") + } +} + +func TestWriteJSONResponse(t *testing.T) { + tests := []struct { + name string + data interface{} + expected string + }{ + { + name: "simple object", + data: map[string]string{"key": "value"}, + expected: `{"key":"value"}`, + }, + { + name: "array", + data: []int{1, 2, 3}, + expected: `[1,2,3]`, + }, + { + name: "nested object", + data: map[string]interface{}{"outer": map[string]int{"inner": 42}}, + expected: `{"outer":{"inner":42}}`, + }, + { + name: "empty object", + data: map[string]string{}, + expected: `{}`, + }, + { + name: "null", + data: nil, + expected: `null`, + }, + { + name: "struct", + data: struct { + Name string `json:"name"` + Count int `json:"count"` + }{Name: "test", Count: 5}, + expected: `{"name":"test","count":5}`, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + w := httptest.NewRecorder() + + err := WriteJSONResponse(w, tc.data) + if err != nil { + t.Fatalf("WriteJSONResponse() error: %v", err) + } + + // Check content type + ct := w.Header().Get("Content-Type") + if ct != "application/json" { + t.Errorf("Content-Type = %q, want %q", ct, "application/json") + } + + // Check body + body := w.Body.String() + if body != tc.expected { + t.Errorf("Body = %q, want %q", body, tc.expected) + } + }) + } +} + +func TestWriteJSONResponse_InvalidData(t *testing.T) { + w := httptest.NewRecorder() + + // Channels cannot be marshaled to JSON + ch := make(chan int) + err := WriteJSONResponse(w, ch) + if err == nil { + t.Error("WriteJSONResponse() should fail on unmarshalable data") + } +} + +func TestWriteJSONResponse_StatusCode(t *testing.T) { + w := httptest.NewRecorder() + + // Set status code before writing + w.WriteHeader(http.StatusCreated) + + err := WriteJSONResponse(w, map[string]string{"status": "created"}) + if err != nil { + t.Fatalf("WriteJSONResponse() error: %v", err) + } + + if w.Code != http.StatusCreated { + t.Errorf("Status code = %d, want %d", w.Code, http.StatusCreated) + } +} + +func TestParseBool(t *testing.T) { + tests := []struct { + input string + expected bool + }{ + // Truthy values + {"true", true}, + {"TRUE", true}, + {"True", true}, + {"1", true}, + {"yes", true}, + {"YES", true}, + {"Yes", true}, + {"y", true}, + {"Y", true}, + {"on", true}, + {"ON", true}, + {"On", true}, + + // Falsy values + {"false", false}, + {"FALSE", false}, + {"0", false}, + {"no", false}, + {"n", false}, + {"off", false}, + {"", false}, + {"random", false}, + {"2", false}, + + // With whitespace + {" true ", true}, + {" false ", false}, + {"\ttrue\n", true}, + } + + for _, tc := range tests { + t.Run(tc.input, func(t *testing.T) { + result := ParseBool(tc.input) + if result != tc.expected { + t.Errorf("ParseBool(%q) = %v, want %v", tc.input, result, tc.expected) + } + }) + } +} + +func TestGetenvTrim(t *testing.T) { + // Set test environment variable + testKey := "TEST_GETENVTRIM_VAR" + + tests := []struct { + name string + value string + expected string + }{ + {"no whitespace", "value", "value"}, + {"leading space", " value", "value"}, + {"trailing space", "value ", "value"}, + {"both sides", " value ", "value"}, + {"tabs", "\tvalue\t", "value"}, + {"newlines", "\nvalue\n", "value"}, + {"empty", "", ""}, + {"only whitespace", " ", ""}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + os.Setenv(testKey, tc.value) + defer os.Unsetenv(testKey) + + result := GetenvTrim(testKey) + if result != tc.expected { + t.Errorf("GetenvTrim(%q) with value %q = %q, want %q", testKey, tc.value, result, tc.expected) + } + }) + } + + // Test unset variable + os.Unsetenv(testKey) + result := GetenvTrim(testKey) + if result != "" { + t.Errorf("GetenvTrim() for unset var = %q, want empty string", result) + } +} + +func TestGetDataDir(t *testing.T) { + envKey := "PULSE_DATA_DIR" + originalValue := os.Getenv(envKey) + defer func() { + if originalValue != "" { + os.Setenv(envKey, originalValue) + } else { + os.Unsetenv(envKey) + } + }() + + // Test with env var set + os.Setenv(envKey, "/custom/data/dir") + result := GetDataDir() + if result != "/custom/data/dir" { + t.Errorf("GetDataDir() with env = %q, want /custom/data/dir", result) + } + + // Test with env var unset (default) + os.Unsetenv(envKey) + result = GetDataDir() + if result != "/etc/pulse" { + t.Errorf("GetDataDir() without env = %q, want /etc/pulse", result) + } + + // Test with empty env var (should use default) + os.Setenv(envKey, "") + result = GetDataDir() + if result != "/etc/pulse" { + t.Errorf("GetDataDir() with empty env = %q, want /etc/pulse", result) + } +} + +func TestWriteJSONResponse_LargePayload(t *testing.T) { + w := httptest.NewRecorder() + + // Create a large payload + data := make([]map[string]interface{}, 1000) + for i := 0; i < 1000; i++ { + data[i] = map[string]interface{}{ + "index": i, + "name": strings.Repeat("x", 100), + } + } + + err := WriteJSONResponse(w, data) + if err != nil { + t.Fatalf("WriteJSONResponse() error on large payload: %v", err) + } + + // Verify it's valid JSON + var decoded []map[string]interface{} + if err := json.Unmarshal(w.Body.Bytes(), &decoded); err != nil { + t.Errorf("Response is not valid JSON: %v", err) + } + + if len(decoded) != 1000 { + t.Errorf("Decoded length = %d, want 1000", len(decoded)) + } +}