Files
Pulse/internal/utils/utils_test.go
rcourtman 3fdf753a5b Enhance devcontainer and CI workflows
- Add persistent volume mounts for Go/npm caches (faster rebuilds)
- Add shell config with helpful aliases and custom prompt
- Add comprehensive devcontainer documentation
- Add pre-commit hooks for Go formatting and linting
- Use go-version-file in CI workflows instead of hardcoded versions
- Simplify docker compose commands with --wait flag
- Add gitignore entries for devcontainer auth files

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 22:29:15 +00:00

382 lines
8.3 KiB
Go

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))
}
}
func TestNormalizeVersion(t *testing.T) {
tests := []struct {
input string
expected string
}{
// With v prefix
{"v4.33.1", "4.33.1"},
{"v1.0.0", "1.0.0"},
{"v0.0.1-rc1", "0.0.1-rc1"},
// Without v prefix
{"4.33.1", "4.33.1"},
{"1.0.0", "1.0.0"},
{"0.0.1-rc1", "0.0.1-rc1"},
// With whitespace
{" v4.33.1", "4.33.1"},
{"v4.33.1 ", "4.33.1"},
{" v4.33.1 ", "4.33.1"},
{"\tv4.33.1\n", "4.33.1"},
// Edge cases
{"", ""},
{"v", ""},
{" ", ""},
{"vv4.33.1", "v4.33.1"}, // Only removes one v
// Build metadata (semver +suffix should be stripped)
{"4.36.2+git.14.g469307d6.dirty", "4.36.2"},
{"v4.36.2+build123", "4.36.2"},
{"1.0.0+20231215", "1.0.0"},
{"v1.0.0-rc1+build.456", "1.0.0-rc1"},
}
for _, tc := range tests {
t.Run(tc.input, func(t *testing.T) {
result := NormalizeVersion(tc.input)
if result != tc.expected {
t.Errorf("NormalizeVersion(%q) = %q, want %q", tc.input, result, tc.expected)
}
})
}
}
func TestCompareVersions(t *testing.T) {
tests := []struct {
a string
b string
expected int
}{
// Equal versions
{"4.33.1", "4.33.1", 0},
{"v4.33.1", "4.33.1", 0},
{"4.33.1", "v4.33.1", 0},
{"1.0.0", "1.0.0", 0},
// a > b (a is newer)
{"4.33.2", "4.33.1", 1},
{"4.34.0", "4.33.1", 1},
{"5.0.0", "4.33.1", 1},
{"4.33.10", "4.33.9", 1},
{"4.33.1", "4.33", 1}, // Missing patch = 0
// a < b (b is newer)
{"4.33.1", "4.33.2", -1},
{"4.33.1", "4.34.0", -1},
{"4.33.1", "5.0.0", -1},
{"4.33.9", "4.33.10", -1},
{"4.33", "4.33.1", -1},
// With v prefix
{"v4.34.0", "v4.33.1", 1},
{"v4.33.1", "v4.34.0", -1},
// Edge cases
{"0.0.1", "0.0.0", 1},
{"0.0.0", "0.0.1", -1},
{"1.0", "0.9.9", 1},
// Build metadata should be ignored (semver +suffix)
// This is the critical fix for the infinite agent update loop bug
{"4.36.2+git.14.g469307d6.dirty", "4.36.2", 0}, // Dirty == clean
{"4.36.2", "4.36.2+git.14.g469307d6.dirty", 0}, // Clean == dirty
{"v4.36.2+build123", "v4.36.2", 0}, // With v prefix
{"4.36.3", "4.36.2+git.14.g469307d6.dirty", 1}, // Newer beats dirty
{"4.36.2+git.14.g469307d6.dirty", "4.36.3", -1}, // Dirty older than newer
}
for _, tc := range tests {
name := tc.a + "_vs_" + tc.b
t.Run(name, func(t *testing.T) {
result := CompareVersions(tc.a, tc.b)
if result != tc.expected {
t.Errorf("CompareVersions(%q, %q) = %d, want %d", tc.a, tc.b, result, tc.expected)
}
})
}
}