3d7ab5a49e
Categorize raw error strings into DNS/TCP/TLS/HTTP/ICMP/TMO/PRIV so users get instant triage from the monitor list without opening the detail panel. - Status column shows DOWN:DNS, DOWN:TLS, DOWN:HTTP, etc. - Inline NAME column errors prefixed with category tag [DNS], [TLS] - Detail panel shows connection chain checklist for HTTP monitors (✓ DNS → ✓ TCP → ✗ TLS → · HTTP) pinpointing failure layer - All display-side only — no database or model changes
187 lines
5.7 KiB
Go
187 lines
5.7 KiB
Go
package tui
|
|
|
|
import "testing"
|
|
|
|
func TestClassifyError(t *testing.T) {
|
|
tests := []struct {
|
|
reason string
|
|
siteType string
|
|
statusCode int
|
|
want ErrorCategory
|
|
}{
|
|
{"", "http", 0, ErrCatUnknown},
|
|
{"some unknown error", "http", 0, ErrCatUnknown},
|
|
|
|
// PRIV
|
|
{"target resolves to private IP", "http", 0, ErrCatPrivate},
|
|
{"blocked: host resolves to private address 10.0.0.1", "port", 0, ErrCatPrivate},
|
|
|
|
// TLS
|
|
{"SSL certificate expired", "http", 0, ErrCatTLS},
|
|
{"tls handshake failure", "http", 0, ErrCatTLS},
|
|
{"x509: certificate has expired", "http", 0, ErrCatTLS},
|
|
{"x509: certificate signed by unknown authority", "http", 0, ErrCatTLS},
|
|
|
|
// DNS
|
|
{"DNS query failed: NXDOMAIN", "http", 0, ErrCatDNS},
|
|
{"DNS RCODE: SERVFAIL", "dns", 0, ErrCatDNS},
|
|
{"dial tcp: lookup example.com: no such host", "http", 0, ErrCatDNS},
|
|
{"DNS query failed: server misbehaving", "dns", 0, ErrCatDNS},
|
|
|
|
// TMO
|
|
{"dial tcp 1.2.3.4:443: i/o timeout", "http", 0, ErrCatTimeout},
|
|
{"context deadline exceeded", "http", 0, ErrCatTimeout},
|
|
{"Get https://example.com: context canceled", "http", 0, ErrCatTimeout},
|
|
|
|
// ICMP
|
|
{"no ICMP response", "ping", 0, ErrCatICMP},
|
|
{"ping setup: permission denied", "ping", 0, ErrCatICMP},
|
|
{"ping failed: packet loss", "ping", 0, ErrCatICMP},
|
|
|
|
// TCP
|
|
{"dial tcp 1.2.3.4:80: connect: connection refused", "port", 0, ErrCatTCP},
|
|
{"connection reset by peer", "http", 0, ErrCatTCP},
|
|
{"dial tcp: no route to host", "http", 0, ErrCatTCP},
|
|
{"network unreachable", "http", 0, ErrCatTCP},
|
|
|
|
// HTTP
|
|
{"HTTP 500 (expected 200-299)", "http", 500, ErrCatHTTP},
|
|
{"HTTP 403 (expected 200-299)", "http", 403, ErrCatHTTP},
|
|
{"keyword not found", "http", 200, ErrCatHTTP},
|
|
{"", "http", 502, ErrCatUnknown},
|
|
{"unexpected status", "http", 404, ErrCatHTTP},
|
|
}
|
|
for _, tt := range tests {
|
|
got := classifyError(tt.reason, tt.siteType, tt.statusCode)
|
|
if got != tt.want {
|
|
t.Errorf("classifyError(%q, %q, %d) = %q, want %q",
|
|
tt.reason, tt.siteType, tt.statusCode, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCategoryTag(t *testing.T) {
|
|
tests := []struct {
|
|
cat ErrorCategory
|
|
want string
|
|
}{
|
|
{ErrCatDNS, "[DNS]"},
|
|
{ErrCatTCP, "[TCP]"},
|
|
{ErrCatTLS, "[TLS]"},
|
|
{ErrCatHTTP, "[HTTP]"},
|
|
{ErrCatICMP, "[ICMP]"},
|
|
{ErrCatTimeout, "[TMO]"},
|
|
{ErrCatPrivate, "[PRIV]"},
|
|
{ErrCatUnknown, ""},
|
|
}
|
|
for _, tt := range tests {
|
|
got := categoryTag(tt.cat)
|
|
if got != tt.want {
|
|
t.Errorf("categoryTag(%q) = %q, want %q", tt.cat, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestConnectionChain(t *testing.T) {
|
|
t.Run("nil for non-http", func(t *testing.T) {
|
|
if chain := connectionChain("no ICMP response", "ping", 0, false); chain != nil {
|
|
t.Errorf("expected nil for ping, got %v", chain)
|
|
}
|
|
})
|
|
|
|
t.Run("nil for empty error", func(t *testing.T) {
|
|
if chain := connectionChain("", "http", 0, true); chain != nil {
|
|
t.Errorf("expected nil for empty error, got %v", chain)
|
|
}
|
|
})
|
|
|
|
t.Run("DNS failure HTTPS", func(t *testing.T) {
|
|
chain := connectionChain("no such host", "http", 0, true)
|
|
if len(chain) != 4 {
|
|
t.Fatalf("expected 4 steps, got %d", len(chain))
|
|
}
|
|
if chain[0].Status != stepFailed {
|
|
t.Errorf("DNS step: want failed, got %d", chain[0].Status)
|
|
}
|
|
if chain[1].Status != stepSkipped {
|
|
t.Errorf("TCP step: want skipped, got %d", chain[1].Status)
|
|
}
|
|
if chain[2].Status != stepSkipped {
|
|
t.Errorf("TLS step: want skipped, got %d", chain[2].Status)
|
|
}
|
|
if chain[3].Status != stepSkipped {
|
|
t.Errorf("HTTP step: want skipped, got %d", chain[3].Status)
|
|
}
|
|
})
|
|
|
|
t.Run("TCP failure HTTP", func(t *testing.T) {
|
|
chain := connectionChain("connection refused", "http", 0, false)
|
|
if len(chain) != 3 {
|
|
t.Fatalf("expected 3 steps (no TLS), got %d", len(chain))
|
|
}
|
|
if chain[0].Status != stepPassed {
|
|
t.Errorf("DNS step: want passed, got %d", chain[0].Status)
|
|
}
|
|
if chain[1].Status != stepFailed {
|
|
t.Errorf("TCP step: want failed, got %d", chain[1].Status)
|
|
}
|
|
if chain[2].Status != stepSkipped {
|
|
t.Errorf("HTTP step: want skipped, got %d", chain[2].Status)
|
|
}
|
|
})
|
|
|
|
t.Run("TLS failure HTTPS", func(t *testing.T) {
|
|
chain := connectionChain("x509: certificate has expired", "http", 0, true)
|
|
if len(chain) != 4 {
|
|
t.Fatalf("expected 4 steps, got %d", len(chain))
|
|
}
|
|
if chain[0].Status != stepPassed {
|
|
t.Errorf("DNS: want passed, got %d", chain[0].Status)
|
|
}
|
|
if chain[1].Status != stepPassed {
|
|
t.Errorf("TCP: want passed, got %d", chain[1].Status)
|
|
}
|
|
if chain[2].Status != stepFailed {
|
|
t.Errorf("TLS: want failed, got %d", chain[2].Status)
|
|
}
|
|
if chain[3].Status != stepSkipped {
|
|
t.Errorf("HTTP: want skipped, got %d", chain[3].Status)
|
|
}
|
|
})
|
|
|
|
t.Run("HTTP failure HTTPS", func(t *testing.T) {
|
|
chain := connectionChain("HTTP 500 (expected 200-299)", "http", 500, true)
|
|
if len(chain) != 4 {
|
|
t.Fatalf("expected 4 steps, got %d", len(chain))
|
|
}
|
|
for i := 0; i < 3; i++ {
|
|
if chain[i].Status != stepPassed {
|
|
t.Errorf("step %d: want passed, got %d", i, chain[i].Status)
|
|
}
|
|
}
|
|
if chain[3].Status != stepFailed {
|
|
t.Errorf("HTTP: want failed, got %d", chain[3].Status)
|
|
}
|
|
if chain[3].Detail != "HTTP 500" {
|
|
t.Errorf("HTTP detail: want %q, got %q", "HTTP 500", chain[3].Detail)
|
|
}
|
|
})
|
|
|
|
t.Run("timeout maps to TCP step", func(t *testing.T) {
|
|
chain := connectionChain("i/o timeout", "http", 0, true)
|
|
if chain[1].Status != stepFailed {
|
|
t.Errorf("TCP step: want failed for timeout, got %d", chain[1].Status)
|
|
}
|
|
if chain[1].Detail != "i/o timeout" {
|
|
t.Errorf("detail: want %q, got %q", "i/o timeout", chain[1].Detail)
|
|
}
|
|
})
|
|
|
|
t.Run("private IP maps to DNS step", func(t *testing.T) {
|
|
chain := connectionChain("target resolves to private IP", "http", 0, true)
|
|
if chain[0].Status != stepFailed {
|
|
t.Errorf("DNS step: want failed for private IP, got %d", chain[0].Status)
|
|
}
|
|
})
|
|
}
|