Files
Pulse/internal/api/http_metrics_test.go
rcourtman 597c1f4850 Add unit tests for HTTP metrics utility functions
83 test cases covering classifyStatus, isNumeric, looksLikeUUID,
normalizeSegment, and normalizeRoute functions used for Prometheus
metrics aggregation. Tests cover status code classification, numeric
string validation, UUID format detection, URL segment normalization,
and route path normalization with query param stripping.
2025-11-30 02:48:05 +00:00

234 lines
6.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package api
import "testing"
func TestClassifyStatus(t *testing.T) {
tests := []struct {
name string
status int
want string
}{
// Server errors (5xx)
{"500 internal server error", 500, "server_error"},
{"501 not implemented", 501, "server_error"},
{"502 bad gateway", 502, "server_error"},
{"503 service unavailable", 503, "server_error"},
{"599 edge case", 599, "server_error"},
// Client errors (4xx)
{"400 bad request", 400, "client_error"},
{"401 unauthorized", 401, "client_error"},
{"403 forbidden", 403, "client_error"},
{"404 not found", 404, "client_error"},
{"429 too many requests", 429, "client_error"},
{"499 edge case", 499, "client_error"},
// Success (2xx)
{"200 OK", 200, "none"},
{"201 created", 201, "none"},
{"204 no content", 204, "none"},
// Redirects (3xx)
{"301 moved permanently", 301, "none"},
{"302 found", 302, "none"},
{"304 not modified", 304, "none"},
// Informational (1xx)
{"100 continue", 100, "none"},
{"101 switching protocols", 101, "none"},
// Edge cases
{"0 zero status", 0, "none"},
{"399 boundary below client error", 399, "none"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := classifyStatus(tt.status)
if got != tt.want {
t.Errorf("classifyStatus(%d) = %q, want %q", tt.status, got, tt.want)
}
})
}
}
func TestIsNumeric(t *testing.T) {
tests := []struct {
name string
input string
want bool
}{
// Valid numeric strings
{"single digit", "0", true},
{"single digit 9", "9", true},
{"multiple digits", "123", true},
{"large number", "9876543210", true},
{"leading zeros", "007", true},
// Invalid strings
{"empty string", "", false},
{"letter", "a", false},
{"mixed alphanumeric", "123abc", false},
{"space", " ", false},
{"number with space", "12 34", false},
{"negative number", "-123", false},
{"decimal number", "12.34", false},
{"hexadecimal prefix", "0x10", false},
{"special characters", "12@34", false},
{"unicode digits", "", false}, // fullwidth digits
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := isNumeric(tt.input)
if got != tt.want {
t.Errorf("isNumeric(%q) = %v, want %v", tt.input, got, tt.want)
}
})
}
}
func TestLooksLikeUUID(t *testing.T) {
tests := []struct {
name string
input string
want bool
}{
// Valid UUIDs
{"lowercase uuid", "550e8400-e29b-41d4-a716-446655440000", true},
{"uppercase uuid", "550E8400-E29B-41D4-A716-446655440000", true},
{"mixed case uuid", "550e8400-E29B-41d4-A716-446655440000", true},
{"nil uuid", "00000000-0000-0000-0000-000000000000", true},
{"max uuid", "ffffffff-ffff-ffff-ffff-ffffffffffff", true},
// Invalid UUIDs - wrong length
{"empty string", "", false},
{"too short", "550e8400-e29b-41d4-a716", false},
{"too long", "550e8400-e29b-41d4-a716-4466554400001", false},
{"35 chars", "550e8400-e29b-41d4-a716-44665544000", false},
{"37 chars", "550e8400-e29b-41d4-a716-4466554400001", false},
// Invalid UUIDs - wrong dash positions
{"dash at wrong position 0", "-50e8400-e29b-41d4-a716-44665544000", false},
{"dash at wrong position 7", "550e840-0e29b-41d4-a716-446655440000", false},
{"no dashes", "550e8400e29b41d4a716446655440000xxxx", false},
{"extra dash", "550e8400-e29b-41d4-a716-4466-5544000", false},
// Invalid UUIDs - invalid characters
{"letter g", "g50e8400-e29b-41d4-a716-446655440000", false},
{"letter z", "550e8400-z29b-41d4-a716-446655440000", false},
{"space in uuid", "550e8400 e29b-41d4-a716-446655440000", false},
{"underscore", "550e8400_e29b-41d4-a716-446655440000", false},
{"special char", "550e8400-e29b-41d4-a716-44665544000!", false},
// Edge cases
{"all zeros no dashes wrong length", "00000000000000000000000000000000xxxx", false},
{"uuid with braces", "{550e8400-e29b-41d4-a716-446655440000}", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := looksLikeUUID(tt.input)
if got != tt.want {
t.Errorf("looksLikeUUID(%q) = %v, want %v", tt.input, got, tt.want)
}
})
}
}
func TestNormalizeSegment(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
// Numeric segments -> :id
{"numeric id", "123", ":id"},
{"single digit", "5", ":id"},
{"large number", "9999999999", ":id"},
// UUID segments -> :uuid
{"uuid", "550e8400-e29b-41d4-a716-446655440000", ":uuid"},
{"uppercase uuid", "550E8400-E29B-41D4-A716-446655440000", ":uuid"},
// Long tokens -> :token
{"33 char string", "abcdefghijklmnopqrstuvwxyz1234567", ":token"},
{"64 char string", "abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz12", ":token"},
// Regular segments preserved
{"api", "api", "api"},
{"v1", "v1", "v1"},
{"users", "users", "users"},
{"settings", "settings", "settings"},
{"exactly 32 chars", "abcdefghijklmnopqrstuvwxyz123456", "abcdefghijklmnopqrstuvwxyz123456"},
// Edge cases
{"empty string", "", ""},
{"single letter", "a", "a"},
{"mixed alphanumeric short", "user123", "user123"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := normalizeSegment(tt.input)
if got != tt.want {
t.Errorf("normalizeSegment(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
func TestNormalizeRoute(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
// Root and empty paths
{"empty string", "", "/"},
{"root path", "/", "/"},
{"root with trailing slash", "//", "/"},
// Simple paths
{"api path", "/api", "/api"},
{"two segments", "/api/v1", "/api/v1"},
{"three segments", "/api/v1/users", "/api/v1/users"},
// Paths with numeric IDs
{"path with numeric id", "/api/users/123", "/api/users/:id"},
{"path with multiple ids", "/api/users/123/posts/456", "/api/users/:id/posts/:id"},
// Paths with UUIDs
{"path with uuid", "/api/users/550e8400-e29b-41d4-a716-446655440000", "/api/users/:uuid"},
// Paths with long tokens
{"path with long token", "/api/auth/abcdefghijklmnopqrstuvwxyz1234567", "/api/auth/:token"},
// Query parameters stripped
{"path with query params", "/api/users?page=1&limit=10", "/api/users"},
{"path with id and query", "/api/users/123?include=posts", "/api/users/:id"},
// Segment limit (max 5)
{"exactly 5 segments", "/a/b/c/d/e", "/a/b/c/d/e"},
{"6 segments truncated", "/a/b/c/d/e/f", "/a/b/c/d/e"},
{"7 segments truncated", "/a/b/c/d/e/f/g", "/a/b/c/d/e"},
// Mixed content
{"complex path", "/api/v1/users/123/posts/550e8400-e29b-41d4-a716-446655440000", "/api/v1/users/:id/posts"},
// Edge cases
{"double slashes", "/api//users", "/api/users"},
{"trailing slash", "/api/users/", "/api/users"},
{"leading double slash", "//api/users", "/api/users"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := normalizeRoute(tt.input)
if got != tt.want {
t.Errorf("normalizeRoute(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}