test: add unit tests for 6 previously-untested packages (R7)
Added pure unit tests for all packages that had 0% coverage. Where pure logic was entangled with DB calls, extracted testable helpers first. internal/domain (0% -> 100%): - TestIsNil, TestCanTransition (all 30 state transitions), TestSentinelErrors, TestSignalTransitionsComplete internal/learning (0% -> 26.2%): - Refactored processGroup to extract 4 pure helpers: countOutcomes, computeConfidence, shouldValidate, shouldQuarantine - TestWilsonLowerBound (monotonicity, edge cases, sample-size cap) - TestCountOutcomes, TestComputeConfidence, TestShouldValidate, TestShouldQuarantine (table-driven) - Remaining gap: extractPatterns/processGroup DB calls need make test-db internal/policy (39% -> 50%): - Extracted determineRoute from ClassifySignal (pure route logic) - TestDetermineRoute (7 cases covering global/entity kill-switches, approval) - Remaining gap: ClassifySignal/computeBlastRadius need DB mock internal/knowledge (0% -> 14.2%): - TestContentHash, TestStr, TestStrSlice, TestMapVal, TestToPGArray - Documented latent bug: toPGArray doesn't escape " or \\ in tags - Remaining gap: ingest* functions need make test-db internal/actuator (0% -> 14.7%): - TestSSHErrorClassString, TestClassifySSHError (11 cases incl. net.Error mock) - TestParseProcedure, TestSetDefaultSSHTimeout - Circuit breaker full state-machine test (open/close/reset/per-target) - Remaining gap: ExecuteProcedure/ProvisionLXC need SSH+DB fixtures internal/scheduler (0% -> 7.3%): - TestParsePingLatency (Linux/macOS formats), TestAllowlistedScript - TestEvaluateSeverity (threshold logic, crit:0 skip, signalKind fallback) - Remaining gap: checkHTTP/checkTCP need httptest; runCheckPass needs DB internal/notifier (0% -> 6.4%): - TestHashToken, TestGenerateApprovalToken (HMAC re-derivation) - Remaining gap: checkReaction/sendMatrixAlert need httptest; DB funcs need make test-db All tests pass with -race. domain hits its 60% gate at 100%. The remaining packages need integration tests (make test-db) and/or httptest-based tests to reach their coverage gates — tracked as follow-up.
This commit is contained in:
208
internal/scheduler/scheduler_test.go
Normal file
208
internal/scheduler/scheduler_test.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package scheduler
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParsePingLatency(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
out []byte
|
||||
want float64
|
||||
}{
|
||||
{
|
||||
name: "linux rtt format",
|
||||
out: []byte("PING host (1.2.3.4): 56 data bytes\n--- host ping statistics ---\n5 packets transmitted, 5 received, 0% packet loss\nrtt min/avg/max/mdev = 0.1/2.5/5.0/1.2 ms\n"),
|
||||
want: 2.5,
|
||||
},
|
||||
{
|
||||
name: "macos round-trip format",
|
||||
out: []byte("PING host (1.2.3.4): 56 data bytes\n--- host ping statistics ---\n5 packets transmitted, 5 received, 0% packet loss\nround-trip min/avg/max/stddev = 0.1/2.5/5.0/1.2 ms\n"),
|
||||
want: 2.5,
|
||||
},
|
||||
{
|
||||
name: "no match empty string",
|
||||
out: []byte(""),
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "malformed number in avg slot",
|
||||
out: []byte("rtt min/avg/max/mdev = 0.1/abc/5.0/1.2 ms\n"),
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "truly empty input nil",
|
||||
out: nil,
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "linux integer avg",
|
||||
out: []byte("rtt min/avg/max/mdev = 1/3/5/0.5 ms\n"),
|
||||
want: 3.0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := parsePingLatency(tc.out)
|
||||
if got != tc.want {
|
||||
t.Fatalf("parsePingLatency(%q) = %v, want %v", tc.out, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowlistedScript(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in string
|
||||
want bool
|
||||
}{
|
||||
{name: "cpu_check.sh", in: "cpu_check.sh", want: true},
|
||||
{name: "disk_usage_check.sh", in: "disk_usage_check.sh", want: true},
|
||||
{name: "hyphen allowed", in: "foo-bar.sh", want: true},
|
||||
{name: "underscore allowed", in: "foo_bar.sh", want: true},
|
||||
{name: "uppercase rejected", in: "Foo.sh", want: false},
|
||||
{name: "bare .sh rejected (no name)", in: ".sh", want: false},
|
||||
{name: "path traversal rejected", in: "../etc/passwd", want: false},
|
||||
{name: "empty rejected", in: "", want: false},
|
||||
// foo.sh.sh: regex ^[a-z][a-z0-9_-]+\.sh$ — the [a-z0-9_-]+ cannot
|
||||
// cross the first '.', so after matching "foo.sh" the trailing ".sh"
|
||||
// breaks the $ anchor → no match.
|
||||
{name: "doubled .sh.sh rejected", in: "foo.sh.sh", want: false},
|
||||
{name: "wrong extension rejected", in: "foo.txt", want: false},
|
||||
{name: "single char name rejected (needs 2+)", in: "a.sh", want: false},
|
||||
{name: "two char name accepted", in: "ab.sh", want: true},
|
||||
{name: "digit after first char", in: "a1.sh", want: true},
|
||||
{name: "leading digit rejected", in: "1foo.sh", want: false},
|
||||
{name: "dot in middle rejected", in: "foo.bar.sh", want: false},
|
||||
{name: "space rejected", in: "foo bar.sh", want: false},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := allowlistedScript(tc.in)
|
||||
if got != tc.want {
|
||||
t.Fatalf("allowlistedScript(%q) = %v, want %v", tc.in, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateSeverity(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
kind string
|
||||
signalKind string
|
||||
config []byte
|
||||
metrics map[string]float64
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty config down signal",
|
||||
kind: "ping",
|
||||
signalKind: "down",
|
||||
config: nil,
|
||||
metrics: map[string]float64{},
|
||||
want: "critical",
|
||||
},
|
||||
{
|
||||
name: "empty config high_temp signal",
|
||||
kind: "ssh-script",
|
||||
signalKind: "high_temp",
|
||||
config: nil,
|
||||
metrics: map[string]float64{},
|
||||
want: "warning",
|
||||
},
|
||||
{
|
||||
name: "crit only exceeded",
|
||||
kind: "ssh-script",
|
||||
signalKind: "high_temp",
|
||||
config: []byte(`{"temp":{"crit":90}}`),
|
||||
metrics: map[string]float64{"temp": 95},
|
||||
want: "critical",
|
||||
},
|
||||
{
|
||||
name: "warn only exceeded",
|
||||
kind: "ssh-script",
|
||||
signalKind: "high_temp",
|
||||
config: []byte(`{"temp":{"warn":80}}`),
|
||||
metrics: map[string]float64{"temp": 85},
|
||||
want: "warning",
|
||||
},
|
||||
{
|
||||
name: "warn and crit, warn exceeded only",
|
||||
kind: "ssh-script",
|
||||
signalKind: "high_temp",
|
||||
config: []byte(`{"temp":{"warn":80,"crit":90}}`),
|
||||
metrics: map[string]float64{"temp": 85},
|
||||
want: "warning",
|
||||
},
|
||||
{
|
||||
name: "warn and crit, crit exceeded",
|
||||
kind: "ssh-script",
|
||||
signalKind: "high_temp",
|
||||
config: []byte(`{"temp":{"warn":80,"crit":90}}`),
|
||||
metrics: map[string]float64{"temp": 95},
|
||||
want: "critical",
|
||||
},
|
||||
{
|
||||
name: "metric below thresholds falls through to warning",
|
||||
kind: "ssh-script",
|
||||
signalKind: "high_temp",
|
||||
config: []byte(`{"temp":{"warn":80,"crit":90}}`),
|
||||
metrics: map[string]float64{"temp": 70},
|
||||
want: "warning",
|
||||
},
|
||||
{
|
||||
name: "metric below thresholds falls through to down critical",
|
||||
kind: "ping",
|
||||
signalKind: "down",
|
||||
config: []byte(`{"ping_latency_ms":{"warn":100,"crit":200}}`),
|
||||
metrics: map[string]float64{"ping_latency_ms": 50},
|
||||
want: "critical",
|
||||
},
|
||||
{
|
||||
name: "crit zero skipped falls through",
|
||||
kind: "ssh-script",
|
||||
signalKind: "high_temp",
|
||||
config: []byte(`{"temp":{"crit":0}}`),
|
||||
metrics: map[string]float64{"temp": 95},
|
||||
want: "warning",
|
||||
},
|
||||
{
|
||||
name: "metric not in thresholds ignored",
|
||||
kind: "ssh-script",
|
||||
signalKind: "high_temp",
|
||||
config: []byte(`{"temp":{"warn":80,"crit":90}}`),
|
||||
metrics: map[string]float64{"cpu": 99},
|
||||
want: "warning",
|
||||
},
|
||||
{
|
||||
name: "equal to warn triggers warning",
|
||||
kind: "ssh-script",
|
||||
signalKind: "high_temp",
|
||||
config: []byte(`{"temp":{"warn":80,"crit":90}}`),
|
||||
metrics: map[string]float64{"temp": 80},
|
||||
want: "warning",
|
||||
},
|
||||
{
|
||||
name: "equal to crit triggers critical",
|
||||
kind: "ssh-script",
|
||||
signalKind: "high_temp",
|
||||
config: []byte(`{"temp":{"warn":80,"crit":90}}`),
|
||||
metrics: map[string]float64{"temp": 90},
|
||||
want: "critical",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := evaluateSeverity(tc.kind, tc.signalKind, tc.config, tc.metrics)
|
||||
if got != tc.want {
|
||||
t.Fatalf("evaluateSeverity(%q, %q, %s, %v) = %q, want %q",
|
||||
tc.kind, tc.signalKind, tc.config, tc.metrics, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user