Add Go branch-coverage tests for pure config and helper packages

Adds table-driven branch-coverage unit tests for previously untested pure
helper functions across internal/alerts/config, internal/config,
internal/ai/safety, internal/ai/modelresolution, internal/operationreceipt,
internal/models, internal/securityutil and pkg/securityutil. New test files
only, with no source changes.

Covers alert-config normalization and validation, sensitive-path and
redaction classifiers, URL normalizers, provider model resolution,
operation-receipt decoding, credential masking, and account-to-org role
mapping. 57 TestBranchCov functions in 12 files, all vet and gofmt clean.
This commit is contained in:
rcourtman
2026-07-16 10:19:51 +01:00
parent 4dcba3a1d8
commit 47f2bdaed4
12 changed files with 3817 additions and 0 deletions
@@ -0,0 +1,295 @@
package modelresolution
import (
"testing"
"github.com/rcourtman/pulse-go-rewrite/internal/ai/providers"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
)
// TestBranchCovSelectRecommendedProviderModel exercises every branch of
// SelectRecommendedProviderModel: the empty/unusable-catalog returns, the
// "first usable becomes best" seeding, and each tie-break arm inside
// recommendedModelBetter (blessed -> suitability rank -> Notable -> CreatedAt
// presence -> CreatedAt value -> lexical sort key on Name -> sort key on ID ->
// stable index tie-break).
func TestBranchCovSelectRecommendedProviderModel(t *testing.T) {
t.Parallel()
tests := []struct {
name string
models []providers.ModelInfo
wantID string
wantOK bool
// wantExtra lets a case assert a non-compared field on the winner (used
// to prove the stable index tie-break returns the earlier entry).
wantExtra func(*testing.T, providers.ModelInfo)
}{
{
name: "nil slice returns zero value and false",
models: nil,
wantOK: false,
},
{
name: "empty slice returns zero value and false",
models: []providers.ModelInfo{},
wantOK: false,
},
{
name: "only blank IDs return false",
models: []providers.ModelInfo{
{ID: ""},
{ID: " "},
{ID: "\t"},
},
wantOK: false,
},
{
name: "only specialized entries return false",
models: []providers.ModelInfo{
{ID: "text-embedding-3"},
{ID: "tts-1"},
{ID: "omni-moderation"},
{Name: "whisper transcription"},
},
wantOK: false,
},
{
name: "specialized marker in name filters entry out",
models: []providers.ModelInfo{
{ID: "acme-1", Name: "Acme Embedding v2"},
{ID: "gpt-4o"},
},
wantID: "gpt-4o",
wantOK: true,
},
{
name: "blank and specialized entries skipped, later valid wins",
models: []providers.ModelInfo{
{ID: " "},
{ID: ""},
{ID: "text-embedding-3"},
{ID: "gpt-4o"},
},
wantID: "gpt-4o",
wantOK: true,
},
{
name: "single usable chat model wins",
models: []providers.ModelInfo{
{ID: "gpt-4o"},
},
wantID: "gpt-4o",
wantOK: true,
},
{
name: "blessed model beats non-blessed regardless of order",
models: []providers.ModelInfo{
{ID: "gpt-4o"},
{ID: "qwen3:8b"},
},
wantID: "qwen3:8b",
wantOK: true,
},
{
name: "blessed model beats non-blessed when blessed is first",
models: []providers.ModelInfo{
{ID: "qwen3:8b"},
{ID: "gpt-4o"},
},
wantID: "qwen3:8b",
wantOK: true,
},
{
name: "blessed equivalent id qwen3:latest also wins",
models: []providers.ModelInfo{
{ID: "gpt-4o"},
{ID: "qwen3:latest"},
},
wantID: "qwen3:latest",
wantOK: true,
},
{
name: "blessed id matched case-insensitively and returned verbatim",
models: []providers.ModelInfo{
{ID: "gpt-4o"},
{ID: " QWEN3:8B "},
},
wantID: " QWEN3:8B ",
wantOK: true,
},
{
name: "chat rank beats unknown rank",
models: []providers.ModelInfo{
{ID: "gpt-4o"},
{ID: "research-only-alpha"},
},
wantID: "gpt-4o",
wantOK: true,
},
{
name: "chat rank beats unknown rank when unknown is first",
models: []providers.ModelInfo{
{ID: "research-only-alpha"},
{ID: "gpt-4o"},
},
wantID: "gpt-4o",
wantOK: true,
},
{
name: "notable flag wins among equal rank",
models: []providers.ModelInfo{
{ID: "gpt-4o", Notable: false},
{ID: "gpt-4.1", Notable: true},
},
wantID: "gpt-4.1",
wantOK: true,
},
{
name: "notable flag wins when notable is first",
models: []providers.ModelInfo{
{ID: "gpt-4.1", Notable: true},
{ID: "gpt-4o", Notable: false},
},
wantID: "gpt-4.1",
wantOK: true,
},
{
name: "presence of created timestamp beats absence",
models: []providers.ModelInfo{
{ID: "gpt-4o", Notable: true, CreatedAt: 0},
{ID: "gpt-4.1", Notable: true, CreatedAt: 1700000000},
},
wantID: "gpt-4.1",
wantOK: true,
},
{
name: "newer created timestamp wins",
models: []providers.ModelInfo{
{ID: "gpt-4o", Notable: true, CreatedAt: 1700000000},
{ID: "gpt-4.1", Notable: true, CreatedAt: 1800000000},
},
wantID: "gpt-4.1",
wantOK: true,
},
{
name: "newer created timestamp wins when newer is first",
models: []providers.ModelInfo{
{ID: "gpt-4.1", Notable: true, CreatedAt: 1800000000},
{ID: "gpt-4o", Notable: true, CreatedAt: 1700000000},
},
wantID: "gpt-4.1",
wantOK: true,
},
{
name: "lexical sort key by name breaks tie",
models: []providers.ModelInfo{
{ID: "gpt-z", Notable: true, Name: "Zeta"},
{ID: "gpt-a", Notable: true, Name: "Alpha"},
},
wantID: "gpt-a",
wantOK: true,
},
{
name: "sort key falls back to id when name is empty",
models: []providers.ModelInfo{
{ID: "zzz-unknown"},
{ID: "aaa-unknown"},
},
wantID: "aaa-unknown",
wantOK: true,
},
{
name: "stable index tie-break returns earlier entry",
models: []providers.ModelInfo{
{ID: "gpt-4o", Name: "Same", Description: "first"},
{ID: "gpt-4o", Name: "Same", Description: "second"},
},
wantID: "gpt-4o",
wantOK: true,
wantExtra: func(t *testing.T, got providers.ModelInfo) {
t.Helper()
if got.Description != "first" {
t.Fatalf("index tie-break returned Description %q, want %q (earlier index)", got.Description, "first")
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, ok := SelectRecommendedProviderModel(tt.models)
if ok != tt.wantOK {
t.Fatalf("SelectRecommendedProviderModel ok = %v, want %v (got=%#v)", ok, tt.wantOK, got)
}
if !tt.wantOK {
return
}
if got.ID != tt.wantID {
t.Fatalf("SelectRecommendedProviderModel ID = %q, want %q", got.ID, tt.wantID)
}
if tt.wantExtra != nil {
tt.wantExtra(t, got)
}
})
}
}
// TestBranchCovIsModelUsableWithConfig exercises every branch of
// IsModelUsableWithConfig (chatOnly=false): empty/whitespace model, retired
// quickstart provider, nil config, configured-but-unconfigured provider, and
// the configured-provider true path. It also pins the notable behaviour that
// the non-chat variant does NOT perform chat-suitability filtering, so a
// specialized model on a configured provider still reports usable.
func TestBranchCovIsModelUsableWithConfig(t *testing.T) {
t.Parallel()
tests := []struct {
name string
cfg *config.AIConfig
model string
want bool
}{
{name: "nil config returns false", cfg: nil, model: "openai:gpt-4o", want: false},
{name: "empty model returns false", cfg: &config.AIConfig{OpenAIAPIKey: "sk"}, model: "", want: false},
{name: "whitespace-only model returns false", cfg: &config.AIConfig{OpenAIAPIKey: "sk"}, model: " ", want: false},
{name: "quickstart provider returns false", cfg: &config.AIConfig{}, model: "quickstart:foo", want: false},
{name: "quickstart provider returns false even with credentials present", cfg: &config.AIConfig{OpenAIAPIKey: "sk"}, model: "quickstart:gpt-4o", want: false},
{name: "explicit configured openai returns true", cfg: &config.AIConfig{OpenAIAPIKey: "sk"}, model: "openai:gpt-4o", want: true},
{name: "explicit configured anthropic returns true", cfg: &config.AIConfig{AnthropicAPIKey: "ak"}, model: "anthropic:claude-sonnet-4", want: true},
{name: "explicit configured deepseek returns true", cfg: &config.AIConfig{DeepSeekAPIKey: "ds"}, model: "deepseek:deepseek-v4", want: true},
{name: "explicit configured gemini returns true", cfg: &config.AIConfig{GeminiAPIKey: "gm"}, model: "gemini:gemini-3-flash", want: true},
{name: "explicit configured openrouter returns true", cfg: &config.AIConfig{OpenRouterAPIKey: "or"}, model: "openrouter:openai/gpt-4o", want: true},
{name: "ollama configured via base url returns true", cfg: &config.AIConfig{OllamaBaseURL: "http://localhost:11434"}, model: "ollama:llama3.2", want: true},
{name: "whitespace-padded model is trimmed then accepted", cfg: &config.AIConfig{OpenAIAPIKey: "sk"}, model: " openai:gpt-4o ", want: true},
{name: "heuristic gpt prefix resolves to openai configured", cfg: &config.AIConfig{OpenAIAPIKey: "sk"}, model: "gpt-4o", want: true},
{name: "heuristic claude prefix resolves to anthropic configured", cfg: &config.AIConfig{AnthropicAPIKey: "ak"}, model: "claude-sonnet-4", want: true},
{name: "unconfigured anthropic returns false", cfg: &config.AIConfig{OpenAIAPIKey: "sk"}, model: "anthropic:claude-sonnet-4", want: false},
{name: "unrecognized local name defaults to ollama unconfigured", cfg: &config.AIConfig{OpenAIAPIKey: "sk"}, model: "totally-unknown-local-model", want: false},
{name: "specialized model still usable on configured provider (chatOnly=false skips suitability)", cfg: &config.AIConfig{OpenAIAPIKey: "sk"}, model: "openai:text-embedding-3", want: true},
{name: "specialized realtime model still usable on configured provider", cfg: &config.AIConfig{OpenAIAPIKey: "sk"}, model: "openai:gpt-4o-realtime", want: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := IsModelUsableWithConfig(tt.cfg, tt.model); got != tt.want {
t.Fatalf("IsModelUsableWithConfig(cfg, %q) = %v, want %v", tt.model, got, tt.want)
}
})
}
}
// TestBranchCovIsModelUsableWithConfig_NonNilZeroConfig pins the nil-vs-zero
// distinction: a non-nil config with no credentials set must report false
// (driven by HasProvider, not by the nil guard), distinguishing the
// cfg==nil short-circuit from the unconfigured-provider branch.
func TestBranchCovIsModelUsableWithConfig_NonNilZeroConfig(t *testing.T) {
t.Parallel()
cfg := &config.AIConfig{}
if IsModelUsableWithConfig(cfg, "openai:gpt-4o") {
t.Fatalf("IsModelUsableWithConfig on zero-value non-nil config = true, want false")
}
}
@@ -0,0 +1,485 @@
package safety
import (
"strings"
"testing"
)
// This file adds branch-coverage tests for the path- and redaction-related
// helpers in package safety. It targets branches that the existing test files
// leave uncovered: empty/zero inputs, every switch arm (including defaults),
// error/short-circuit returns, normalization edge cases, and the multi-arm
// PEM/kv redaction state machine in RedactSensitiveText / RedactSensitiveValue.
func TestBranchCovIsSensitivePath(t *testing.T) {
tests := []struct {
name string
path string
wantSense bool
wantReasons []string // any of these substrings is acceptable
}{
// Empty / zero input -> early return.
{"empty path", "", false, nil},
// System credential databases: switch-case arms.
{"etc shadow", "/etc/shadow", true, []string{"system credential file"}},
{"etc gshadow", "/etc/gshadow", true, []string{"system credential file"}},
{"etc sudoers", "/etc/sudoers", true, []string{"system credential file"}},
// filepath.Clean + ToLower normalization paths.
{"cleaned dotdot shadow", "/etc/../etc/shadow", true, []string{"system credential file"}},
{"trailing slash shadow", "/etc/shadow/", true, []string{"system credential file"}},
{"uppercase shadow lowercased", "/ETC/SHADOW", true, []string{"system credential file"}},
{"mixed-case Sudoers", "/EtC/SuDoErS", true, []string{"system credential file"}},
// SSH directory contains-arm.
{"ssh dir config", "/home/u/.ssh/config", true, []string{"ssh key/config directory"}},
{"ssh dir anywhere", "/root/.ssh/random", true, []string{"ssh key/config directory"}},
// SSH key suffix loop, every name. NOTE: these paths deliberately do NOT
// contain "/.ssh/" so they fall through the contains-arm and reach the
// HasSuffix("/<name>") arm (otherwise the contains-arm wins and returns
// "ssh key/config directory" first).
{"ssh id_rsa suffix", "/tmp/id_rsa", true, []string{"ssh key material"}},
{"ssh id_ed25519 suffix", "/tmp/id_ed25519", true, []string{"ssh key material"}},
{"ssh authorized_keys suffix", "/root/authorized_keys", true, []string{"ssh key material"}},
{"ssh known_hosts suffix", "/root/known_hosts", true, []string{"ssh key material"}},
// Bare relative ssh filename (no slash) does NOT match the suffix arm.
{"bare id_rsa no slash", "id_rsa", false, nil},
// Secrets directory prefix loop, every prefix.
{"run secrets prefix", "/run/secrets/db", true, []string{"secrets directory"}},
{"var run secrets prefix", "/var/run/secrets/db", true, []string{"secrets directory"}},
{"etc secrets prefix", "/etc/secrets/db", true, []string{"secrets directory"}},
{"secrets root prefix", "/secrets/db", true, []string{"secrets directory"}},
// /proc/<pid>/environ combined predicate.
{"proc environ", "/proc/1/environ", true, []string{"process environment file"}},
// Only one half of the /proc/ + /environ predicate -> not sensitive.
{"proc without environ", "/proc/1/status", false, nil},
{"environ without proc", "/tmp/environ", false, nil},
// Private key / cert extension loop, every ext.
{"pem ext", "/srv/tls/server.pem", true, []string{"private key or certificate file"}},
{"key ext", "/srv/tls/server.key", true, []string{"private key or certificate file"}},
{"p12 ext", "/srv/cert.p12", true, []string{"private key or certificate file"}},
{"pfx ext", "/srv/cert.pfx", true, []string{"private key or certificate file"}},
// ai.enc store: HasSuffix OR Contains arms.
{"ai enc suffix", "/var/lib/pulse/ai.enc", true, []string{"pulse encrypted AI provider config store"}},
{"ai enc contains embedded", "/tmp/ai.enc.bak", true, []string{"pulse encrypted AI provider config store"}},
// Credentials dotfile suffix loop, every base name.
{"env dotfile", "/app/.env", true, []string{"credentials dotfile"}},
{"npmrc dotfile", "/home/u/.npmrc", true, []string{"credentials dotfile"}},
{"pypirc dotfile", "/home/u/.pypirc", true, []string{"credentials dotfile"}},
{"netrc dotfile", "/home/u/.netrc", true, []string{"credentials dotfile"}},
{"aws credentials", "/home/u/.aws/credentials", true, []string{"credentials dotfile"}},
// Bare relative ".env" (no slash) -> suffix arm does NOT match.
{"bare env no slash", ".env", false, nil},
// Wholly benign path -> final return (false, "").
{"benign readme", "/srv/app/README.md", false, nil},
{"benign source", "/srv/app/main.go", false, nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, reason := IsSensitivePath(tt.path)
if got != tt.wantSense {
t.Fatalf("IsSensitivePath(%q) sense = %v, want %v (reason=%q)", tt.path, got, tt.wantSense, reason)
}
if !tt.wantSense {
if reason != "" {
t.Errorf("IsSensitivePath(%q) benign returned reason %q, want empty", tt.path, reason)
}
return
}
// For sensitive hits, reason must be non-empty and contain an expected substring.
if reason == "" {
t.Fatalf("IsSensitivePath(%q) returned true with empty reason", tt.path)
}
matched := false
for _, want := range tt.wantReasons {
if strings.Contains(reason, want) {
matched = true
break
}
}
if !matched {
t.Errorf("IsSensitivePath(%q) reason = %q, want one of %v", tt.path, reason, tt.wantReasons)
}
})
}
}
func TestBranchCovCommandTouchesSensitivePath(t *testing.T) {
tests := []struct {
name string
cmd string
wantSense bool
wantReasons []string
}{
// Empty command -> early return. (Lowercasing "" still yields "".)
{"empty command", "", false, nil},
// Each high-confidence substring arm of the loop.
{"etc shadow substring", "cat /etc/shadow", true, []string{"references sensitive path"}},
{"etc gshadow substring", "cat /etc/gshadow", true, []string{"references sensitive path"}},
{"etc sudoers substring", "grep root /etc/sudoers", true, []string{"references sensitive path"}},
{"run secrets substring", "ls /run/secrets/db", true, []string{"references sensitive path"}},
{"var run secrets substring", "ls /var/run/secrets/db", true, []string{"references sensitive path"}},
{"ssh substring", "cat /home/u/.ssh/id_rsa", true, []string{"references sensitive path"}},
{"ai enc substring", "cp /var/lib/pulse/ai.enc /tmp", true, []string{"references sensitive path"}},
// Case-insensitivity: input is lowercased before matching.
{"uppercase etc shadow", "CAT /ETC/SHADOW", true, []string{"references sensitive path"}},
// Combined /proc/ + environ predicate: both substrings required.
{"proc environ combined", "cat /proc/42/environ", true, []string{"references process environment file"}},
{"proc only not environ", "cat /proc/42/status", false, nil},
{"environ only not proc", "cat /tmp/environ", false, nil},
// Benign command -> final return (false, "").
{"benign ls", "ls -la /tmp", false, nil},
{"benign ps", "ps aux", false, nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, reason := CommandTouchesSensitivePath(tt.cmd)
if got != tt.wantSense {
t.Fatalf("CommandTouchesSensitivePath(%q) sense = %v, want %v (reason=%q)", tt.cmd, got, tt.wantSense, reason)
}
if !tt.wantSense {
if reason != "" {
t.Errorf("CommandTouchesSensitivePath(%q) benign returned reason %q, want empty", tt.cmd, reason)
}
return
}
if reason == "" {
t.Fatalf("CommandTouchesSensitivePath(%q) returned true with empty reason", tt.cmd)
}
matched := false
for _, want := range tt.wantReasons {
if strings.Contains(reason, want) {
matched = true
break
}
}
if !matched {
t.Errorf("CommandTouchesSensitivePath(%q) reason = %q, want one of %v", tt.cmd, reason, tt.wantReasons)
}
})
}
}
func TestBranchCovIsSensitiveValueCarrierFieldName(t *testing.T) {
positive := []string{
"value", "values", "default", "defaults", "example", "examples", "enum", "const",
}
negative := []string{
// Empty / whitespace-only -> normalized "" -> switch default.
"",
" ",
"\t\n",
// Unrelated names -> default arm.
"description",
"username",
"type",
// Looks related but normalizes to something not in the switch.
"defaultvalue", // default + value concatenated
"exampleurl",
}
for _, n := range positive {
if !IsSensitiveValueCarrierFieldName(n) {
t.Errorf("IsSensitiveValueCarrierFieldName(%q) = false, want true", n)
}
}
for _, n := range negative {
if IsSensitiveValueCarrierFieldName(n) {
t.Errorf("IsSensitiveValueCarrierFieldName(%q) = true, want false", n)
}
}
// Normalization: case-insensitive and punctuation stripping.
normalizations := map[string]string{
"VALUE": "value",
"Defaults": "defaults",
"Example": "example",
" enum ": "enum",
"ex-ample": "example", // hyphen stripped -> "example" matches
}
for raw, want := range normalizations {
// Both the raw and the equivalent canonical form must classify identically.
if IsSensitiveValueCarrierFieldName(raw) != IsSensitiveValueCarrierFieldName(want) {
t.Errorf("normalization mismatch: raw=%q canonical=%q classify differently", raw, want)
}
if !IsSensitiveValueCarrierFieldName(raw) {
t.Errorf("IsSensitiveValueCarrierFieldName(%q) = false, want true (canonical %q)", raw, want)
}
}
}
func TestBranchCovIsSensitiveFieldName(t *testing.T) {
positive := []string{
"password", "passwd", "passphrase", "secret", "token", "apikey",
"clientsecret", "privatekey", "accesstoken", "refreshtoken",
"authorization", "xapikey", "credential", "credentials",
}
negative := []string{
// Empty -> early return (normalized == "").
"",
" ",
// Unrelated names -> switch default.
"username",
"displayname",
"email",
// Sensitive-looking but normalizes away from the list.
"secretpassword", // concatenated, not in list
}
for _, n := range positive {
if !IsSensitiveFieldName(n) {
t.Errorf("IsSensitiveFieldName(%q) = false, want true", n)
}
}
for _, n := range negative {
if IsSensitiveFieldName(n) {
t.Errorf("IsSensitiveFieldName(%q) = true, want false", n)
}
}
// Normalization: punctuation/case-insensitivity maps to canonical arms.
cases := map[string]string{
"API-Key": "apikey",
"api_key": "apikey",
"ClientSecret": "clientsecret",
"client-secret": "clientsecret",
"X-API-Key": "xapikey",
"Password": "password",
"password!!!": "password", // non-alnum stripped
" token ": "token",
}
for raw, want := range cases {
if IsSensitiveFieldName(raw) != IsSensitiveFieldName(want) {
t.Errorf("normalization mismatch: raw=%q canonical=%q classify differently", raw, want)
}
if !IsSensitiveFieldName(raw) {
t.Errorf("IsSensitiveFieldName(%q) = false, want true (canonical %q)", raw, want)
}
}
}
func TestBranchCovRedactSensitiveText(t *testing.T) {
// Empty input -> early return ("", 0).
t.Run("empty input returns empty", func(t *testing.T) {
out, n := RedactSensitiveText("")
if out != "" || n != 0 {
t.Fatalf("RedactSensitiveText(\"\") = (%q, %d), want (\"\", 0)", out, n)
}
})
// Newline/whitespace-only input: no redactions. The "drop empty lines" loop
// only drops *truly-empty* lines; whitespace-only lines (e.g. " ") survive.
// For input "\n\n \n" the surviving line is " ", so output is " " (count 0).
t.Run("whitespace lines are not secret and pass through", func(t *testing.T) {
out, n := RedactSensitiveText("\n\n \n")
if n != 0 {
t.Fatalf("RedactSensitiveText(%q) count = %d, want 0", "\n\n \n", n)
}
if out != " " {
t.Errorf("RedactSensitiveText(%q) = %q, want %q (only truly-empty lines are dropped)", "\n\n \n", out, " ")
}
})
// kvSecretRE arm: only the value portion is replaced, key context preserved.
t.Run("kv secret value redacted key preserved", func(t *testing.T) {
out, n := RedactSensitiveText("password: hunter2")
if n != 1 {
t.Fatalf("expected 1 redaction, got %d (%q)", n, out)
}
if strings.Contains(out, "hunter2") {
t.Errorf("value leaked: %q", out)
}
if !strings.HasPrefix(out, "password:") || !strings.Contains(out, "[REDACTED]") {
t.Errorf("key context lost: %q", out)
}
})
// PEM state machine: begin marker -> "[REDACTED PEM BLOCK]", body dropped,
// end marker reached -> inPEM=false and the marker line is also dropped.
t.Run("pem block begin body and end", func(t *testing.T) {
input := strings.Join([]string{
"prelude",
"-----BEGIN PRIVATE KEY-----",
"ZmFrZS1iYXNlNjQtcGVtLmJvZHk=",
"-----END PRIVATE KEY-----",
"epilogue",
}, "\n")
out, n := RedactSensitiveText(input)
if n != 1 {
t.Fatalf("expected exactly 1 redaction (begin marker), got %d (%q)", n, out)
}
if !strings.Contains(out, "[REDACTED PEM BLOCK]") {
t.Errorf("missing PEM block marker: %q", out)
}
// Both base64 body and END marker must be stripped.
if strings.Contains(out, "ZmFrZS1iYXNlNjQtcGVtLmJvZHk=") {
t.Errorf("PEM body leaked: %q", out)
}
if strings.Contains(out, "END PRIVATE KEY") {
t.Errorf("PEM END marker should be dropped, present in %q", out)
}
// Lines outside the block are retained.
if !strings.Contains(out, "prelude") || !strings.Contains(out, "epilogue") {
t.Errorf("non-PEM lines dropped: %q", out)
}
})
// PEM state machine: unterminated block -> inPEM stays true to EOF,
// every subsequent line (including a would-be secret kv line) is blanked.
t.Run("unterminated pem swallows rest", func(t *testing.T) {
input := strings.Join([]string{
"-----BEGIN CERTIFICATE-----",
"line1",
"password: should-not-be-processed-by-kv-path",
}, "\n")
out, n := RedactSensitiveText(input)
if n != 1 {
t.Fatalf("expected exactly 1 redaction, got %d (%q)", n, out)
}
if !strings.Contains(out, "[REDACTED PEM BLOCK]") {
t.Errorf("missing PEM block marker: %q", out)
}
// The kv line inside the unterminated PEM is blanked, NOT kv-redacted.
if strings.Contains(out, "should-not-be-processed-by-kv-path") {
t.Errorf("PEM body line leaked: %q", out)
}
if strings.Contains(out, "[REDACTED]") {
t.Errorf("kv redactor should not run inside PEM block: %q", out)
}
})
// redactLineSecretPatterns: each provider-token regex produces its own marker.
// Covers AWS, JWT, OpenAI-style, Google API key, GitHub token arms.
t.Run("provider token patterns", func(t *testing.T) {
cases := []struct {
name string
line string
}{
{"aws access key", "id=AKIAIOSFODNN7EXAMPLE"},
{"jwt", "tok=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"},
{"openai key", "k=sk-abcdef123456"},
{"google api key", "k=AIzaSyABCdefghIJKlmnoPQRstuVWxyz1234567"},
{"github token", "t=ghp_abcdefghijklmnopqrstuvwxyz"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
out, n := RedactSensitiveText(c.line)
if n == 0 {
t.Fatalf("expected a redaction, got 0 for %q", c.line)
}
if strings.Contains(out, "AKIAIOSFODNN7EXAMPLE") && c.name == "aws access key" {
t.Errorf("aws key leaked: %q", out)
}
// All markers are documented as bracketed; ensure no raw secret remains
// and that the result contains a "[REDACTED" token.
if !strings.Contains(out, "[REDACTED") {
t.Errorf("missing redaction marker in %q", out)
}
})
}
})
// Header / URL / quoted-JSON arms (distinct from kvSecretRE).
t.Run("header and url and json forms", func(t *testing.T) {
input := strings.Join([]string{
`Authorization: Bearer abc123def456`,
`x-api-key: somekeyvalue`,
`https://user:pass@example.test/`,
`{"secret":"shh"}`,
`GET https://example.test/v1?key=AIzaSyAbcDefGhiJklMnoPqrStuVwxYz1234567`,
}, "\n")
out, n := RedactSensitiveText(input)
if n == 0 {
t.Fatalf("expected redactions, got 0")
}
for _, leak := range []string{"abc123def456", "somekeyvalue", "user:pass@", "shh", "AIzaSyAbcDefGhiJklMnoPqrStuVwxYz1234567"} {
if strings.Contains(out, leak) {
t.Errorf("leaked %q in %q", leak, out)
}
}
})
// No-match text passes through unchanged with count 0.
t.Run("no secrets returns unchanged", func(t *testing.T) {
in := "just a regular log line with no secrets"
out, n := RedactSensitiveText(in)
if out != in || n != 0 {
t.Fatalf("RedactSensitiveText(%q) = (%q, %d), want unchanged/0", in, out, n)
}
})
}
func TestBranchCovRedactSensitiveValue(t *testing.T) {
// Branch: input "" -> RedactSensitiveText returns ("", 0); TrimSpace == "" -> return as-is.
t.Run("empty input", func(t *testing.T) {
out, n := RedactSensitiveValue("")
if out != "" || n != 0 {
t.Fatalf("RedactSensitiveValue(\"\") = (%q, %d), want (\"\", 0)", out, n)
}
})
// Branch: redacted text is whitespace-only (non-empty but trims to "").
// RedactSensitiveText leaves the surviving whitespace line intact (" "),
// and the TrimSpace(redacted)=="" arm returns that untrimmed value with the
// inner count (0) WITHOUT forcing the [REDACTED] marker.
t.Run("whitespace only trims empty short circuits", func(t *testing.T) {
out, n := RedactSensitiveValue("\n\n \n")
if n != 0 {
t.Fatalf("RedactSensitiveValue(%q) count = %d, want 0", "\n\n \n", n)
}
if out != " " {
t.Errorf("RedactSensitiveValue(%q) = %q, want %q (TrimSpace arm preserves untrimmed redacted text)", "\n\n \n", out, " ")
}
})
// Branch: redacted already equals the canonical marker -> returned unchanged,
// count NOT incremented. Achievable by feeding the literal marker string.
t.Run("already redacted marker", func(t *testing.T) {
out, n := RedactSensitiveValue("[REDACTED]")
if out != "[REDACTED]" {
t.Fatalf("RedactSensitiveValue(\"[REDACTED]\") = %q, want \"[REDACTED]\"", out)
}
if n != 0 {
t.Errorf("expected count 0 for already-redacted marker, got %d", n)
}
})
// Branch: non-empty, non-marker plain value -> final arm forces the marker
// and increments count by 1 (key-context collapses any non-secret value).
t.Run("plain value collapsed to marker", func(t *testing.T) {
out, n := RedactSensitiveValue("just a plain string, nothing secret-shaped")
if out != "[REDACTED]" {
t.Fatalf("RedactSensitiveValue(plain) = %q, want \"[REDACTED]\"", out)
}
if n != 1 {
t.Errorf("expected count 1 for forced collapse, got %d", n)
}
})
// A value that itself contains a redactable token: count reflects the inner
// redaction (>=1) PLUS the final collapse increment.
t.Run("value with embedded token then collapsed", func(t *testing.T) {
out, n := RedactSensitiveValue("token=ghp_abcdefghijklmnopqrstuvwxyz")
if out != "[REDACTED]" {
t.Fatalf("RedactSensitiveValue(embedded) = %q, want \"[REDACTED]\"", out)
}
if n < 2 {
t.Errorf("expected count >= 2 (inner redaction + collapse), got %d", n)
}
})
}
@@ -0,0 +1,214 @@
package config_test
import (
"fmt"
"reflect"
"testing"
alertconfig "github.com/rcourtman/pulse-go-rewrite/internal/alerts/config"
)
// This file adds branch-coverage tests for the identity-normalization helpers
// in internal/alerts/config/identity.go:
// - CanonicalAlertResourceType (previously only exercised indirectly via the
// alerts facade wrapper; its switch arms and trim/lowercase normalization
// are covered directly here).
// - CanonicalResourceTypeKeys (legacy-alias rejection paths returning a true
// nil, case-insensitivity, and equivalence between display aliases and
// their canonical hyphenated forms).
// TestBranchCovCanonicalAlertResourceType exercises every switch arm of
// CanonicalAlertResourceType directly, including the whitespace trimming and
// case-folding performed before the switch, each multi-token display alias,
// every vsphere/virtual-machine variant, and the default passthrough.
func TestBranchCovCanonicalAlertResourceType(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
// Normalization: leading/trailing whitespace is trimmed and the value
// is lower-cased before the switch is consulted.
{name: "trim and lowercase", in: " Kubernetes Pod ", want: "pod"},
{name: "all uppercased input", in: "VMWARE VM", want: "vmware-vm"},
{name: "mixed case input", in: "TruENas SYStem", want: "truenas-system"},
// Multi-token display aliases -> hyphenated canonical forms.
{name: "kubernetes cluster", in: "kubernetes cluster", want: "k8s-cluster"},
{name: "kubernetes node", in: "kubernetes node", want: "k8s-node"},
{name: "kubernetes deployment", in: "kubernetes deployment", want: "k8s-deployment"},
{name: "kubernetes namespace", in: "kubernetes namespace", want: "k8s-namespace"},
{name: "kubernetes pod", in: "kubernetes pod", want: "pod"},
{name: "truenas system", in: "truenas system", want: "truenas-system"},
{name: "truenas pool", in: "truenas pool", want: "truenas-pool"},
{name: "truenas dataset", in: "truenas dataset", want: "truenas-dataset"},
{name: "truenas disk", in: "truenas disk", want: "truenas-disk"},
// vmware/vsphere host variants collapse to vmware-host.
{name: "vmware host", in: "vmware host", want: "vmware-host"},
{name: "vsphere host", in: "vsphere host", want: "vmware-host"},
// vmware/vsphere vm + "virtual machine" variants collapse to vmware-vm.
{name: "vmware vm", in: "vmware vm", want: "vmware-vm"},
{name: "vsphere vm", in: "vsphere vm", want: "vmware-vm"},
{name: "vmware virtual machine", in: "vmware virtual machine", want: "vmware-vm"},
{name: "vsphere virtual machine", in: "vsphere virtual machine", want: "vmware-vm"},
// datastore / network variants.
{name: "vmware datastore", in: "vmware datastore", want: "vmware-datastore"},
{name: "vsphere datastore", in: "vsphere datastore", want: "vmware-datastore"},
{name: "vmware network", in: "vmware network", want: "vmware-network"},
{name: "vsphere network", in: "vsphere network", want: "vmware-network"},
// Default arm: unknown tokens and already-canonical tokens pass through
// unchanged (after lowercasing/trimming).
{name: "unknown type passthrough", in: "custom-thing", want: "custom-thing"},
{name: "already canonical k8s-cluster", in: "k8s-cluster", want: "k8s-cluster"},
{name: "already canonical vmware-vm", in: "vmware-vm", want: "vmware-vm"},
{name: "single token node", in: "node", want: "node"},
// Empty / whitespace-only input: TrimSpace yields "" which hits default.
{name: "empty string", in: "", want: ""},
{name: "whitespace only", in: " ", want: ""},
{name: "tabs only", in: "\t\t", want: ""},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := alertconfig.CanonicalAlertResourceType(tc.in)
if got != tc.want {
t.Errorf("CanonicalAlertResourceType(%q) = %q, want %q", tc.in, got, tc.want)
}
})
}
}
// TestBranchCovCanonicalResourceTypeKeysLegacyNil verifies that inputs which
// are rejected as unsupported legacy aliases (or empty) return a true nil
// slice rather than an empty non-nil slice. This exercises both the
// `typeKey == ""` branch and the `isUnsupportedLegacyAlertResourceType` branch
// of the early return, covering legacy tokens sourced from the
// unifiedresources alias map as well as the local switch in
// isUnsupportedLegacyAlertResourceType that are not asserted by the existing
// alerts-package test.
func TestBranchCovCanonicalResourceTypeKeysLegacyNil(t *testing.T) {
// Each of these must yield a nil slice (the `return nil` path).
legacyCases := []string{
// Empty / blank -> typeKey == "" branch.
"",
" ",
// Local switch arms in isUnsupportedLegacyAlertResourceType not
// previously asserted.
"qemu", "lxc", "docker container", "dockercontainer", "docker service",
"dockerservice", "k8s pod", "kubernetes", "kubernetes-cluster",
"agent disk", "agentdisk", "pbs server", "pbsserver", "pmg server",
"proxmox mail gateway",
// unifiedresources-backed aliases.
"system_container", "docker_container", "app_container",
"docker_host", "kubernetes_cluster", "k8s_cluster",
// Sanity: a couple already covered by the alerts-package test still
// return nil here when invoked through the config package directly.
"host", "docker", "dockerhost", "k8s",
}
for idx, in := range legacyCases {
// Include the index so subtest names stay unique even when two tokens
// differ only by separator (e.g. "docker container" vs "docker_container"),
// which the test runner would otherwise collapse via space->underscore.
name := fmt.Sprintf("legacy_%02d_%q", idx, in)
t.Run(name, func(t *testing.T) {
got := alertconfig.CanonicalResourceTypeKeys(in)
if got != nil {
t.Errorf("CanonicalResourceTypeKeys(%q) = %v, want nil", in, got)
}
})
}
}
// TestBranchCovCanonicalResourceTypeKeysCaseInsensitive verifies that the
// case-folding performed inside CanonicalAlertResourceType makes
// CanonicalResourceTypeKeys case-insensitive on its input, exercising the
// integration between the two functions for representative type families.
func TestBranchCovCanonicalResourceTypeKeysCaseInsensitive(t *testing.T) {
tests := []struct {
name string
in string
want []string
}{
{name: "upper VM", in: "VM", want: []string{"vm", "guest"}},
{name: "mixed Node", in: "Node", want: []string{"node"}},
{name: "upper AGENT-DISK", in: "AGENT-DISK", want: []string{"agent-disk", "agent", "storage"}},
{name: "upper STORAGE", in: "STORAGE", want: []string{"storage"}},
{name: "title Kubernetes Cluster", in: "Kubernetes Cluster", want: []string{"k8s-cluster", "node"}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := alertconfig.CanonicalResourceTypeKeys(tc.in)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("CanonicalResourceTypeKeys(%q) = %v, want %v", tc.in, got, tc.want)
}
})
}
}
// TestBranchCovCanonicalResourceTypeKeysDisplayAliasEquivalence documents and
// pins the fact that a spaced display alias (e.g. "kubernetes cluster") and its
// hyphenated canonical form ("k8s-cluster") produce identical key sets. This is
// because CanonicalAlertResourceType normalizes the spaced form to the
// hyphenated form before CanonicalResourceTypeKeys' switch is evaluated. See
// the report for the related dead-code observation.
func TestBranchCovCanonicalResourceTypeKeysDisplayAliasEquivalence(t *testing.T) {
pairs := []struct {
display string
canonical string
}{
{"kubernetes cluster", "k8s-cluster"},
{"kubernetes node", "k8s-node"},
{"kubernetes deployment", "k8s-deployment"},
{"kubernetes namespace", "k8s-namespace"},
{"truenas system", "truenas-system"},
{"truenas pool", "truenas-pool"},
{"truenas disk", "truenas-disk"},
}
for _, p := range pairs {
t.Run(p.display, func(t *testing.T) {
fromDisplay := alertconfig.CanonicalResourceTypeKeys(p.display)
fromCanonical := alertconfig.CanonicalResourceTypeKeys(p.canonical)
if !reflect.DeepEqual(fromDisplay, fromCanonical) {
t.Errorf("display %q -> %v, canonical %q -> %v; expected identical key sets",
p.display, fromDisplay, p.canonical, fromCanonical)
}
// And both must be non-nil / non-empty for these valid types.
if len(fromDisplay) == 0 {
t.Errorf("CanonicalResourceTypeKeys(%q) returned no keys", p.display)
}
})
}
}
// TestBranchCovCanonicalResourceTypeKeysDefaultPassthrough covers the default
// switch arm (unknown type returned as the sole key) directly through the
// config package, including a type that contains characters that would be a
// display alias only if it matched a known arm.
func TestBranchCovCanonicalResourceTypeKeysDefaultPassthrough(t *testing.T) {
tests := []struct {
name string
in string
want []string
}{
{name: "unknown custom type", in: "widget", want: []string{"widget"}},
{name: "unknown with dashes", in: "foo-bar-baz", want: []string{"foo-bar-baz"}},
{name: "empty after normalize default", in: " ", want: nil},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := alertconfig.CanonicalResourceTypeKeys(tc.in)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("CanonicalResourceTypeKeys(%q) = %v, want %v", tc.in, got, tc.want)
}
})
}
}
@@ -0,0 +1,819 @@
package config_test
import (
"reflect"
"testing"
alertconfig "github.com/rcourtman/pulse-go-rewrite/internal/alerts/config"
)
// This file exercises genuinely uncovered branches of the pure/near-pure
// normalization helpers in internal/alerts/config/normalize.go. Test names use
// the BranchCov prefix so `-run BranchCov` selects them in isolation.
// floatEq compares two float64 values with a tiny tolerance to avoid surprises
// from any future floating-point derivation; the current implementations only
// perform exact assignments/subtractions so equality holds exactly.
func floatEq(a, b float64) bool {
return a == b
}
// htEq compares two HysteresisThreshold values by value.
func htEq(a, b alertconfig.HysteresisThreshold) bool {
return floatEq(a.Trigger, b.Trigger) && floatEq(a.Clear, b.Clear)
}
// TestBranchCovEnsureValidHysteresis covers every branch of EnsureValidHysteresis:
// nil receiver, disabled (Trigger<=0), Clear>=Trigger with positive result,
// Clear>=Trigger with negative result clamped to 0, and the no-op valid case.
func TestBranchCovEnsureValidHysteresis(t *testing.T) {
tests := []struct {
name string
input *alertconfig.HysteresisThreshold
metric string
wantTrigger float64
wantClear float64
}{
{
name: "nil threshold returns without panic",
input: nil,
metric: "nil.metric",
},
{
name: "trigger zero leaves threshold untouched (disabled)",
input: &alertconfig.HysteresisThreshold{Trigger: 0, Clear: 50},
metric: "disabled.zero",
wantTrigger: 0,
wantClear: 50,
},
{
name: "trigger negative leaves threshold untouched (disabled)",
input: &alertconfig.HysteresisThreshold{Trigger: -5, Clear: 90},
metric: "disabled.negative",
wantTrigger: -5,
wantClear: 90,
},
{
name: "clear greater than trigger auto-fixes to trigger-5",
input: &alertconfig.HysteresisThreshold{Trigger: 80, Clear: 90},
metric: "cpu",
wantTrigger: 80,
wantClear: 75,
},
{
name: "clear equals trigger boundary still triggers fix (>=)",
input: &alertconfig.HysteresisThreshold{Trigger: 80, Clear: 80},
metric: "boundary.equal",
wantTrigger: 80,
wantClear: 75,
},
{
name: "fix clamps to zero when trigger less than 5",
input: &alertconfig.HysteresisThreshold{Trigger: 3, Clear: 10},
metric: "clamp.zero",
wantTrigger: 3,
wantClear: 0,
},
{
name: "already valid threshold is untouched",
input: &alertconfig.HysteresisThreshold{Trigger: 85, Clear: 80},
metric: "valid",
wantTrigger: 85,
wantClear: 80,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
alertconfig.EnsureValidHysteresis(tc.input, tc.metric)
if tc.input == nil {
return
}
if !floatEq(tc.input.Trigger, tc.wantTrigger) {
t.Fatalf("Trigger = %v, want %v", tc.input.Trigger, tc.wantTrigger)
}
if !floatEq(tc.input.Clear, tc.wantClear) {
t.Fatalf("Clear = %v, want %v", tc.input.Clear, tc.wantClear)
}
})
}
}
// TestBranchCovNormalizeStorageDefaults covers all three trigger/clear branches
// of NormalizeStorageDefaults including the inner clamp-to-zero sub-branch.
func TestBranchCovNormalizeStorageDefaults(t *testing.T) {
tests := []struct {
name string
trigger float64
clear float64
wantTrigger float64
wantClear float64
}{
{
name: "negative trigger resets to full default pair",
trigger: -10,
clear: 1234,
wantTrigger: 85,
wantClear: 80,
},
{
name: "zero trigger forces clear to zero even when clear was positive",
trigger: 0,
clear: 50,
wantTrigger: 0,
wantClear: 0,
},
{
name: "positive trigger with non-positive clear derives clear as trigger-5",
trigger: 90,
clear: 0,
wantTrigger: 90,
wantClear: 85,
},
{
name: "positive trigger with negative clear derives clear and clamps to zero when trigger<5",
trigger: 3,
clear: -1,
wantTrigger: 3,
wantClear: 0,
},
{
name: "already populated valid pair is left untouched",
trigger: 95,
clear: 88,
wantTrigger: 95,
wantClear: 88,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
cfg := &alertconfig.AlertConfig{
StorageDefault: alertconfig.HysteresisThreshold{
Trigger: tc.trigger,
Clear: tc.clear,
},
}
alertconfig.NormalizeStorageDefaults(cfg)
got := cfg.StorageDefault
if !floatEq(got.Trigger, tc.wantTrigger) {
t.Fatalf("Trigger = %v, want %v", got.Trigger, tc.wantTrigger)
}
if !floatEq(got.Clear, tc.wantClear) {
t.Fatalf("Clear = %v, want %v", got.Clear, tc.wantClear)
}
})
}
}
// TestBranchCovNormalizeDockerThreshold covers each branch of
// NormalizeDockerThreshold, including the early-return Trigger==0 path that
// intentionally preserves a positive Clear (unlike NormalizeStorageDefaults).
func TestBranchCovNormalizeDockerThreshold(t *testing.T) {
const def = 80.0
tests := []struct {
name string
in alertconfig.HysteresisThreshold
want alertconfig.HysteresisThreshold
}{
{
name: "negative trigger falls back to default then derives clear",
in: alertconfig.HysteresisThreshold{Trigger: -1, Clear: 0},
want: alertconfig.HysteresisThreshold{Trigger: def, Clear: def - 5},
},
{
name: "zero trigger with negative clear clamps clear to zero and returns early",
in: alertconfig.HysteresisThreshold{Trigger: 0, Clear: -7},
want: alertconfig.HysteresisThreshold{Trigger: 0, Clear: 0},
},
{
name: "zero trigger with positive clear preserves clear (disabled metric)",
in: alertconfig.HysteresisThreshold{Trigger: 0, Clear: 42},
want: alertconfig.HysteresisThreshold{Trigger: 0, Clear: 42},
},
{
name: "positive trigger with non-positive clear derives clear",
in: alertconfig.HysteresisThreshold{Trigger: 90, Clear: 0},
want: alertconfig.HysteresisThreshold{Trigger: 90, Clear: 85},
},
{
name: "derived clear clamps to zero when trigger less than 5",
in: alertconfig.HysteresisThreshold{Trigger: 2, Clear: -1},
want: alertconfig.HysteresisThreshold{Trigger: 2, Clear: 0},
},
{
name: "clear above trigger gets repaired by EnsureValidHysteresis safety net",
in: alertconfig.HysteresisThreshold{Trigger: 80, Clear: 95},
want: alertconfig.HysteresisThreshold{Trigger: 80, Clear: 75},
},
{
name: "valid pair untouched",
in: alertconfig.HysteresisThreshold{Trigger: 85, Clear: 80},
want: alertconfig.HysteresisThreshold{Trigger: 85, Clear: 80},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := alertconfig.NormalizeDockerThreshold(tc.in, def, "docker.test")
if !htEq(got, tc.want) {
t.Fatalf("NormalizeDockerThreshold(%+v) = %+v, want %+v", tc.in, got, tc.want)
}
})
}
}
// TestBranchCovNormalizeDockerDefaults covers NormalizeDockerDefaults: default
// seeding, the ServiceCritGapPct<ServiceWarnGapPct repair, every
// StatePoweredOffSeverity NormalizePoweredOffSeverity arm, and the
// UpdateAlertDelayHours==0 default.
func TestBranchCovNormalizeDockerDefaults(t *testing.T) {
t.Run("all zero fields are seeded with canonical defaults", func(t *testing.T) {
cfg := &alertconfig.AlertConfig{}
alertconfig.NormalizeDockerDefaults(cfg)
d := cfg.DockerDefaults
// NOTE on actual behavior: NormalizeDockerThreshold treats Trigger==0 as
// "disabled" (early return), so a zero-value CPU/Memory/Disk threshold is
// NOT seeded with the defaultTrigger passed by NormalizeDockerDefaults.
// Only the scalar fields (<= 0 guards) receive defaults. See GLM_REPORT.md
// for the suspected source inconsistency this documents.
if !htEq(d.CPU, alertconfig.HysteresisThreshold{Trigger: 0, Clear: 0}) {
t.Fatalf("CPU = %+v, want {0 0} (Trigger==0 disabled path, NOT seeded)", d.CPU)
}
if !htEq(d.Memory, alertconfig.HysteresisThreshold{Trigger: 0, Clear: 0}) {
t.Fatalf("Memory = %+v, want {0 0} (Trigger==0 disabled path, NOT seeded)", d.Memory)
}
if !htEq(d.Disk, alertconfig.HysteresisThreshold{Trigger: 0, Clear: 0}) {
t.Fatalf("Disk = %+v, want {0 0} (Trigger==0 disabled path, NOT seeded)", d.Disk)
}
if d.RestartCount != 3 {
t.Fatalf("RestartCount = %d, want 3", d.RestartCount)
}
if d.RestartWindow != 300 {
t.Fatalf("RestartWindow = %d, want 300", d.RestartWindow)
}
if d.MemoryWarnPct != 90 {
t.Fatalf("MemoryWarnPct = %d, want 90", d.MemoryWarnPct)
}
if d.MemoryCriticalPct != 95 {
t.Fatalf("MemoryCriticalPct = %d, want 95", d.MemoryCriticalPct)
}
if d.ServiceWarnGapPct != 10 {
t.Fatalf("ServiceWarnGapPct = %d, want 10", d.ServiceWarnGapPct)
}
if d.ServiceCritGapPct != 50 {
t.Fatalf("ServiceCritGapPct = %d, want 50", d.ServiceCritGapPct)
}
if d.StatePoweredOffSeverity != alertconfig.AlertLevelWarning {
t.Fatalf("StatePoweredOffSeverity = %q, want %q", d.StatePoweredOffSeverity, alertconfig.AlertLevelWarning)
}
if d.UpdateAlertDelayHours != 24 {
t.Fatalf("UpdateAlertDelayHours = %d, want 24", d.UpdateAlertDelayHours)
}
})
t.Run("negative trigger CPU falls back to defaultTrigger and derives clear", func(t *testing.T) {
cfg := &alertconfig.AlertConfig{
DockerDefaults: alertconfig.DockerThresholdConfig{
CPU: alertconfig.HysteresisThreshold{Trigger: -1, Clear: 0},
Memory: alertconfig.HysteresisThreshold{Trigger: -1, Clear: 0},
Disk: alertconfig.HysteresisThreshold{Trigger: -1, Clear: 0},
},
}
alertconfig.NormalizeDockerDefaults(cfg)
d := cfg.DockerDefaults
if !htEq(d.CPU, alertconfig.HysteresisThreshold{Trigger: 80, Clear: 75}) {
t.Fatalf("CPU = %+v, want {80 75}", d.CPU)
}
if !htEq(d.Memory, alertconfig.HysteresisThreshold{Trigger: 85, Clear: 80}) {
t.Fatalf("Memory = %+v, want {85 80}", d.Memory)
}
if !htEq(d.Disk, alertconfig.HysteresisThreshold{Trigger: 85, Clear: 80}) {
t.Fatalf("Disk = %+v, want {85 80}", d.Disk)
}
})
t.Run("critical gap below warning gap is bumped up to warning gap", func(t *testing.T) {
cfg := &alertconfig.AlertConfig{
DockerDefaults: alertconfig.DockerThresholdConfig{
ServiceWarnGapPct: 20,
ServiceCritGapPct: 5,
},
}
alertconfig.NormalizeDockerDefaults(cfg)
if cfg.DockerDefaults.ServiceWarnGapPct != 20 {
t.Fatalf("ServiceWarnGapPct = %d, want 20 (preserved)", cfg.DockerDefaults.ServiceWarnGapPct)
}
if cfg.DockerDefaults.ServiceCritGapPct != 20 {
t.Fatalf("ServiceCritGapPct = %d, want 20 (bumped to warning gap)", cfg.DockerDefaults.ServiceCritGapPct)
}
})
t.Run("critical gap above warning gap is left as configured", func(t *testing.T) {
cfg := &alertconfig.AlertConfig{
DockerDefaults: alertconfig.DockerThresholdConfig{
ServiceWarnGapPct: 10,
ServiceCritGapPct: 60,
},
}
alertconfig.NormalizeDockerDefaults(cfg)
if cfg.DockerDefaults.ServiceCritGapPct != 60 {
t.Fatalf("ServiceCritGapPct = %d, want 60 (preserved)", cfg.DockerDefaults.ServiceCritGapPct)
}
})
t.Run("StatePoweredOffSeverity empty defaults to warning", func(t *testing.T) {
cfg := &alertconfig.AlertConfig{}
alertconfig.NormalizeDockerDefaults(cfg)
if cfg.DockerDefaults.StatePoweredOffSeverity != alertconfig.AlertLevelWarning {
t.Fatalf("severity = %q, want warning", cfg.DockerDefaults.StatePoweredOffSeverity)
}
})
t.Run("StatePoweredOffSeverity critical is preserved through NormalizePoweredOffSeverity", func(t *testing.T) {
cfg := &alertconfig.AlertConfig{
DockerDefaults: alertconfig.DockerThresholdConfig{
StatePoweredOffSeverity: alertconfig.AlertLevelCritical,
},
}
alertconfig.NormalizeDockerDefaults(cfg)
if cfg.DockerDefaults.StatePoweredOffSeverity != alertconfig.AlertLevelCritical {
t.Fatalf("severity = %q, want critical", cfg.DockerDefaults.StatePoweredOffSeverity)
}
})
t.Run("StatePoweredOffSeverity uppercase critical is normalized to critical", func(t *testing.T) {
cfg := &alertconfig.AlertConfig{
DockerDefaults: alertconfig.DockerThresholdConfig{
StatePoweredOffSeverity: alertconfig.AlertLevel("CRITICAL"),
},
}
alertconfig.NormalizeDockerDefaults(cfg)
if cfg.DockerDefaults.StatePoweredOffSeverity != alertconfig.AlertLevelCritical {
t.Fatalf("severity = %q, want critical (case-insensitive)", cfg.DockerDefaults.StatePoweredOffSeverity)
}
})
t.Run("StatePoweredOffSeverity unrecognized value falls back to warning default arm", func(t *testing.T) {
cfg := &alertconfig.AlertConfig{
DockerDefaults: alertconfig.DockerThresholdConfig{
StatePoweredOffSeverity: alertconfig.AlertLevel("bogus"),
},
}
alertconfig.NormalizeDockerDefaults(cfg)
if cfg.DockerDefaults.StatePoweredOffSeverity != alertconfig.AlertLevelWarning {
t.Fatalf("severity = %q, want warning (default arm)", cfg.DockerDefaults.StatePoweredOffSeverity)
}
})
t.Run("UpdateAlertDelayHours zero defaults to 24 and positive preserved", func(t *testing.T) {
cfg := &alertconfig.AlertConfig{}
alertconfig.NormalizeDockerDefaults(cfg)
if cfg.DockerDefaults.UpdateAlertDelayHours != 24 {
t.Fatalf("UpdateAlertDelayHours = %d, want 24", cfg.DockerDefaults.UpdateAlertDelayHours)
}
cfg2 := &alertconfig.AlertConfig{
DockerDefaults: alertconfig.DockerThresholdConfig{UpdateAlertDelayHours: 48},
}
alertconfig.NormalizeDockerDefaults(cfg2)
if cfg2.DockerDefaults.UpdateAlertDelayHours != 48 {
t.Fatalf("UpdateAlertDelayHours = %d, want 48 (preserved)", cfg2.DockerDefaults.UpdateAlertDelayHours)
}
})
t.Run("positive threshold values preserved end to end", func(t *testing.T) {
cfg := &alertconfig.AlertConfig{
DockerDefaults: alertconfig.DockerThresholdConfig{
CPU: alertconfig.HysteresisThreshold{Trigger: 77, Clear: 70},
Memory: alertconfig.HysteresisThreshold{Trigger: 88, Clear: 83},
Disk: alertconfig.HysteresisThreshold{Trigger: 99, Clear: 94},
RestartCount: 7,
RestartWindow: 600,
MemoryWarnPct: 80,
MemoryCriticalPct: 88,
},
}
alertconfig.NormalizeDockerDefaults(cfg)
d := cfg.DockerDefaults
if !htEq(d.CPU, alertconfig.HysteresisThreshold{Trigger: 77, Clear: 70}) {
t.Fatalf("CPU = %+v, want preserved", d.CPU)
}
if d.RestartCount != 7 {
t.Fatalf("RestartCount = %d, want 7", d.RestartCount)
}
if d.MemoryCriticalPct != 88 {
t.Fatalf("MemoryCriticalPct = %d, want 88", d.MemoryCriticalPct)
}
})
}
// TestBranchCovNormalizeDiskFillByType exercises the branch internals of
// NormalizeDiskFillByType that are distinct from the sibling-package coverage:
// the lower=="" drop arm, the "existing canonical key not overwritten" arm,
// and the per-key reset path for a negative clear with positive trigger.
func TestBranchCovNormalizeDiskFillByType(t *testing.T) {
t.Run("whitespace only key is dropped and not re-added", func(t *testing.T) {
cfg := &alertconfig.AlertConfig{
DiskFillByType: map[string]alertconfig.HysteresisThreshold{
" ": {Trigger: 99, Clear: 99},
},
}
alertconfig.NormalizeDiskFillByType(cfg)
if _, exists := cfg.DiskFillByType[" "]; exists {
t.Fatalf("whitespace key should be removed, map=%+v", cfg.DiskFillByType)
}
if _, exists := cfg.DiskFillByType[""]; exists {
t.Fatalf("empty canonical key should not be added, map=%+v", cfg.DiskFillByType)
}
for _, k := range []string{"nvme", "sata", "hdd"} {
if _, ok := cfg.DiskFillByType[k]; !ok {
t.Fatalf("canonical key %q missing, map=%+v", k, cfg.DiskFillByType)
}
}
})
t.Run("non-canonical already-lowercase key is left in place", func(t *testing.T) {
custom := alertconfig.HysteresisThreshold{Trigger: 70, Clear: 60}
cfg := &alertconfig.AlertConfig{
DiskFillByType: map[string]alertconfig.HysteresisThreshold{
"external": custom,
},
}
alertconfig.NormalizeDiskFillByType(cfg)
got, ok := cfg.DiskFillByType["external"]
if !ok {
t.Fatalf("non-canonical lowercase key should survive, map=%+v", cfg.DiskFillByType)
}
if !htEq(got, custom) {
t.Fatalf("external = %+v, want %+v preserved", got, custom)
}
})
t.Run("uppercased duplicate of existing canonical key does not overwrite it", func(t *testing.T) {
original := alertconfig.HysteresisThreshold{Trigger: 50, Clear: 40}
cfg := &alertconfig.AlertConfig{
DiskFillByType: map[string]alertconfig.HysteresisThreshold{
"NVME": {Trigger: 1, Clear: 1},
"nvme": original,
},
}
alertconfig.NormalizeDiskFillByType(cfg)
if _, exists := cfg.DiskFillByType["NVME"]; exists {
t.Fatalf("uppercase NVME key should be removed, map=%+v", cfg.DiskFillByType)
}
got := cfg.DiskFillByType["nvme"]
if !htEq(got, original) {
t.Fatalf("nvme = %+v, want %+v (lowercased dup must not overwrite existing)", got, original)
}
})
t.Run("mixed-case key lowercased into absent canonical slot is added with value preserved", func(t *testing.T) {
custom := alertconfig.HysteresisThreshold{Trigger: 93, Clear: 88}
cfg := &alertconfig.AlertConfig{
DiskFillByType: map[string]alertconfig.HysteresisThreshold{
"NVMe": custom,
},
}
alertconfig.NormalizeDiskFillByType(cfg)
if _, exists := cfg.DiskFillByType["NVMe"]; exists {
t.Fatalf("mixed-case NVMe should be removed, map=%+v", cfg.DiskFillByType)
}
got, ok := cfg.DiskFillByType["nvme"]
if !ok {
t.Fatalf("lowercased nvme should be added, map=%+v", cfg.DiskFillByType)
}
if !htEq(got, custom) {
t.Fatalf("nvme = %+v, want %+v (operator value preserved through rename)", got, custom)
}
})
t.Run("positive trigger with non-positive clear resets to default", func(t *testing.T) {
cfg := &alertconfig.AlertConfig{
DiskFillByType: map[string]alertconfig.HysteresisThreshold{
"sata": {Trigger: 88, Clear: 0},
"hdd": {Trigger: 80, Clear: -3},
},
}
alertconfig.NormalizeDiskFillByType(cfg)
if sata := cfg.DiskFillByType["sata"]; !htEq(sata, alertconfig.HysteresisThreshold{Trigger: 90, Clear: 85}) {
t.Fatalf("sata = %+v, want default {90 85}", sata)
}
if hdd := cfg.DiskFillByType["hdd"]; !htEq(hdd, alertconfig.HysteresisThreshold{Trigger: 85, Clear: 80}) {
t.Fatalf("hdd = %+v, want default {85 80}", hdd)
}
})
t.Run("nil map seeds canonical defaults", func(t *testing.T) {
cfg := &alertconfig.AlertConfig{}
alertconfig.NormalizeDiskFillByType(cfg)
want := map[string]alertconfig.HysteresisThreshold{
"nvme": {Trigger: 92, Clear: 87},
"sata": {Trigger: 90, Clear: 85},
"hdd": {Trigger: 85, Clear: 80},
}
if !reflect.DeepEqual(cfg.DiskFillByType, want) {
t.Fatalf("DiskFillByType = %+v, want %+v", cfg.DiskFillByType, want)
}
})
}
// TestBranchCovNormalizeGeneralSettings covers the default-seeding branch and
// the preserved-positive branch for every field.
func TestBranchCovNormalizeGeneralSettings(t *testing.T) {
t.Run("zero valued config is fully seeded", func(t *testing.T) {
cfg := &alertconfig.AlertConfig{}
alertconfig.NormalizeGeneralSettings(cfg)
if cfg.MinimumDelta != 2.0 {
t.Fatalf("MinimumDelta = %v, want 2.0", cfg.MinimumDelta)
}
if cfg.SuppressionWindow != 5 {
t.Fatalf("SuppressionWindow = %d, want 5", cfg.SuppressionWindow)
}
if cfg.HysteresisMargin != 5.0 {
t.Fatalf("HysteresisMargin = %v, want 5.0", cfg.HysteresisMargin)
}
if cfg.ObservationWindowHours != 24 {
t.Fatalf("ObservationWindowHours = %d, want 24", cfg.ObservationWindowHours)
}
if cfg.FlappingWindowSeconds != 300 {
t.Fatalf("FlappingWindowSeconds = %d, want 300", cfg.FlappingWindowSeconds)
}
if cfg.FlappingThreshold != 5 {
t.Fatalf("FlappingThreshold = %d, want 5", cfg.FlappingThreshold)
}
if cfg.FlappingCooldownMinutes != 15 {
t.Fatalf("FlappingCooldownMinutes = %d, want 15", cfg.FlappingCooldownMinutes)
}
})
t.Run("explicit positive values preserved, negative replaced", func(t *testing.T) {
cfg := &alertconfig.AlertConfig{
MinimumDelta: 1.5,
SuppressionWindow: 9,
HysteresisMargin: 2.5,
ObservationWindowHours: 48,
FlappingWindowSeconds: 120,
FlappingThreshold: -1,
FlappingCooldownMinutes: 0,
}
alertconfig.NormalizeGeneralSettings(cfg)
if cfg.MinimumDelta != 1.5 {
t.Fatalf("MinimumDelta = %v, want 1.5", cfg.MinimumDelta)
}
if cfg.SuppressionWindow != 9 {
t.Fatalf("SuppressionWindow = %d, want 9", cfg.SuppressionWindow)
}
if cfg.HysteresisMargin != 2.5 {
t.Fatalf("HysteresisMargin = %v, want 2.5", cfg.HysteresisMargin)
}
if cfg.ObservationWindowHours != 48 {
t.Fatalf("ObservationWindowHours = %d, want 48", cfg.ObservationWindowHours)
}
if cfg.FlappingWindowSeconds != 120 {
t.Fatalf("FlappingWindowSeconds = %d, want 120", cfg.FlappingWindowSeconds)
}
if cfg.FlappingThreshold != 5 {
t.Fatalf("FlappingThreshold = %d, want 5 (negative replaced)", cfg.FlappingThreshold)
}
if cfg.FlappingCooldownMinutes != 15 {
t.Fatalf("FlappingCooldownMinutes = %d, want 15 (zero replaced)", cfg.FlappingCooldownMinutes)
}
})
}
// expectedTimeThresholdKeys is the canonical set NormalizeTimeThresholds
// guarantees to populate (the "all" key is intentionally NOT auto-added).
var expectedTimeThresholdKeys = []string{
"guest", "node", "storage", "pbs", "agent",
"k8s-cluster", "k8s-node", "k8s-deployment", "k8s-namespace", "pod",
"truenas-system", "truenas-pool", "truenas-dataset", "truenas-disk",
"vmware-host", "vmware-vm", "vmware-datastore", "vmware-network",
}
// TestBranchCovNormalizeTimeThresholds covers the nil-init path, the
// preserve-existing path, the negative-delay repair path, and the special-cased
// "all" key (negative repaired, positive preserved, absent not inserted).
func TestBranchCovNormalizeTimeThresholds(t *testing.T) {
t.Run("nil TimeThresholds is initialized with all default keys set to 5", func(t *testing.T) {
cfg := &alertconfig.AlertConfig{}
alertconfig.NormalizeTimeThresholds(cfg)
if cfg.TimeThresholds == nil {
t.Fatal("TimeThresholds should be initialized, got nil")
}
for _, k := range expectedTimeThresholdKeys {
if v, ok := cfg.TimeThresholds[k]; !ok || v != 5 {
t.Fatalf("TimeThresholds[%q] = %d ok=%v, want 5", k, v, ok)
}
}
if _, ok := cfg.TimeThresholds["all"]; ok {
t.Fatalf("\"all\" key should NOT be auto-added, map=%+v", cfg.TimeThresholds)
}
})
t.Run("existing positive delays preserved and missing keys filled", func(t *testing.T) {
cfg := &alertconfig.AlertConfig{
TimeThresholds: map[string]int{"guest": 30},
}
alertconfig.NormalizeTimeThresholds(cfg)
if cfg.TimeThresholds["guest"] != 30 {
t.Fatalf("guest = %d, want 30 (preserved)", cfg.TimeThresholds["guest"])
}
if cfg.TimeThresholds["node"] != 5 {
t.Fatalf("node = %d, want 5 (filled)", cfg.TimeThresholds["node"])
}
})
t.Run("negative per-type delay repaired to 5", func(t *testing.T) {
cfg := &alertconfig.AlertConfig{
TimeThresholds: map[string]int{"storage": -12},
}
alertconfig.NormalizeTimeThresholds(cfg)
if cfg.TimeThresholds["storage"] != 5 {
t.Fatalf("storage = %d, want 5 (repaired)", cfg.TimeThresholds["storage"])
}
})
t.Run("negative all delay repaired while positive all preserved", func(t *testing.T) {
t.Run("negative all", func(t *testing.T) {
cfg := &alertconfig.AlertConfig{
TimeThresholds: map[string]int{"all": -1},
}
alertconfig.NormalizeTimeThresholds(cfg)
if cfg.TimeThresholds["all"] != 5 {
t.Fatalf("all = %d, want 5 (repaired)", cfg.TimeThresholds["all"])
}
})
t.Run("positive all", func(t *testing.T) {
cfg := &alertconfig.AlertConfig{
TimeThresholds: map[string]int{"all": 100},
}
alertconfig.NormalizeTimeThresholds(cfg)
if cfg.TimeThresholds["all"] != 100 {
t.Fatalf("all = %d, want 100 (preserved)", cfg.TimeThresholds["all"])
}
})
})
t.Run("nil MetricTimeThresholds collapses to nil", func(t *testing.T) {
cfg := &alertconfig.AlertConfig{}
alertconfig.NormalizeTimeThresholds(cfg)
if cfg.MetricTimeThresholds != nil {
t.Fatalf("MetricTimeThresholds = %+v, want nil", cfg.MetricTimeThresholds)
}
})
}
// TestBranchCovValidateHysteresisThresholds covers the repair of an invalid
// value-typed threshold (StorageDefault), a pointer threshold, an unchanged
// valid config, and safe handling of nil pointers.
func TestBranchCovValidateHysteresisThresholds(t *testing.T) {
t.Run("invalid value-typed StorageDefault is repaired in place", func(t *testing.T) {
cfg := &alertconfig.AlertConfig{
StorageDefault: alertconfig.HysteresisThreshold{Trigger: 85, Clear: 95},
}
alertconfig.ValidateHysteresisThresholds(cfg)
if cfg.StorageDefault.Clear != 80 {
t.Fatalf("StorageDefault.Clear = %v, want 80 (trigger-5)", cfg.StorageDefault.Clear)
}
})
t.Run("invalid pointer threshold on guest defaults is repaired", func(t *testing.T) {
cfg := &alertconfig.AlertConfig{
GuestDefaults: alertconfig.ThresholdConfig{
CPU: &alertconfig.HysteresisThreshold{Trigger: 80, Clear: 90},
},
}
alertconfig.ValidateHysteresisThresholds(cfg)
if cfg.GuestDefaults.CPU == nil || cfg.GuestDefaults.CPU.Clear != 75 {
t.Fatalf("GuestDefaults.CPU.Clear = %v, want 75", cfg.GuestDefaults.CPU)
}
})
t.Run("already valid config is unchanged", func(t *testing.T) {
cfg := &alertconfig.AlertConfig{
StorageDefault: alertconfig.HysteresisThreshold{Trigger: 85, Clear: 80},
GuestDefaults: alertconfig.ThresholdConfig{
Memory: &alertconfig.HysteresisThreshold{Trigger: 85, Clear: 80},
},
}
alertconfig.ValidateHysteresisThresholds(cfg)
if cfg.StorageDefault.Clear != 80 {
t.Fatalf("StorageDefault.Clear = %v, want 80 (untouched)", cfg.StorageDefault.Clear)
}
if cfg.GuestDefaults.Memory == nil || cfg.GuestDefaults.Memory.Clear != 80 {
t.Fatalf("GuestDefaults.Memory.Clear = %v, want 80 (untouched)", cfg.GuestDefaults.Memory)
}
})
t.Run("nil pointer thresholds do not panic", func(t *testing.T) {
cfg := &alertconfig.AlertConfig{}
alertconfig.ValidateHysteresisThresholds(cfg)
if cfg.GuestDefaults.CPU != nil {
t.Fatalf("GuestDefaults.CPU = %v, want nil", cfg.GuestDefaults.CPU)
}
})
}
// TestBranchCovValidateQuietHoursTimezone covers all four combinations of the
// Enabled && Timezone != "" guard plus the error/success outcomes of
// time.LoadLocation.
func TestBranchCovValidateQuietHoursTimezone(t *testing.T) {
tests := []struct {
name string
enabled bool
timezone string
wantEnabled bool
}{
{
name: "enabled with invalid timezone gets disabled",
enabled: true,
timezone: "Nowhere/Invalid",
wantEnabled: false,
},
{
name: "enabled with valid timezone stays enabled",
enabled: true,
timezone: "America/New_York",
wantEnabled: true,
},
{
name: "enabled with empty timezone is a no-op (stays enabled)",
enabled: true,
timezone: "",
wantEnabled: true,
},
{
name: "disabled with invalid timezone is a no-op (stays disabled)",
enabled: false,
timezone: "Nowhere/Invalid",
wantEnabled: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
cfg := &alertconfig.AlertConfig{}
cfg.Schedule.QuietHours.Enabled = tc.enabled
cfg.Schedule.QuietHours.Timezone = tc.timezone
alertconfig.ValidateQuietHoursTimezone(cfg)
if cfg.Schedule.QuietHours.Enabled != tc.wantEnabled {
t.Fatalf("QuietHours.Enabled = %v, want %v", cfg.Schedule.QuietHours.Enabled, tc.wantEnabled)
}
})
}
}
// TestBranchCovNormalizeDockerIgnoredPrefixes covers the empty/nil early
// returns, the all-whitespace collapse to nil, case-insensitive dedup that
// preserves the first-seen original casing, and surrounding-space trimming.
func TestBranchCovNormalizeDockerIgnoredPrefixes(t *testing.T) {
tests := []struct {
name string
in []string
want []string
}{
{name: "nil input returns nil", in: nil, want: nil},
{name: "empty input returns nil", in: []string{}, want: nil},
{name: "all whitespace entries collapse to nil", in: []string{" ", "", "\t"}, want: nil},
{
name: "case-insensitive dedup keeps first occurrence original casing",
in: []string{"Foo", "foo", "FOO"},
want: []string{"Foo"},
},
{
name: "leading and trailing spaces are trimmed but inner case preserved",
in: []string{" MyPrefix ", "\tBar\t"},
want: []string{"MyPrefix", "Bar"},
},
{
name: "distinct prefixes all retained in first-seen order",
in: []string{"alpha", "Beta", "ALPHA"},
want: []string{"alpha", "Beta"},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := alertconfig.NormalizeDockerIgnoredPrefixes(tc.in)
if len(got) != len(tc.want) {
t.Fatalf("len = %d, want %d (got=%v want=%v)", len(got), len(tc.want), got, tc.want)
}
// Normalize nil vs empty []string{} for comparison after length check.
if len(got) == 0 {
if got != nil {
t.Fatalf("expected nil slice for empty result, got %v", got)
}
return
}
for i := range tc.want {
if got[i] != tc.want[i] {
t.Fatalf("result[%d] = %q, want %q (got=%v)", i, got[i], tc.want[i], got)
}
}
})
}
}
@@ -0,0 +1,264 @@
package config
import (
"reflect"
"testing"
)
// These tests pin the static provider-registry accessors in ai_providers.go
// (SuggestedModelForProvider, SuggestedModelIDSet, IsOpenAICompatibleProvider,
// AIProviderFallbackModels, AIProviderDisplayName) which were previously
// uncovered. They exercise the not-found / empty / normalization branches each
// helper inherits from LookupAIProviderDefinition, plus the per-protocol and
// per-catalog arms of the registry data.
func TestBranchCovSuggestedModelForProvider(t *testing.T) {
tests := []struct {
name string
provider string
want string
}{
// Found-provider arm with a real blessing.
{name: "ollama returns blessed patrol model", provider: AIProviderOllama, want: OllamaSuggestedPatrolModel},
// LookupAIProviderDefinition normalizes via ToLower(TrimSpace); the
// suggestion must still resolve through that normalization.
{name: "ollama id case-insensitive", provider: "OLLAMA", want: OllamaSuggestedPatrolModel},
{name: "ollama id trims surrounding whitespace", provider: " ollama ", want: OllamaSuggestedPatrolModel},
// Found-provider arm where the registry deliberately sets no blessing.
{name: "anthropic has no suggestion", provider: AIProviderAnthropic, want: ""},
{name: "openai has no suggestion", provider: AIProviderOpenAI, want: ""},
{name: "retired quickstart has no suggestion", provider: AIProviderQuickstart, want: ""},
// Not-found arm: empty, whitespace-only, unknown, and a lookalike id
// that must NOT be treated as a fuzzy match.
{name: "empty provider returns empty", provider: "", want: ""},
{name: "whitespace-only provider returns empty", provider: " ", want: ""},
{name: "unknown provider returns empty", provider: "no-such-provider", want: ""},
{name: "lookalike id with trailing colon rejected", provider: "ollama:", want: ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := SuggestedModelForProvider(tt.provider); got != tt.want {
t.Fatalf("SuggestedModelForProvider(%q) = %q, want %q", tt.provider, got, tt.want)
}
})
}
}
func TestBranchCovSuggestedModelIDSet(t *testing.T) {
set := SuggestedModelIDSet()
if set == nil {
t.Fatal("SuggestedModelIDSet returned a nil map")
}
// Only the Ollama provider contributes a SuggestedModel plus equivalents;
// every other provider's empty suggestion is skipped by the `if id != ""`
// guard, which is the branch under test here.
want := map[string]struct{}{
OllamaSuggestedPatrolModel: {},
"qwen3:latest": {},
}
if !reflect.DeepEqual(set, want) {
t.Fatalf("SuggestedModelIDSet() = %v, want exactly %v (ollama blessing + equivalents only)", set, want)
}
// Guard against the suggested set being confused with the fallback catalog:
// Z.ai exposes "glm-5.2" as a fallback model but has no Patrol blessing, so
// it must be absent from the suggestion set.
if _, ok := set["glm-5.2"]; ok {
t.Fatal(`fallback-catalog model "glm-5.2" leaked into the suggested-model set`)
}
}
func TestBranchCovIsOpenAICompatibleProvider(t *testing.T) {
tests := []struct {
name string
provider string
want bool
}{
// OpenAI-compatible protocol arm (ok && protocol == openai_compatible).
{name: "openai", provider: AIProviderOpenAI, want: true},
{name: "openrouter gateway", provider: AIProviderOpenRouter, want: true},
{name: "deepseek", provider: AIProviderDeepSeek, want: true},
{name: "zai", provider: AIProviderZai, want: true},
{name: "groq", provider: AIProviderGroq, want: true},
{name: "mistral", provider: AIProviderMistral, want: true},
{name: "cerebras", provider: AIProviderCerebras, want: true},
{name: "together", provider: AIProviderTogether, want: true},
{name: "fireworks", provider: AIProviderFireworks, want: true},
{name: "openai id case-insensitive", provider: "OpenAI", want: true},
// Known provider with a non-compatible protocol arm
// (ok && protocol != openai_compatible).
{name: "anthropic native protocol", provider: AIProviderAnthropic, want: false},
{name: "gemini native protocol", provider: AIProviderGemini, want: false},
{name: "ollama native protocol", provider: AIProviderOllama, want: false},
{name: "codex subscription agent", provider: AIProviderCodexSubscription, want: false},
{name: "claude subscription agent", provider: AIProviderClaudeSubscription, want: false},
{name: "retired quickstart marker", provider: AIProviderQuickstart, want: false},
// Not-found arm (ok == false short-circuits the protocol check).
{name: "unknown provider", provider: "no-such", want: false},
{name: "empty provider", provider: "", want: false},
{name: "whitespace-only provider", provider: " ", want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsOpenAICompatibleProvider(tt.provider); got != tt.want {
t.Fatalf("IsOpenAICompatibleProvider(%q) = %v, want %v", tt.provider, got, tt.want)
}
})
}
}
func TestBranchCovAIProviderFallbackModels(t *testing.T) {
t.Run("unknown provider returns nil", func(t *testing.T) {
if got := AIProviderFallbackModels("no-such"); got != nil {
t.Fatalf("AIProviderFallbackModels(unknown) = %#v, want nil", got)
}
})
t.Run("empty provider returns nil", func(t *testing.T) {
if got := AIProviderFallbackModels(""); got != nil {
t.Fatalf(`AIProviderFallbackModels("") = %#v, want nil`, got)
}
})
t.Run("whitespace-only provider returns nil", func(t *testing.T) {
if got := AIProviderFallbackModels(" "); got != nil {
t.Fatalf(`AIProviderFallbackModels(" ") = %#v, want nil`, got)
}
})
// Known-provider-but-no-catalog arm: the early return fires because
// len(def.FallbackModels) == 0 even though the lookup succeeded.
t.Run("anthropic has no static fallback catalog", func(t *testing.T) {
if got := AIProviderFallbackModels(AIProviderAnthropic); got != nil {
t.Fatalf("AIProviderFallbackModels(%q) = %#v, want nil", AIProviderAnthropic, got)
}
})
t.Run("ollama has no static fallback catalog", func(t *testing.T) {
if got := AIProviderFallbackModels(AIProviderOllama); got != nil {
t.Fatalf("AIProviderFallbackModels(%q) = %#v, want nil", AIProviderOllama, got)
}
})
t.Run("openrouter relies on gateway catalog not static list", func(t *testing.T) {
if got := AIProviderFallbackModels(AIProviderOpenRouter); got != nil {
t.Fatalf("AIProviderFallbackModels(%q) = %#v, want nil", AIProviderOpenRouter, got)
}
})
// Catalog arm: returns a non-nil copy whose contents match the registry.
t.Run("deepseek returns four-entry fallback catalog copy", func(t *testing.T) {
got := AIProviderFallbackModels(AIProviderDeepSeek)
if got == nil {
t.Fatal("expected a non-nil fallback catalog for deepseek")
}
if len(got) != 4 {
t.Fatalf("deepseek fallback count = %d, want 4 (%#v)", len(got), got)
}
if got[0].ID != DeepSeekModelV4Flash {
t.Fatalf("first deepseek fallback = %q, want %q", got[0].ID, DeepSeekModelV4Flash)
}
// The two current V4 entries are flagged Notable; the two legacy
// aliases are not. Pinning this guards the catalog's shape.
notable := 0
for _, m := range got {
if m.Notable {
notable++
}
}
if notable != 2 {
t.Fatalf("deepseek notable fallback count = %d, want 2", notable)
}
})
t.Run("zai returns single fallback entry", func(t *testing.T) {
got := AIProviderFallbackModels(AIProviderZai)
if got == nil || len(got) != 1 {
t.Fatalf("zai fallback = %#v, want exactly one entry", got)
}
if got[0].ID != "glm-5.2" || !got[0].Notable {
t.Fatalf("zai fallback[0] = %#v, want {ID:glm-5.2 Notable:true}", got[0])
}
})
t.Run("claude subscription returns two alias entries", func(t *testing.T) {
got := AIProviderFallbackModels(AIProviderClaudeSubscription)
if got == nil || len(got) != 2 {
t.Fatalf("claude subscription fallback = %#v, want exactly two entries", got)
}
})
t.Run("codex subscription returns single fallback entry", func(t *testing.T) {
got := AIProviderFallbackModels(AIProviderCodexSubscription)
if got == nil || len(got) != 1 {
t.Fatalf("codex subscription fallback = %#v, want exactly one entry", got)
}
})
// The accessor must hand back an independent copy on every call so that
// callers cannot mutate the canonical registry records.
t.Run("returned slice is an independent copy", func(t *testing.T) {
a := AIProviderFallbackModels(AIProviderDeepSeek)
b := AIProviderFallbackModels(AIProviderDeepSeek)
if len(a) != 4 || len(b) != 4 {
t.Fatalf("precondition: expected 4 deepseek fallbacks, got a=%d b=%d", len(a), len(b))
}
originalFirst := a[0].ID
// Grow one returned slice and rewrite an element field on the other.
b = append(b, AIProviderModelDefinition{ID: "injected"})
a[0] = AIProviderModelDefinition{ID: "mutated"}
if len(a) != 4 {
t.Fatalf("appending to b leaked into a: len(a) = %d, want 4", len(a))
}
if a[0].ID != "mutated" {
t.Fatalf("a[0].ID = %q, want \"mutated\" (independent element expected)", a[0].ID)
}
// A fresh lookup must reflect neither mutation.
fresh := AIProviderFallbackModels(AIProviderDeepSeek)
if len(fresh) != 4 || fresh[0].ID != originalFirst {
t.Fatalf("mutation poisoned the canonical record: fresh = %#v, want first id %q", fresh, originalFirst)
}
})
}
func TestBranchCovAIProviderDisplayName(t *testing.T) {
tests := []struct {
name string
provider string
want string
}{
// Found-provider arm: returns the registry DisplayName, never the raw
// input id.
{name: "anthropic display name", provider: AIProviderAnthropic, want: "Anthropic"},
{name: "openrouter display name", provider: AIProviderOpenRouter, want: "OpenRouter"},
{name: "zai display name", provider: AIProviderZai, want: "Z.ai"},
{name: "codex subscription display name", provider: AIProviderCodexSubscription, want: "Codex subscription (local)"},
{name: "retired quickstart keeps display name", provider: AIProviderQuickstart, want: "Pulse hosted quickstart"},
// Lookup normalizes case/whitespace before resolving the id, so the
// canonical DisplayName still comes back.
{name: "lookup is case-insensitive", provider: "ANTHROPIC", want: "Anthropic"},
{name: "lookup trims surrounding whitespace", provider: " anthropic ", want: "Anthropic"},
// Not-found arm: the raw input is returned VERBATIM (it is not
// normalized). This pins the current asymmetric behaviour.
{name: "unknown provider returns input verbatim", provider: "WeirdThing", want: "WeirdThing"},
{name: "unknown mixed-case input preserved", provider: "SomeCloud", want: "SomeCloud"},
{name: "empty input returns empty", provider: "", want: ""},
{name: "whitespace-only input returned with whitespace", provider: " ", want: " "},
{name: "lookalike id returned verbatim", provider: "ollama:", want: "ollama:"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := AIProviderDisplayName(tt.provider); got != tt.want {
t.Fatalf("AIProviderDisplayName(%q) = %q, want %q", tt.provider, got, tt.want)
}
})
}
}
@@ -0,0 +1,302 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestBranchCovEffectivePollIntervalSecs covers both branches of
// AvailabilityTarget.EffectivePollIntervalSecs: the "> 0" arm returns the
// configured value, and the <= 0 (zero and negative) arm falls back to the
// package default.
func TestBranchCovEffectivePollIntervalSecs(t *testing.T) {
tests := []struct {
name string
value int
want int
}{
{"positive value returned as-is", 45, 45},
{"minimum positive value returned", 1, 1},
{"zero falls back to default", 0, DefaultAvailabilityPollIntervalSecs},
{"negative falls back to default", -5, DefaultAvailabilityPollIntervalSecs},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
target := AvailabilityTarget{PollIntervalSecs: tt.value}
assert.Equal(t, tt.want, target.EffectivePollIntervalSecs())
})
}
}
// TestBranchCovEffectiveTimeoutMillis covers both branches of
// AvailabilityTarget.EffectiveTimeoutMillis.
func TestBranchCovEffectiveTimeoutMillis(t *testing.T) {
tests := []struct {
name string
value int
want int
}{
{"positive value returned as-is", 1500, 1500},
{"minimum positive value returned", 1, 1},
{"zero falls back to default", 0, DefaultAvailabilityTimeoutMillis},
{"negative falls back to default", -100, DefaultAvailabilityTimeoutMillis},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
target := AvailabilityTarget{TimeoutMillis: tt.value}
assert.Equal(t, tt.want, target.EffectiveTimeoutMillis())
})
}
}
// TestBranchCovEffectiveFailureThreshold covers both branches of
// AvailabilityTarget.EffectiveFailureThreshold.
func TestBranchCovEffectiveFailureThreshold(t *testing.T) {
tests := []struct {
name string
value int
want int
}{
{"positive value returned as-is", 7, 7},
{"minimum positive value returned", 1, 1},
{"zero falls back to default", 0, DefaultAvailabilityFailureThreshold},
{"negative falls back to default", -3, DefaultAvailabilityFailureThreshold},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
target := AvailabilityTarget{FailureThreshold: tt.value}
assert.Equal(t, tt.want, target.EffectiveFailureThreshold())
})
}
}
// TestBranchCovDisplayName covers every branch of
// AvailabilityTarget.DisplayName: a present (trimmed) name wins; a blank name
// falls back to the trimmed address; both blank yields an empty string.
func TestBranchCovDisplayName(t *testing.T) {
tests := []struct {
name string
target AvailabilityTarget
want string
}{
{
name: "name present is returned trimmed",
target: AvailabilityTarget{Name: " Energy Monitor ", Address: "device.local"},
want: "Energy Monitor",
},
{
name: "whitespace-only name falls back to trimmed address",
target: AvailabilityTarget{Name: " ", Address: " device.local "},
want: "device.local",
},
{
name: "empty name falls back to address",
target: AvailabilityTarget{Name: "", Address: "10.0.0.1"},
want: "10.0.0.1",
},
{
name: "both blank yields empty string",
target: AvailabilityTarget{Name: " ", Address: "\t\n"},
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, tt.target.DisplayName())
})
}
}
// TestBranchCovValidate exercises every branch of AvailabilityTarget.Validate:
// the address-presence check, each arm of the target-kind and protocol
// switches (including their defaults), the three numeric bound checks
// (including the exact boundary that is accepted), the HTTP/HTTPS URL
// validation path (both success and error propagation), and the non-HTTP
// host presence/whitespace checks.
func TestBranchCovValidate(t *testing.T) {
tests := []struct {
name string
target AvailabilityTarget
wantErr bool
wantErrContains string
}{
// --- Address presence (first guard). ---
{
name: "empty address rejected",
target: AvailabilityTarget{TargetKind: AvailabilityTargetService, Protocol: AvailabilityProbeICMP},
wantErr: true,
wantErrContains: "address is required",
},
{
name: "whitespace-only address rejected",
target: AvailabilityTarget{Address: " ", TargetKind: AvailabilityTargetService, Protocol: AvailabilityProbeICMP},
wantErr: true,
wantErrContains: "address is required",
},
// --- TargetKind switch: default arm. ---
{
name: "unsupported target kind rejected",
target: AvailabilityTarget{Address: "device.local", TargetKind: AvailabilityTargetKind("database"), Protocol: AvailabilityProbeICMP},
wantErr: true,
wantErrContains: "unsupported availability target kind",
},
{
name: "empty target kind hits default arm",
target: AvailabilityTarget{Address: "device.local", TargetKind: AvailabilityTargetKind(""), Protocol: AvailabilityProbeICMP},
wantErr: true,
wantErrContains: "unsupported availability target kind",
},
// --- Protocol switch: ICMP arm. ---
{
name: "icmp with port set rejected",
target: AvailabilityTarget{Address: "device.local", TargetKind: AvailabilityTargetService, Protocol: AvailabilityProbeICMP, Port: 80},
wantErr: true,
wantErrContains: "icmp availability targets must not set a port",
},
{
name: "icmp machine kind valid happy path",
target: AvailabilityTarget{Address: "device.local", TargetKind: AvailabilityTargetMachine, Protocol: AvailabilityProbeICMP, Port: 0, PollIntervalSecs: 30, TimeoutMillis: 1000, FailureThreshold: 3},
wantErr: false,
},
{
name: "icmp device kind valid happy path",
target: AvailabilityTarget{Address: "device.local", TargetKind: AvailabilityTargetDevice, Protocol: AvailabilityProbeICMP, Port: 0, PollIntervalSecs: 30, TimeoutMillis: 1000, FailureThreshold: 3},
wantErr: false,
},
// --- Protocol switch: TCP arm. ---
{
name: "tcp with valid port accepted",
target: AvailabilityTarget{Address: "device.local", TargetKind: AvailabilityTargetService, Protocol: AvailabilityProbeTCP, Port: 443},
wantErr: false,
},
{
name: "tcp with zero port rejected",
target: AvailabilityTarget{Address: "device.local", TargetKind: AvailabilityTargetService, Protocol: AvailabilityProbeTCP, Port: 0},
wantErr: true,
wantErrContains: "tcp availability targets require a valid port",
},
{
name: "tcp with negative port rejected",
target: AvailabilityTarget{Address: "device.local", TargetKind: AvailabilityTargetService, Protocol: AvailabilityProbeTCP, Port: -1},
wantErr: true,
wantErrContains: "tcp availability targets require a valid port",
},
{
name: "tcp with out-of-range high port rejected",
target: AvailabilityTarget{Address: "device.local", TargetKind: AvailabilityTargetService, Protocol: AvailabilityProbeTCP, Port: 70000},
wantErr: true,
wantErrContains: "tcp availability targets require a valid port",
},
// --- Protocol switch: HTTP/HTTPS arm. ---
{
name: "http valid address accepted returns nil at http branch",
target: AvailabilityTarget{Address: "device.local", TargetKind: AvailabilityTargetService, Protocol: AvailabilityProbeHTTP, Port: 0},
wantErr: false,
},
{
name: "https valid address accepted returns nil at http branch",
target: AvailabilityTarget{Address: "device.local", TargetKind: AvailabilityTargetService, Protocol: AvailabilityProbeHTTPS, Port: 0},
wantErr: false,
},
{
name: "http negative port rejected",
target: AvailabilityTarget{Address: "device.local", TargetKind: AvailabilityTargetService, Protocol: AvailabilityProbeHTTP, Port: -1},
wantErr: true,
wantErrContains: "http availability target port must be valid",
},
{
name: "http out-of-range high port rejected",
target: AvailabilityTarget{Address: "device.local", TargetKind: AvailabilityTargetService, Protocol: AvailabilityProbeHTTP, Port: 70000},
wantErr: true,
wantErrContains: "http availability target port must be valid",
},
{
name: "http address with non-http scheme rejected via HTTPURL",
target: AvailabilityTarget{Address: "ftp://device.local", TargetKind: AvailabilityTargetService, Protocol: AvailabilityProbeHTTP, Port: 0},
wantErr: true,
wantErrContains: "http availability targets require http or https scheme",
},
// --- Protocol switch: default arm. ---
{
name: "unsupported protocol rejected",
target: AvailabilityTarget{Address: "device.local", TargetKind: AvailabilityTargetService, Protocol: AvailabilityProbeProtocol("udp")},
wantErr: true,
wantErrContains: "unsupported availability protocol",
},
{
name: "empty protocol hits default arm",
target: AvailabilityTarget{Address: "device.local", TargetKind: AvailabilityTargetService, Protocol: AvailabilityProbeProtocol("")},
wantErr: true,
wantErrContains: "unsupported availability protocol",
},
// --- Numeric bound checks (below-min rejected, boundary accepted). ---
{
name: "poll interval below minimum rejected",
target: AvailabilityTarget{Address: "device.local", TargetKind: AvailabilityTargetService, Protocol: AvailabilityProbeICMP, Port: 0, PollIntervalSecs: 9},
wantErr: true,
wantErrContains: "availability poll interval must be at least 10 seconds",
},
{
name: "poll interval at minimum boundary accepted",
target: AvailabilityTarget{Address: "device.local", TargetKind: AvailabilityTargetService, Protocol: AvailabilityProbeICMP, Port: 0, PollIntervalSecs: 10},
wantErr: false,
},
{
name: "timeout below minimum rejected",
target: AvailabilityTarget{Address: "device.local", TargetKind: AvailabilityTargetService, Protocol: AvailabilityProbeICMP, Port: 0, TimeoutMillis: 100},
wantErr: true,
wantErrContains: "availability timeout must be at least 250 milliseconds",
},
{
name: "timeout at minimum boundary accepted",
target: AvailabilityTarget{Address: "device.local", TargetKind: AvailabilityTargetService, Protocol: AvailabilityProbeICMP, Port: 0, TimeoutMillis: 250},
wantErr: false,
},
{
name: "failure threshold above maximum rejected",
target: AvailabilityTarget{Address: "device.local", TargetKind: AvailabilityTargetService, Protocol: AvailabilityProbeICMP, Port: 0, FailureThreshold: 11},
wantErr: true,
wantErrContains: "availability failure threshold must be 10 or less",
},
{
name: "failure threshold at maximum boundary accepted",
target: AvailabilityTarget{Address: "device.local", TargetKind: AvailabilityTargetService, Protocol: AvailabilityProbeICMP, Port: 0, FailureThreshold: 10},
wantErr: false,
},
// --- Non-HTTP host checks (reached only for icmp/tcp protocols). ---
{
name: "non-http address normalizing to empty host rejected",
target: AvailabilityTarget{Address: "[]", TargetKind: AvailabilityTargetService, Protocol: AvailabilityProbeICMP, Port: 0},
wantErr: true,
wantErrContains: "address is required",
},
{
name: "non-http address containing whitespace rejected",
target: AvailabilityTarget{Address: "host with space", TargetKind: AvailabilityTargetService, Protocol: AvailabilityProbeICMP, Port: 0},
wantErr: true,
wantErrContains: "must not contain whitespace",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.target.Validate()
if tt.wantErr {
assert.Error(t, err, "expected an error for %s", tt.name)
if tt.wantErrContains != "" && err != nil {
assert.Contains(t, err.Error(), tt.wantErrContains)
}
} else {
assert.NoError(t, err, "expected no error for %s", tt.name)
}
})
}
}
@@ -0,0 +1,279 @@
package config
import (
"testing"
"github.com/stretchr/testify/require"
)
// This file adds branch-coverage tests for the TrueNAS/VMware credential
// masking helpers and the VMware vCenter instance lifecycle methods. It
// targets branches that the existing *_test.go files in this package do not
// exercise: nil receivers, whitespace-tolerant mask matching, both sides of
// every conditional in PreserveMaskedSecrets/Validate/ApplyDefaults/Redacted,
// and the <= 0 / empty / whitespace-only boundary inputs.
func TestBranchCovIsTrueNASSensitiveMask(t *testing.T) {
tests := []struct {
name string
in string
want bool
}{
{"exact mask", trueNASSensitiveMask, true},
{"empty string", "", false},
{"whitespace padded mask still matches", " " + trueNASSensitiveMask + "\t", true},
{"newline wrapped mask still matches", "\n" + trueNASSensitiveMask + "\n", true},
{"whitespace only input", " \t\n", false},
{"unrelated secret value", "api-token-12345", false},
{"shorter star run not equal", "****", false},
{"longer star run not equal", "**********", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, IsTrueNASSensitiveMask(tt.in))
})
}
}
func TestBranchCovIsVMwareSensitiveMask(t *testing.T) {
tests := []struct {
name string
in string
want bool
}{
{"exact mask", vmwareSensitiveMask, true},
{"empty string", "", false},
{"whitespace padded mask still matches", " " + vmwareSensitiveMask + " ", true},
{"whitespace only input", "\t\t", false},
{"unrelated secret value", "vmware-password", false},
{"substring of mask not equal", "****", false},
{"longer star run not equal", "**********", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, IsVMwareSensitiveMask(tt.in))
})
}
}
func TestBranchCovTrueNASPreserveMaskedSecrets(t *testing.T) {
t.Run("nil receiver is a no-op and does not panic", func(t *testing.T) {
var inst *TrueNASInstance
require.NotPanics(t, func() {
inst.PreserveMaskedSecrets(TrueNASInstance{APIKey: "stored-key", Password: "stored-pass"})
})
})
t.Run("both fields masked restores both from existing", func(t *testing.T) {
inst := TrueNASInstance{APIKey: trueNASSensitiveMask, Password: trueNASSensitiveMask}
inst.PreserveMaskedSecrets(TrueNASInstance{APIKey: "stored-key", Password: "stored-pass"})
require.Equal(t, "stored-key", inst.APIKey)
require.Equal(t, "stored-pass", inst.Password)
})
t.Run("only api key masked leaves password from payload intact", func(t *testing.T) {
inst := TrueNASInstance{APIKey: trueNASSensitiveMask, Password: "new-pass"}
inst.PreserveMaskedSecrets(TrueNASInstance{APIKey: "stored-key", Password: "stored-pass"})
require.Equal(t, "stored-key", inst.APIKey)
require.Equal(t, "new-pass", inst.Password)
})
t.Run("only password masked leaves api key from payload intact", func(t *testing.T) {
inst := TrueNASInstance{APIKey: "new-key", Password: trueNASSensitiveMask}
inst.PreserveMaskedSecrets(TrueNASInstance{APIKey: "stored-key", Password: "stored-pass"})
require.Equal(t, "new-key", inst.APIKey)
require.Equal(t, "stored-pass", inst.Password)
})
t.Run("neither field masked keeps payload values unchanged", func(t *testing.T) {
inst := TrueNASInstance{APIKey: "fresh-key", Password: "fresh-pass"}
inst.PreserveMaskedSecrets(TrueNASInstance{APIKey: "stored-key", Password: "stored-pass"})
require.Equal(t, "fresh-key", inst.APIKey)
require.Equal(t, "fresh-pass", inst.Password)
})
t.Run("masked fields with empty existing restore to empty", func(t *testing.T) {
inst := TrueNASInstance{APIKey: trueNASSensitiveMask, Password: trueNASSensitiveMask}
inst.PreserveMaskedSecrets(TrueNASInstance{})
require.Equal(t, "", inst.APIKey)
require.Equal(t, "", inst.Password)
})
t.Run("whitespace padded mask is treated as the placeholder", func(t *testing.T) {
inst := TrueNASInstance{
APIKey: " " + trueNASSensitiveMask + " ",
Password: "\t" + trueNASSensitiveMask + "\n",
}
inst.PreserveMaskedSecrets(TrueNASInstance{APIKey: "stored-key", Password: "stored-pass"})
require.Equal(t, "stored-key", inst.APIKey)
require.Equal(t, "stored-pass", inst.Password)
})
}
func TestBranchCovVMwarePreserveMaskedSecrets(t *testing.T) {
t.Run("nil receiver is a no-op and does not panic", func(t *testing.T) {
var v *VMwareVCenterInstance
require.NotPanics(t, func() {
v.PreserveMaskedSecrets(VMwareVCenterInstance{Password: "stored-secret"})
})
})
t.Run("masked password restored from existing", func(t *testing.T) {
v := VMwareVCenterInstance{Password: vmwareSensitiveMask}
v.PreserveMaskedSecrets(VMwareVCenterInstance{Password: "stored-secret"})
require.Equal(t, "stored-secret", v.Password)
})
t.Run("unmasked password kept from payload", func(t *testing.T) {
v := VMwareVCenterInstance{Password: "new-secret"}
v.PreserveMaskedSecrets(VMwareVCenterInstance{Password: "stored-secret"})
require.Equal(t, "new-secret", v.Password)
})
t.Run("masked password with empty existing restores to empty", func(t *testing.T) {
v := VMwareVCenterInstance{Password: vmwareSensitiveMask}
v.PreserveMaskedSecrets(VMwareVCenterInstance{})
require.Equal(t, "", v.Password)
})
t.Run("whitespace padded mask is treated as the placeholder", func(t *testing.T) {
v := VMwareVCenterInstance{Password: " " + vmwareSensitiveMask + " "}
v.PreserveMaskedSecrets(VMwareVCenterInstance{Password: "stored-secret"})
require.Equal(t, "stored-secret", v.Password)
})
}
func TestBranchCovVMwareValidate(t *testing.T) {
tests := []struct {
name string
instance *VMwareVCenterInstance
wantError string
}{
{
name: "nil instance returns required error",
instance: nil,
wantError: "vmware vcenter instance is required",
},
{
name: "missing host returns host error",
instance: &VMwareVCenterInstance{Username: "admin", Password: "pass"},
wantError: "vmware vcenter host is required",
},
{
name: "whitespace only host returns host error",
instance: &VMwareVCenterInstance{Host: " ", Username: "admin", Password: "pass"},
wantError: "vmware vcenter host is required",
},
{
name: "missing username returns credentials error",
instance: &VMwareVCenterInstance{Host: "vc.local", Password: "pass"},
wantError: "vmware credentials are required",
},
{
name: "missing password returns credentials error",
instance: &VMwareVCenterInstance{Host: "vc.local", Username: "admin"},
wantError: "vmware credentials are required",
},
{
name: "whitespace only username returns credentials error",
instance: &VMwareVCenterInstance{Host: "vc.local", Username: " ", Password: "pass"},
wantError: "vmware credentials are required",
},
{
name: "whitespace only password returns credentials error",
instance: &VMwareVCenterInstance{Host: "vc.local", Username: "admin", Password: "\t"},
wantError: "vmware credentials are required",
},
{
name: "valid host and credentials pass",
instance: &VMwareVCenterInstance{Host: "vc.local", Username: "admin", Password: "pass"},
wantError: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.instance.Validate()
if tt.wantError == "" {
require.NoError(t, err)
return
}
require.Error(t, err)
require.Contains(t, err.Error(), tt.wantError)
})
}
}
func TestBranchCovVMwareApplyDefaults(t *testing.T) {
t.Run("nil receiver is a no-op and does not panic", func(t *testing.T) {
var v *VMwareVCenterInstance
require.NotPanics(t, func() { v.ApplyDefaults() })
})
t.Run("positive port preserved and explicit single surface kept", func(t *testing.T) {
v := VMwareVCenterInstance{Port: 8443, MonitorHosts: true}
v.ApplyDefaults()
require.Equal(t, 8443, v.Port)
require.False(t, v.MonitorVMs)
require.True(t, v.MonitorHosts)
require.False(t, v.MonitorDatastores)
})
t.Run("negative port reset to default and legacy scope migrated", func(t *testing.T) {
v := VMwareVCenterInstance{Port: -1}
v.ApplyDefaults()
require.Equal(t, defaultVMwarePort, v.Port)
require.True(t, v.MonitorVMs)
require.True(t, v.MonitorHosts)
require.True(t, v.MonitorDatastores)
})
t.Run("all surfaces explicitly enabled are left enabled", func(t *testing.T) {
v := VMwareVCenterInstance{
Port: defaultVMwarePort,
MonitorVMs: true,
MonitorHosts: true,
MonitorDatastores: true,
}
v.ApplyDefaults()
require.True(t, v.MonitorVMs)
require.True(t, v.MonitorHosts)
require.True(t, v.MonitorDatastores)
require.Equal(t, defaultVMwarePort, v.Port)
})
}
func TestBranchCovVMwareRedacted(t *testing.T) {
t.Run("nil receiver returns zero value and does not panic", func(t *testing.T) {
var v *VMwareVCenterInstance
require.NotPanics(t, func() {
require.Equal(t, VMwareVCenterInstance{}, v.Redacted())
})
})
t.Run("empty password is not masked", func(t *testing.T) {
v := VMwareVCenterInstance{Host: "vc.local", Password: ""}
got := v.Redacted()
require.Equal(t, "", got.Password)
})
t.Run("whitespace only password is not masked", func(t *testing.T) {
v := VMwareVCenterInstance{Host: "vc.local", Password: " "}
got := v.Redacted()
require.Equal(t, " ", got.Password)
})
t.Run("whitespace padded real password is masked", func(t *testing.T) {
v := VMwareVCenterInstance{Host: "vc.local", Password: " real-secret "}
got := v.Redacted()
require.Equal(t, vmwareSensitiveMask, got.Password)
})
t.Run("receiver is not mutated by redaction", func(t *testing.T) {
v := VMwareVCenterInstance{Host: "vc.local", Username: "admin", Password: "super-secret"}
got := v.Redacted()
require.Equal(t, "super-secret", v.Password)
require.Equal(t, vmwareSensitiveMask, got.Password)
require.Equal(t, "admin", got.Username)
require.Equal(t, "vc.local", got.Host)
})
}
@@ -0,0 +1,190 @@
package config
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
// This file adds branch coverage for the report-branding helpers in
// report_branding.go: DecodeReportBrandLogoBase64 and
// CanonicalReportBrandLogoFormat. Test functions use the BranchCov prefix so
// they can be selected with `-run BranchCov`.
// TestBranchCovDecodeReportBrandLogoBase64 exercises every branch of
// DecodeReportBrandLogoBase64:
// - empty / whitespace-only input (returns nil, nil)
// - input without a ";base64," data-URL separator (used verbatim)
// - input with a ";base64," separator (prefix stripped)
// - leading and repeated ";base64," separators
// - the base64.StdEncoding success path
// - the base64.StdEncoding failure / RawStdEncoding fallback success path
// - both encodings failing (final error return)
// - the length boundary at exactly ReportBrandLogoBase64MaxLength and one
// character over it
func TestBranchCovDecodeReportBrandLogoBase64(t *testing.T) {
// "AAAAAA" (6 chars, no padding) decodes cleanly under RawStdEncoding but
// is rejected by StdEncoding (which expects padding to a multiple of 4),
// so it forces the fallback path. "AAA" (3 chars) does the same.
const rawFallbackInput = "AAAAAA"
// "QUJD" is "ABC" in standard base64 (4 chars, multiple of 4) and succeeds
// under StdEncoding on the first try.
const stdSuccessInput = "QUJD"
tests := []struct {
name string
input string
want []byte
wantErr bool
errSub string
}{
{
name: "empty input returns nil nil",
input: "",
want: nil,
},
{
name: "whitespace only trims to empty returns nil nil",
input: " \t\n ",
want: nil,
},
{
name: "plain std base64 no separator std success path",
input: stdSuccessInput,
want: []byte("ABC"),
},
{
name: "plain raw base64 no separator raw fallback path",
input: rawFallbackInput,
want: []byte{0, 0, 0, 0},
},
{
name: "non-base64 garbage both encodings fail",
input: "!!!!not-base64!!!!",
wantErr: true,
errSub: "logoBase64 must be valid base64",
},
{
name: "five chars not multiple of four both fail",
input: "QUJDA",
wantErr: true,
errSub: "logoBase64 must be valid base64",
},
{
name: "data url separator strips prefix and decodes",
input: "data:image/png;base64," + stdSuccessInput,
want: []byte("ABC"),
},
{
name: "data url separator with raw payload uses fallback",
input: "data:image/png;base64," + rawFallbackInput,
want: []byte{0, 0, 0, 0},
},
{
name: "leading base64 separator at index zero",
input: ";base64," + stdSuccessInput,
want: []byte("ABC"),
},
{
name: "multiple separators only first is stripped second leaks into payload",
input: ";base64," + stdSuccessInput + ";base64,BBBB",
wantErr: true,
errSub: "logoBase64 must be valid base64",
},
{
name: "data url with empty payload decodes to empty non-nil slice",
input: "data:image/png;base64,",
want: []byte{},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := DecodeReportBrandLogoBase64(tc.input)
if tc.wantErr {
assert.Error(t, err)
if tc.errSub != "" {
assert.Contains(t, err.Error(), tc.errSub)
}
assert.Nil(t, got)
return
}
assert.NoError(t, err)
assert.Equal(t, tc.want, got)
})
}
// Boundary: exactly at the configured max length. Build a valid standard
// base64 value whose length equals ReportBrandLogoBase64MaxLength (which is
// a multiple of 4) so StdEncoding succeeds and the length check passes.
t.Run("length exactly at limit succeeds", func(t *testing.T) {
assert.Equal(t, 0, ReportBrandLogoBase64MaxLength%4,
"max length must be a multiple of 4 for this boundary case to be well-defined")
value := strings.Repeat("A", ReportBrandLogoBase64MaxLength)
dec, err := DecodeReportBrandLogoBase64(value)
assert.NoError(t, err)
assert.Len(t, dec, ReportBrandLogoBase64MaxLength/4*3)
})
// Boundary: one character over the limit must be rejected by the length
// check before any decoding is attempted, regardless of payload validity.
t.Run("length one over limit rejected before decode", func(t *testing.T) {
value := strings.Repeat("A", ReportBrandLogoBase64MaxLength+1)
dec, err := DecodeReportBrandLogoBase64(value)
assert.Error(t, err)
assert.Contains(t, err.Error(), "logoBase64 must be <= ")
assert.Contains(t, err.Error(), "49152 characters")
assert.Nil(t, dec)
})
// Boundary: length check is applied to the post-strip value, so a data URL
// whose base64 payload alone exceeds the limit must still be rejected.
t.Run("oversized payload after data url strip rejected", func(t *testing.T) {
payload := strings.Repeat("A", ReportBrandLogoBase64MaxLength+1)
dec, err := DecodeReportBrandLogoBase64("data:image/png;base64," + payload)
assert.Error(t, err)
assert.Contains(t, err.Error(), "logoBase64 must be <= 49152 characters")
assert.Nil(t, dec)
})
}
// TestBranchCovCanonicalReportBrandLogoFormat covers every arm of the switch in
// CanonicalReportBrandLogoFormat (including the default rejection branch) and
// the leading/trailing whitespace plus case-normalisation performed before the
// switch is entered.
func TestBranchCovCanonicalReportBrandLogoFormat(t *testing.T) {
tests := []struct {
name string
input string
want string
wantOk bool
}{
{name: "empty string accepted as empty canonical", input: "", want: "", wantOk: true},
{name: "whitespace only trims to empty accepted", input: " \t ", want: "", wantOk: true},
{name: "png canonical", input: "png", want: "png", wantOk: true},
{name: "png uppercased normalised", input: "PNG", want: "png", wantOk: true},
{name: "png mixed case trimmed", input: " PnG ", want: "png", wantOk: true},
{name: "jpg canonical", input: "jpg", want: "jpg", wantOk: true},
{name: "jpeg alias collapses to jpg", input: "jpeg", want: "jpg", wantOk: true},
{name: "JPEG uppercased collapses to jpg", input: " JPEG ", want: "jpg", wantOk: true},
{name: "gif canonical", input: "gif", want: "gif", wantOk: true},
{name: "GIF uppercased normalised", input: "GIF", want: "gif", wantOk: true},
{name: "unknown format webp rejected", input: "webp", want: "", wantOk: false},
{name: "unknown format svg rejected", input: "svg", want: "", wantOk: false},
{name: "png with trailing digit rejected by default", input: "png2", want: "", wantOk: false},
{name: "mime style value rejected by default", input: "image/png", want: "", wantOk: false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, ok := CanonicalReportBrandLogoFormat(tc.input)
assert.Equal(t, tc.wantOk, ok, "ok mismatch for input %q", tc.input)
assert.Equal(t, tc.want, got, "canonical value mismatch for input %q", tc.input)
})
}
}
@@ -0,0 +1,110 @@
package models
import "testing"
// TestBranchCovOrganizationRoleFromAccountRole exercises every switch arm of
// OrganizationRoleFromAccountRole, including the strings.ToLower /
// strings.TrimSpace normalization that gates the switch, and the default arm
// reached by empty, whitespace-only, and unknown inputs.
func TestBranchCovOrganizationRoleFromAccountRole(t *testing.T) {
testCases := []struct {
name string
role string
want OrganizationRole
}{
// Explicit named arms (canonical spelling).
{name: "owner arm", role: "owner", want: OrgRoleOwner},
{name: "admin arm", role: "admin", want: OrgRoleAdmin},
{name: "tech arm maps to editor", role: "tech", want: OrgRoleEditor},
{name: "read_only arm maps to viewer", role: "read_only", want: OrgRoleViewer},
// ToLower normalization: each named arm must be reached case-insensitively.
{name: "OWNER upper hits owner arm", role: "OWNER", want: OrgRoleOwner},
{name: "Owner mixed hits owner arm", role: "Owner", want: OrgRoleOwner},
{name: "ADMIN upper hits admin arm", role: "ADMIN", want: OrgRoleAdmin},
{name: "Tech mixed hits tech arm", role: "Tech", want: OrgRoleEditor},
{name: "READ_ONLY upper hits read_only arm", role: "READ_ONLY", want: OrgRoleViewer},
// TrimSpace normalization: surrounding whitespace must not affect mapping.
{name: "leading space owner", role: " owner", want: OrgRoleOwner},
{name: "trailing space owner", role: "owner ", want: OrgRoleOwner},
{name: "surrounding whitespace admin", role: "\tadmin\n", want: OrgRoleAdmin},
{name: "surrounding whitespace read_only", role: " read_only ", want: OrgRoleViewer},
// Combined case-folding + trimming for each arm.
{name: "trimmed upper OWNER", role: " OWNER ", want: OrgRoleOwner},
{name: "trimmed mixed Tech", role: " Tech ", want: OrgRoleEditor},
// Default arm: empty, whitespace-only, and unknown inputs all fall
// through to OrgRoleViewer.
{name: "empty string default", role: "", want: OrgRoleViewer},
{name: "whitespace-only default", role: " \t\n", want: OrgRoleViewer},
{name: "unknown role default", role: "billing", want: OrgRoleViewer},
{name: "superuser unknown default", role: "superuser", want: OrgRoleViewer},
// Boundary: "read_only" (with underscore) is a recognized arm, whereas
// the visually similar "readonly" (no underscore) is NOT and must
// fall through to the default arm. Both yield OrgRoleViewer, but they
// exercise different branches.
{name: "read_only underscore arm", role: "read_only", want: OrgRoleViewer},
{name: "readonly no-underscore hits default", role: "readonly", want: OrgRoleViewer},
// Sanity: the viewer-equivalent account roles are NOT OrgRoleOwner/Admin/Editor.
{name: "read_only does not upgrade", role: "read_only", want: OrgRoleViewer},
{name: "unknown does not upgrade", role: "root", want: OrgRoleViewer},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := OrganizationRoleFromAccountRole(tc.role)
if got != tc.want {
t.Fatalf("OrganizationRoleFromAccountRole(%q) = %q, want %q",
tc.role, got, tc.want)
}
})
}
}
// TestBranchCovOrganizationRoleFromAccountRole_DistinctArms verifies that the
// four non-default arms each return a distinct (or specifically viewer-bound)
// result, so coverage of the read_only -> viewer path is not conflated with
// the default -> viewer path. This guards against accidental arm removal.
func TestBranchCovOrganizationRoleFromAccountRole_DistinctArms(t *testing.T) {
owner := OrganizationRoleFromAccountRole("owner")
admin := OrganizationRoleFromAccountRole("admin")
editor := OrganizationRoleFromAccountRole("tech")
readOnly := OrganizationRoleFromAccountRole("read_only")
def := OrganizationRoleFromAccountRole("totally-unknown")
if owner != OrgRoleOwner {
t.Fatalf("owner arm = %q, want %q", owner, OrgRoleOwner)
}
if admin != OrgRoleAdmin {
t.Fatalf("admin arm = %q, want %q", admin, OrgRoleAdmin)
}
if editor != OrgRoleEditor {
t.Fatalf("tech arm = %q, want %q", editor, OrgRoleEditor)
}
// Both read_only and default return OrgRoleViewer by design (read_only is
// the least-privileged known account role; unknown roles are downgraded to
// least privilege for safety). Assert they agree on the value but exercise
// both arms.
if readOnly != OrgRoleViewer {
t.Fatalf("read_only arm = %q, want %q", readOnly, OrgRoleViewer)
}
if def != OrgRoleViewer {
t.Fatalf("default arm = %q, want %q", def, OrgRoleViewer)
}
// owner/admin/editor must all be distinct from each other and from viewer.
seen := map[OrganizationRole]string{}
for label, r := range map[string]OrganizationRole{
"owner": owner, "admin": admin, "editor": editor, "viewer(read_only)": readOnly,
} {
if prev, dup := seen[r]; dup {
t.Fatalf("role collision: %q and %q both map to %q", prev, label, r)
}
seen[r] = label
}
}
@@ -0,0 +1,240 @@
package operationreceipt
import (
"encoding/json"
"strings"
"testing"
"time"
)
// This file exercises the previously uncovered branches of DecodeQuery and
// DecodeQueryResult in internal/operationreceipt/types.go: every error arm
// of decodeStrict, the version guards, the per-status switch arms of
// DecodeQueryResult (including the default), both sides of each record
// nil/state conditional, and ValidateRecord invocation on the non-nil path.
// withRogueField injects an unknown top-level JSON field into a marshalled
// object so we can exercise decodeStrict's DisallowUnknownFields branch
// without hand-rolling a long valid payload.
func withRogueField(t *testing.T, valid []byte) []byte {
t.Helper()
var obj map[string]json.RawMessage
if err := json.Unmarshal(valid, &obj); err != nil {
t.Fatalf("unmarshal for rogue injection: %v", err)
}
obj["rogue_field"] = json.RawMessage(`true`)
out, err := json.Marshal(obj)
if err != nil {
t.Fatalf("marshal for rogue injection: %v", err)
}
return out
}
func TestBranchCovDecodeQuery(t *testing.T) {
validID := testIdentity("decode-query")
validQueryBytes, err := json.Marshal(Query{Version: ProtocolVersion, Identity: validID})
if err != nil {
t.Fatalf("marshal valid query: %v", err)
}
// Identity whose string fields carry surrounding whitespace; after
// DecodeQuery runs NormalizeIdentity the result must equal validID.
spaceyID := validID
spaceyID.AttemptID = " " + validID.AttemptID + "\t"
spaceyID.ActionID = " " + validID.ActionID + " "
spaceyID.OperationKind = "\t" + validID.OperationKind + " "
spaceyID.RequestDigest = " " + validID.RequestDigest + " "
spaceyID.AgentID = " " + validID.AgentID + " "
spaceyQueryBytes, err := json.Marshal(Query{Version: ProtocolVersion, Identity: spaceyID})
if err != nil {
t.Fatalf("marshal spacey query: %v", err)
}
badVersionBytes, err := json.Marshal(Query{Version: 99, Identity: validID})
if err != nil {
t.Fatalf("marshal bad-version query: %v", err)
}
emptyIdentityBytes, err := json.Marshal(Query{Version: ProtocolVersion, Identity: Identity{}})
if err != nil {
t.Fatalf("marshal empty-identity query: %v", err)
}
zeroVersionBytes, err := json.Marshal(Query{Version: ProtocolVersion, Identity: Identity{
AttemptID: "a", ActionID: "b", OperationKind: "k", OperationVersion: 0,
RequestDigest: validID.RequestDigest, AgentID: "g",
}})
if err != nil {
t.Fatalf("marshal zero-version query: %v", err)
}
trailingBytes := append(append([]byte{}, validQueryBytes...), []byte(` {"after":true}`)...)
cases := []struct {
name string
data []byte
wantOK bool
errSub string // asserted (as substring) only when !wantOK
wantID Identity
checkTrim bool // on success, assert identity equals wantID (post-trim)
}{
{name: "nil payload rejected", data: nil, wantOK: false, errSub: "payload is empty"},
{name: "whitespace-only payload rejected", data: []byte(" \n\t "), wantOK: false, errSub: "payload is empty"},
{name: "malformed JSON rejected", data: []byte(`{"version":1`), wantOK: false},
{name: "trailing JSON rejected", data: trailingBytes, wantOK: false, errSub: "trailing"},
{name: "unknown top-level field rejected", data: withRogueField(t, validQueryBytes), wantOK: false, errSub: "unknown field"},
{name: "unsupported version rejected", data: badVersionBytes, wantOK: false, errSub: "unsupported operation query version 99"},
{name: "empty identity rejected", data: emptyIdentityBytes, wantOK: false, errSub: "identity fields are required"},
{name: "non-positive operation version rejected", data: zeroVersionBytes, wantOK: false, errSub: "operation version must be positive"},
{name: "valid query decodes", data: validQueryBytes, wantOK: true, wantID: validID, checkTrim: true},
{name: "untrimmed identity fields are normalized", data: spaceyQueryBytes, wantOK: true, wantID: validID, checkTrim: true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
q, err := DecodeQuery(tc.data)
if !tc.wantOK {
if err == nil {
t.Fatalf("expected error, got nil (q=%+v)", q)
}
if tc.errSub != "" && !strings.Contains(err.Error(), tc.errSub) {
t.Fatalf("error %q does not contain %q", err.Error(), tc.errSub)
}
if q.Version != 0 || q.Identity != (Identity{}) {
t.Fatalf("expected zero-value Query on error, got %+v", q)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if q.Version != ProtocolVersion {
t.Fatalf("version=%d want=%d", q.Version, ProtocolVersion)
}
if tc.checkTrim && q.Identity != tc.wantID {
t.Fatalf("identity not normalized/round-tripped:\n got %+v\n want %+v", q.Identity, tc.wantID)
}
})
}
}
func TestBranchCovDecodeQueryResult(t *testing.T) {
id := testIdentity("decode-result")
now := time.Date(2026, 7, 16, 12, 0, 0, 0, time.UTC)
acceptedRec := Record{Identity: id, State: StateAccepted, AcceptedAt: now}
startedRec := Record{Identity: id, State: StateStarted, AcceptedAt: now, StartedAt: now}
interruptedRec := Record{Identity: id, State: StateInterrupted, AcceptedAt: now, StartedAt: now}
tombstoneRec := Record{Identity: id, State: StateTombstone, AcceptedAt: now, StartedAt: now, TerminalAt: now}
terminalRec := Record{
Identity: id,
State: StateTerminal,
AcceptedAt: now,
StartedAt: now,
TerminalAt: now,
ResultKind: "test.kind",
ResultVersion: 1,
Result: json.RawMessage(`{}`),
}
// Record that passes the found_interrupted state check (State==accepted)
// but fails ValidateRecord because AcceptedAt is zero.
boundsBadRec := Record{Identity: id, State: StateAccepted}
marshal := func(status QueryStatus, record *Record) []byte {
t.Helper()
data, err := json.Marshal(QueryResult{Version: ProtocolVersion, Status: status, Record: record})
if err != nil {
t.Fatalf("marshal result: %v", err)
}
return data
}
marshalVersion := func(version int, status QueryStatus, record *Record) []byte {
t.Helper()
data, err := json.Marshal(QueryResult{Version: version, Status: status, Record: record})
if err != nil {
t.Fatalf("marshal result: %v", err)
}
return data
}
emptyRecord := &Record{}
cases := []struct {
name string
data []byte
wantOK bool
errSub string
wantStatus QueryStatus
wantState State // checked when non-empty and wantOK
wantNilRec bool
}{
// decodeStrict / version branches.
{name: "empty payload rejected", data: nil, wantOK: false, errSub: "payload is empty"},
{name: "trailing JSON rejected", data: append(append([]byte{}, marshal(QueryNotFound, nil)...), []byte(" {}")...), wantOK: false, errSub: "trailing"},
{name: "unknown field rejected", data: withRogueField(t, marshal(QueryNotFound, nil)), wantOK: false, errSub: "unknown field"},
{name: "unsupported version rejected", data: marshalVersion(7, QueryNotFound, nil), wantOK: false, errSub: "unsupported operation query result version 7"},
// QueryNotFound arm.
{name: "not_found without record succeeds", data: marshal(QueryNotFound, nil), wantOK: true, wantStatus: QueryNotFound, wantNilRec: true},
{name: "not_found with record rejected", data: marshal(QueryNotFound, emptyRecord), wantOK: false, errSub: "not-found query result cannot include a record"},
// QueryFoundTerminal arm: nil record, wrong-state record, and valid record.
{name: "found_terminal with nil record rejected", data: marshal(QueryFoundTerminal, nil), wantOK: false, errSub: "terminal query result requires terminal record"},
{name: "found_terminal with non-terminal record rejected", data: marshal(QueryFoundTerminal, &startedRec), wantOK: false, errSub: "terminal query result requires terminal record"},
{name: "found_terminal with valid terminal record succeeds", data: marshal(QueryFoundTerminal, &terminalRec), wantOK: true, wantStatus: QueryFoundTerminal, wantState: StateTerminal},
// QueryFoundInterrupted arm: nil record, wrong-state record, and each
// accepted state.
{name: "found_interrupted with nil record rejected", data: marshal(QueryFoundInterrupted, nil), wantOK: false, errSub: "interrupted query result requires nonterminal or tombstone record"},
{name: "found_interrupted with terminal record rejected", data: marshal(QueryFoundInterrupted, &terminalRec), wantOK: false, errSub: "interrupted query result requires nonterminal or tombstone record"},
{name: "found_interrupted with accepted record succeeds", data: marshal(QueryFoundInterrupted, &acceptedRec), wantOK: true, wantStatus: QueryFoundInterrupted, wantState: StateAccepted},
{name: "found_interrupted with started record succeeds", data: marshal(QueryFoundInterrupted, &startedRec), wantOK: true, wantStatus: QueryFoundInterrupted, wantState: StateStarted},
{name: "found_interrupted with interrupted record succeeds", data: marshal(QueryFoundInterrupted, &interruptedRec), wantOK: true, wantStatus: QueryFoundInterrupted, wantState: StateInterrupted},
{name: "found_interrupted with tombstone record succeeds", data: marshal(QueryFoundInterrupted, &tombstoneRec), wantOK: true, wantStatus: QueryFoundInterrupted, wantState: StateTombstone},
// Non-nil record path must still run ValidateRecord; a record that
// passes the state check but fails validation is rejected.
{name: "non-nil record failing ValidateRecord rejected", data: marshal(QueryFoundInterrupted, &boundsBadRec), wantOK: false, errSub: "invalid operation receipt bounds"},
// default arm: unsupported status.
{name: "unsupported status rejected", data: marshal(QueryStatus("bogus"), nil), wantOK: false, errSub: "unsupported operation query status"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
r, err := DecodeQueryResult(tc.data)
if !tc.wantOK {
if err == nil {
t.Fatalf("expected error, got nil (r=%+v)", r)
}
if tc.errSub != "" && !strings.Contains(err.Error(), tc.errSub) {
t.Fatalf("error %q does not contain %q", err.Error(), tc.errSub)
}
if r.Version != 0 || r.Status != "" || r.Record != nil {
t.Fatalf("expected zero-value QueryResult on error, got %+v", r)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if r.Version != ProtocolVersion {
t.Fatalf("version=%d want=%d", r.Version, ProtocolVersion)
}
if r.Status != tc.wantStatus {
t.Fatalf("status=%q want=%q", r.Status, tc.wantStatus)
}
if tc.wantNilRec {
if r.Record != nil {
t.Fatalf("expected nil record, got %+v", r.Record)
}
return
}
if r.Record == nil {
t.Fatalf("expected non-nil record")
}
if tc.wantState != "" && r.Record.State != tc.wantState {
t.Fatalf("state=%q want=%q", r.Record.State, tc.wantState)
}
})
}
}
@@ -0,0 +1,129 @@
package securityutil
import (
"strings"
"testing"
)
// TestBranchCovHashedStorageName pins HashedStorageName to the published
// SHA-256 test vectors (an independent oracle, not a re-implementation of the
// function under test) and exercises the edge cases the function's single
// expression quietly handles: empty input, multibyte/Unicode input, and the
// filename-safety contract of the returned stem.
func TestBranchCovHashedStorageName(t *testing.T) {
t.Run("known_sha256_vectors", func(t *testing.T) {
tests := []struct {
name string
id string
want string
}{
{name: "empty string maps to sha256 of empty input", id: "", want: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},
{name: "nist abc vector", id: "abc", want: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"},
{name: "hello vector", id: "hello", want: "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"},
{name: "multibyte unicode input is hashed over utf-8 bytes", id: "unicode-→-é-你", want: "2efbbf4012701ef1bdceac2226531cc5ad0f940083dbf7bc75d1d883c0012586"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := HashedStorageName(tt.id); got != tt.want {
t.Fatalf("HashedStorageName(%q) = %q, want %q", tt.id, got, tt.want)
}
})
}
})
t.Run("deterministic_and_distinct", func(t *testing.T) {
a := HashedStorageName("tenant-a/user-123")
b := HashedStorageName("tenant-a/user-123")
if a != b {
t.Fatalf("HashedStorageName is not deterministic: %q vs %q", a, b)
}
// Distinct inputs (including the empty/non-empty boundary) must not collide.
empty := HashedStorageName("")
nonEmpty := HashedStorageName("x")
if empty == nonEmpty {
t.Fatalf("HashedStorageName collapsed empty and %q to the same digest %q", "x", empty)
}
})
t.Run("filename_safe_fixed_width_lowercase_hex", func(t *testing.T) {
for _, id := range []string{"", "a", strings.Repeat("long-", 64), "with/slash", "with..dot"} {
got := HashedStorageName(id)
if len(got) != 64 {
t.Fatalf("HashedStorageName(%q) length = %d, want fixed 64 hex chars", id, len(got))
}
for i, r := range got {
isHex := (r >= '0' && r <= '9') || (r >= 'a' && r <= 'f')
if !isHex {
t.Fatalf("HashedStorageName(%q) produced non-lowercase-hex rune %q at %d in %q", id, r, i, got)
}
}
// A storage stem must never smuggle path separators or extensions.
if strings.ContainsAny(got, `/\.`) {
t.Fatalf("HashedStorageName(%q) produced unsafe filename %q", id, got)
}
}
})
}
// TestBranchCovNormalizeAbsoluteHTTPURL drives every branch of the underlying
// NormalizeAbsoluteHTTPURL validator through the in-package wrapper: the
// empty-input guard, the url.Parse error path, the scheme/host/userinfo/hostname
// rejection arms, and the success path (which preserves query and fragment and
// relies on url.Parse to lowercase the scheme while leaving the host cased as
// given).
func TestBranchCovNormalizeAbsoluteHTTPURL(t *testing.T) {
tests := []struct {
name string
raw string
want string
wantError string
}{
{name: "empty input rejected", raw: "", wantError: "URL is required"},
{name: "whitespace-only input rejected after trim", raw: " \t\n ", wantError: "URL is required"},
{name: "malformed ipv6 host triggers parse error", raw: "http://[::1", wantError: "invalid URL"},
{name: "unsupported ftp scheme rejected", raw: "ftp://example.com", wantError: "URL scheme must be http or https"},
{name: "scheme-relative url rejected as missing scheme", raw: "//example.com/path", wantError: "URL scheme must be http or https"},
{name: "bare path rejected as missing scheme", raw: "example.com", wantError: "URL scheme must be http or https"},
{name: "empty host on bare scheme rejected", raw: "http://", wantError: "URL host is required"},
{name: "empty host with path rejected", raw: "http:///path", wantError: "URL host is required"},
{name: "userinfo user password rejected", raw: "http://user:pass@example.com", wantError: "URL userinfo is not allowed"},
{name: "userinfo user only rejected", raw: "http://user@example.com", wantError: "URL userinfo is not allowed"},
{name: "port-only host has empty hostname", raw: "http://:8080/path", wantError: "URL hostname is required"},
{name: "uppercase scheme accepted via url parse lowercasing, host case preserved", raw: "HTTPS://Example.com", want: "https://Example.com"},
{name: "leading and trailing whitespace is trimmed before parsing", raw: " https://example.com/path?q=1#frag ", want: "https://example.com/path?q=1#frag"},
{name: "success preserves query and fragment unlike base url normalizers", raw: "https://example.com/path?q=1#frag", want: "https://example.com/path?q=1#frag"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NormalizeAbsoluteHTTPURL(tt.raw)
if tt.wantError != "" {
if err == nil {
t.Fatalf("NormalizeAbsoluteHTTPURL(%q) = %q, want error containing %q", tt.raw, got, tt.wantError)
}
if !strings.Contains(err.Error(), tt.wantError) {
t.Fatalf("NormalizeAbsoluteHTTPURL(%q) error = %q, want substring %q", tt.raw, err.Error(), tt.wantError)
}
if got != nil {
t.Fatalf("NormalizeAbsoluteHTTPURL(%q) returned non-nil URL %q alongside error", tt.raw, got)
}
return
}
if err != nil {
t.Fatalf("NormalizeAbsoluteHTTPURL(%q) error = %v", tt.raw, err)
}
if got == nil {
t.Fatalf("NormalizeAbsoluteHTTPURL(%q) returned nil URL", tt.raw)
}
if got.String() != tt.want {
t.Fatalf("NormalizeAbsoluteHTTPURL(%q) = %q, want %q", tt.raw, got.String(), tt.want)
}
})
}
}
@@ -0,0 +1,490 @@
package securityutil
import (
"net/url"
"strings"
"testing"
)
// mustParseURL parses raw and fails the test if it cannot be parsed. It is a
// small convenience used only to construct *url.URL fixtures for the append/
// resolve helpers; it does not re-implement any package logic.
func mustParseURL(t *testing.T, raw string) *url.URL {
t.Helper()
u, err := url.Parse(raw)
if err != nil {
t.Fatalf("mustParseURL(%q): %v", raw, err)
}
return u
}
func TestBranchCovNormalizeAbsoluteHTTPURL(t *testing.T) {
tests := []struct {
name string
raw string
wantErr string // non-empty substring expected when an error is returned
wantOK bool // when true, expect success and verify resulting URL
wantStr string // expected String() when wantOK
wantHost string
}{
{name: "empty after trim", raw: " ", wantErr: "URL is required"},
{name: "whitespace trimmed to success", raw: " https://example.com/a ", wantOK: true, wantStr: "https://example.com/a", wantHost: "example.com"},
{name: "parse error invalid escape", raw: "http://host/%zz", wantErr: "invalid URL"},
{name: "missing protocol scheme parse error", raw: ":badurl", wantErr: "invalid URL"},
{name: "unsupported scheme ftp", raw: "ftp://host", wantErr: "scheme must be http or https"},
{name: "empty host", raw: "https://", wantErr: "URL host is required"},
{name: "userinfo rejected", raw: "https://user:pw@host", wantErr: "URL userinfo is not allowed"},
{name: "port only hostname empty", raw: "https://:443", wantErr: "URL hostname is required"},
{name: "https success default port", raw: "https://example.com:443/path", wantOK: true, wantStr: "https://example.com:443/path"},
{name: "http success", raw: "http://1.2.3.4/x", wantOK: true, wantStr: "http://1.2.3.4/x"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NormalizeAbsoluteHTTPURL(tt.raw)
if tt.wantErr != "" {
if err == nil {
t.Fatalf("NormalizeAbsoluteHTTPURL(%q) = %v, want error containing %q", tt.raw, got, tt.wantErr)
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("NormalizeAbsoluteHTTPURL(%q) err = %q, want substring %q", tt.raw, err.Error(), tt.wantErr)
}
return
}
if err != nil {
t.Fatalf("NormalizeAbsoluteHTTPURL(%q) unexpected error: %v", tt.raw, err)
}
if tt.wantOK {
if got == nil {
t.Fatalf("NormalizeAbsoluteHTTPURL(%q) returned nil URL", tt.raw)
}
if got.String() != tt.wantStr {
t.Fatalf("NormalizeAbsoluteHTTPURL(%q) String() = %q, want %q", tt.raw, got.String(), tt.wantStr)
}
if tt.wantHost != "" && got.Hostname() != tt.wantHost {
t.Fatalf("NormalizeAbsoluteHTTPURL(%q) Hostname() = %q, want %q", tt.raw, got.Hostname(), tt.wantHost)
}
}
})
}
}
func TestBranchCovNormalizeHTTPBaseURL(t *testing.T) {
tests := []struct {
name string
raw string
defaultScheme string
wantErr string
wantPath string
wantScheme string
wantString string
wantDefaultApplied bool // verifies the defaultScheme prepend branch fired
}{
{name: "empty required", raw: " ", defaultScheme: "https", wantErr: "base URL is required"},
{name: "default scheme applied", raw: "example.com", defaultScheme: "https", wantScheme: "https", wantPath: "", wantString: "https://example.com", wantDefaultApplied: true},
{name: "default scheme applied with path", raw: "example.com/pulse", defaultScheme: "http", wantScheme: "http", wantPath: "/pulse", wantString: "http://example.com/pulse", wantDefaultApplied: true},
{name: "default scheme ignored when scheme present", raw: "https://example.com", defaultScheme: "ftp", wantScheme: "https", wantPath: "", wantString: "https://example.com"},
{name: "no default scheme and no scheme delegates error", raw: "example.com", defaultScheme: "", wantErr: "scheme must be http or https"},
{name: "delegates unsupported scheme", raw: "ftp://host", defaultScheme: "", wantErr: "scheme must be http or https"},
{name: "query rejected", raw: "https://host/?q=1", defaultScheme: "", wantErr: "must not include query or fragment"},
{name: "fragment rejected", raw: "https://host/#frag", defaultScheme: "", wantErr: "must not include query or fragment"},
{name: "root path collapses to empty", raw: "https://host/", defaultScheme: "", wantScheme: "https", wantPath: "", wantString: "https://host"},
{name: "dot path collapses to empty", raw: "https://host/.", defaultScheme: "", wantScheme: "https", wantPath: "", wantString: "https://host"},
{name: "normal subpath kept", raw: "https://host/a/b", defaultScheme: "", wantScheme: "https", wantPath: "/a/b", wantString: "https://host/a/b"},
{name: "host local backslash path rejected", raw: "https://host/\\evil", defaultScheme: "", wantErr: "base URL path must be host-local"},
{name: "userinfo rejected via delegation", raw: "https://u@host", defaultScheme: "", wantErr: "URL userinfo is not allowed"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NormalizeHTTPBaseURL(tt.raw, tt.defaultScheme)
if tt.wantErr != "" {
if err == nil {
t.Fatalf("NormalizeHTTPBaseURL(%q,%q) = %v, want error containing %q", tt.raw, tt.defaultScheme, got, tt.wantErr)
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("NormalizeHTTPBaseURL(%q,%q) err = %q, want substring %q", tt.raw, tt.defaultScheme, err.Error(), tt.wantErr)
}
return
}
if err != nil {
t.Fatalf("NormalizeHTTPBaseURL(%q,%q) unexpected error: %v", tt.raw, tt.defaultScheme, err)
}
if got.Scheme != tt.wantScheme {
t.Fatalf("Scheme = %q, want %q", got.Scheme, tt.wantScheme)
}
if got.Path != tt.wantPath {
t.Fatalf("Path = %q, want %q", got.Path, tt.wantPath)
}
if got.String() != tt.wantString {
t.Fatalf("String() = %q, want %q", got.String(), tt.wantString)
}
// RawPath is always cleared by NormalizeHTTPBaseURL.
if got.RawPath != "" {
t.Fatalf("RawPath = %q, want empty", got.RawPath)
}
})
}
}
func TestBranchCovNormalizeLocalRedirectPath(t *testing.T) {
tests := []struct {
name string
raw string
want string
wantErr string
}{
{name: "empty required", raw: " ", wantErr: "redirect path is required"},
{name: "query only path empty", raw: "?foo=bar", wantErr: "redirect must be a local absolute path"},
{name: "fragment only path empty", raw: "#frag", wantErr: "redirect must be a local absolute path"},
{name: "del control char 0x7f", raw: "/x\x7f", wantErr: "redirect path contains control characters"},
{name: "low control char 0x1f", raw: "/x\x1f", wantErr: "redirect path contains control characters"},
{name: "parse error invalid escape", raw: "/%zz", wantErr: "invalid redirect path"},
{name: "backslash in path rejected", raw: "/a\\b", wantErr: "redirect path must not contain backslashes"},
{name: "success preserves query", raw: "/dashboard?next=/home", want: "/dashboard?next=/home"},
{name: "success simple path", raw: "/settings/profile", want: "/settings/profile"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NormalizeLocalRedirectPath(tt.raw)
if tt.wantErr != "" {
if err == nil {
t.Fatalf("NormalizeLocalRedirectPath(%q) = %q, want error containing %q", tt.raw, got, tt.wantErr)
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("NormalizeLocalRedirectPath(%q) err = %q, want substring %q", tt.raw, err.Error(), tt.wantErr)
}
return
}
if err != nil {
t.Fatalf("NormalizeLocalRedirectPath(%q) unexpected error: %v", tt.raw, err)
}
if got != tt.want {
t.Fatalf("NormalizeLocalRedirectPath(%q) = %q, want %q", tt.raw, got, tt.want)
}
})
}
}
func TestBranchCovNormalizePulseHTTPBaseURL(t *testing.T) {
tests := []struct {
name string
raw string
wantErr string
wantStr string
}{
{name: "empty required", raw: " ", wantErr: "Pulse URL is required"},
{name: "parse error invalid escape", raw: "https://host/%zz", wantErr: "is invalid"},
{name: "missing scheme non websocket message", raw: "example.com", wantErr: "must include scheme (https:// or loopback http://)"},
{name: "host empty", raw: "https://", wantErr: "must include host"},
{name: "hostname empty port only", raw: "https://:443", wantErr: "must include host"},
{name: "user credentials rejected", raw: "https://u@host", wantErr: "must not include user credentials"},
{name: "query rejected", raw: "https://host/?q=1", wantErr: "must not include query or fragment"},
{name: "fragment rejected", raw: "https://host/#f", wantErr: "must not include query or fragment"},
{name: "port zero invalid", raw: "https://host:0", wantErr: "invalid port"},
{name: "port too high invalid", raw: "https://host:99999", wantErr: "invalid port"},
{name: "wss scheme unsupported for http channel", raw: "wss://host", wantErr: "unsupported scheme"},
{name: "ws scheme unsupported for http channel", raw: "ws://localhost", wantErr: "unsupported scheme"},
{name: "ftp unsupported scheme", raw: "ftp://host", wantErr: "unsupported scheme"},
{name: "http non loopback rejected", raw: "http://example.com", wantErr: "must use https unless host is loopback"},
{name: "https success normalizes host and trims path", raw: "https://API.Example.COM/Pulse/", wantStr: "https://api.example.com/Pulse"},
{name: "http loopback allowed", raw: "http://LocalHost:8080/", wantStr: "http://localhost:8080"},
{name: "http ipv4 loopback allowed", raw: "http://127.0.0.1", wantStr: "http://127.0.0.1"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NormalizePulseHTTPBaseURL(tt.raw)
if tt.wantErr != "" {
if err == nil {
t.Fatalf("NormalizePulseHTTPBaseURL(%q) = %v, want error containing %q", tt.raw, got, tt.wantErr)
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("NormalizePulseHTTPBaseURL(%q) err = %q, want substring %q", tt.raw, err.Error(), tt.wantErr)
}
return
}
if err != nil {
t.Fatalf("NormalizePulseHTTPBaseURL(%q) unexpected error: %v", tt.raw, err)
}
if got.String() != tt.wantStr {
t.Fatalf("NormalizePulseHTTPBaseURL(%q) = %q, want %q", tt.raw, got.String(), tt.wantStr)
}
})
}
}
func TestBranchCovNormalizeSecureHTTPBaseURL(t *testing.T) {
tests := []struct {
name string
raw string
wantErr string
wantStr string
}{
{name: "empty required", raw: " ", wantErr: "base URL is required"},
{name: "https normalizes host and trims trailing slash", raw: "https://API.Example.COM/Pulse/", wantStr: "https://api.example.com/Pulse"},
{name: "http loopback allowed", raw: "http://127.0.0.1/path/", wantStr: "http://127.0.0.1/path"},
{name: "http localhost allowed", raw: "http://LocalHost", wantStr: "http://localhost"},
{name: "http non loopback rejected", raw: "http://example.com", wantErr: "must use https unless host is loopback"},
{name: "delegates bad scheme", raw: "ftp://host", wantErr: "scheme must be http or https"},
{name: "clears query and fragment on https", raw: "https://host/path", wantStr: "https://host/path"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NormalizeSecureHTTPBaseURL(tt.raw)
if tt.wantErr != "" {
if err == nil {
t.Fatalf("NormalizeSecureHTTPBaseURL(%q) = %v, want error containing %q", tt.raw, got, tt.wantErr)
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("NormalizeSecureHTTPBaseURL(%q) err = %q, want substring %q", tt.raw, err.Error(), tt.wantErr)
}
return
}
if err != nil {
t.Fatalf("NormalizeSecureHTTPBaseURL(%q) unexpected error: %v", tt.raw, err)
}
if got.String() != tt.wantStr {
t.Fatalf("NormalizeSecureHTTPBaseURL(%q) = %q, want %q", tt.raw, got.String(), tt.wantStr)
}
// NormalizeSecureHTTPBaseURL must clear query and fragment.
if got.RawQuery != "" || got.Fragment != "" {
t.Fatalf("expected cleared query/fragment, got RawQuery=%q Fragment=%q", got.RawQuery, got.Fragment)
}
})
}
}
func TestBranchCovNormalizePulseWebSocketBaseURL(t *testing.T) {
tests := []struct {
name string
raw string
wantErr string
wantStr string
}{
{name: "empty required", raw: " ", wantErr: "Pulse URL is required"},
{name: "missing scheme websocket message", raw: "example.com", wantErr: "must include scheme (https://, wss://, or loopback http:// / ws://)"},
{name: "https upgraded to wss", raw: "https://LocalHost/", wantStr: "wss://localhost"},
{name: "wss success normalizes host", raw: "wss://API.Example.COM/Pulse/", wantStr: "wss://api.example.com/Pulse"},
{name: "http loopback downgraded to ws", raw: "http://127.0.0.1/", wantStr: "ws://127.0.0.1"},
{name: "ws loopback kept", raw: "ws://LocalHost", wantStr: "ws://localhost"},
{name: "http non loopback rejected websocket message", raw: "http://example.com", wantErr: "must use https/wss unless host is loopback"},
{name: "ws non loopback rejected", raw: "ws://example.com", wantErr: "must use https/wss unless host is loopback"},
{name: "unsupported scheme", raw: "ftp://host", wantErr: "unsupported scheme"},
{name: "invalid port high", raw: "wss://host:99999", wantErr: "invalid port"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NormalizePulseWebSocketBaseURL(tt.raw)
if tt.wantErr != "" {
if err == nil {
t.Fatalf("NormalizePulseWebSocketBaseURL(%q) = %v, want error containing %q", tt.raw, got, tt.wantErr)
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("NormalizePulseWebSocketBaseURL(%q) err = %q, want substring %q", tt.raw, err.Error(), tt.wantErr)
}
return
}
if err != nil {
t.Fatalf("NormalizePulseWebSocketBaseURL(%q) unexpected error: %v", tt.raw, err)
}
if got.String() != tt.wantStr {
t.Fatalf("NormalizePulseWebSocketBaseURL(%q) = %q, want %q", tt.raw, got.String(), tt.wantStr)
}
})
}
}
func TestBranchCovAppendURLPath(t *testing.T) {
t.Run("nil base returns nil", func(t *testing.T) {
if got := AppendURLPath(nil, "x"); got != nil {
t.Fatalf("AppendURLPath(nil) = %v, want nil", got)
}
})
t.Run("empty and slash only segments skipped", func(t *testing.T) {
base := mustParseURL(t, "https://host/api")
got := AppendURLPath(base, "", "/", "//", "///")
// All segments trim (via strings.Trim of "/") to empty and are skipped; path unchanged.
if got.Path != "/api" {
t.Fatalf("Path = %q, want %q", got.Path, "/api")
}
})
t.Run("appends multiple segments", func(t *testing.T) {
base := mustParseURL(t, "https://host/api")
got := AppendURLPath(base, "v1", "/users/", "/42")
if got.String() != "https://host/api/v1/users/42" {
t.Fatalf("String() = %q", got.String())
}
})
t.Run("segments with parent refs collapse", func(t *testing.T) {
base := mustParseURL(t, "https://host/a/b")
got := AppendURLPath(base, "../c")
// path.Join resolves .. -> /a/c
if got.Path != "/a/c" {
t.Fatalf("Path = %q, want %q", got.Path, "/a/c")
}
})
t.Run("base with empty path gets leading slash", func(t *testing.T) {
base := mustParseURL(t, "https://host")
got := AppendURLPath(base, "x")
// joined "x" has no leading slash -> normalized to "/x"
if got.Path != "/x" {
t.Fatalf("Path = %q, want %q", got.Path, "/x")
}
if got.String() != "https://host/x" {
t.Fatalf("String() = %q", got.String())
}
})
t.Run("no segments and root path collapses to empty", func(t *testing.T) {
base := mustParseURL(t, "https://host/")
got := AppendURLPath(base)
if got.Path != "" {
t.Fatalf("Path = %q, want empty", got.Path)
}
})
t.Run("rawpath and fragment cleared but base query preserved", func(t *testing.T) {
base := mustParseURL(t, "https://host/api?keep=1#/frag")
got := AppendURLPath(base, "users")
// AppendURLPath clears RawPath and Fragment but does NOT clear RawQuery.
if got.RawPath != "" {
t.Fatalf("RawPath = %q, want empty", got.RawPath)
}
if got.Fragment != "" {
t.Fatalf("Fragment = %q, want empty", got.Fragment)
}
if got.RawQuery != "keep=1" {
t.Fatalf("RawQuery = %q, want %q (base query is preserved)", got.RawQuery, "keep=1")
}
})
t.Run("base is not mutated", func(t *testing.T) {
base := mustParseURL(t, "https://host/api")
_ = AppendURLPath(base, "x", "y")
if base.Path != "/api" {
t.Fatalf("base.Path mutated to %q", base.Path)
}
if base.RawPath != "" {
t.Fatalf("base.RawPath mutated to %q", base.RawPath)
}
})
}
func TestBranchCovResolveRelativeURL(t *testing.T) {
t.Run("nil base errors", func(t *testing.T) {
_, err := ResolveRelativeURL(nil, "/x")
if err == nil || !strings.Contains(err.Error(), "base URL is required") {
t.Fatalf("err = %v, want base URL is required", err)
}
})
t.Run("empty relative after trim errors", func(t *testing.T) {
base := mustParseURL(t, "https://host/api")
_, err := ResolveRelativeURL(base, " ")
if err == nil || !strings.Contains(err.Error(), "relative path is required") {
t.Fatalf("err = %v, want relative path is required", err)
}
})
t.Run("backslash rejected", func(t *testing.T) {
base := mustParseURL(t, "https://host/api")
_, err := ResolveRelativeURL(base, `/a\b`)
if err == nil || !strings.Contains(err.Error(), "must not contain backslashes") {
t.Fatalf("err = %v, want backslash rejection", err)
}
})
t.Run("parse error invalid escape", func(t *testing.T) {
base := mustParseURL(t, "https://host/api")
_, err := ResolveRelativeURL(base, "/%zz")
if err == nil || !strings.Contains(err.Error(), "invalid relative path") {
t.Fatalf("err = %v, want invalid relative path", err)
}
})
t.Run("absolute url rejected", func(t *testing.T) {
base := mustParseURL(t, "https://host/api")
_, err := ResolveRelativeURL(base, "https://evil/x")
if err == nil || !strings.Contains(err.Error(), "must not include scheme or host") {
t.Fatalf("err = %v, want scheme/host rejection", err)
}
})
t.Run("scheme relative host rejected", func(t *testing.T) {
base := mustParseURL(t, "https://host/api")
_, err := ResolveRelativeURL(base, "//evil/x")
if err == nil || !strings.Contains(err.Error(), "must not include scheme or host") {
t.Fatalf("err = %v, want host rejection", err)
}
})
t.Run("relative without leading slash rejected", func(t *testing.T) {
base := mustParseURL(t, "https://host/api")
_, err := ResolveRelativeURL(base, "users")
if err == nil || !strings.Contains(err.Error(), "must start with '/'") {
t.Fatalf("err = %v, want leading slash rejection", err)
}
})
t.Run("success joins and clears fragment", func(t *testing.T) {
base := mustParseURL(t, "https://host/api")
got, err := ResolveRelativeURL(base, "/users")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got.String() != "https://host/api/users" {
t.Fatalf("String() = %q", got.String())
}
// No encoding in path -> RawPath collapsed to empty.
if got.RawPath != "" {
t.Fatalf("RawPath = %q, want empty", got.RawPath)
}
if got.Fragment != "" {
t.Fatalf("Fragment = %q, want empty", got.Fragment)
}
if got.RawQuery != "" {
t.Fatalf("RawQuery = %q, want empty", got.RawQuery)
}
})
t.Run("success propagates relative query", func(t *testing.T) {
base := mustParseURL(t, "https://host/api")
got, err := ResolveRelativeURL(base, "/users?active=1")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got.RawQuery != "active=1" {
t.Fatalf("RawQuery = %q, want %q", got.RawQuery, "active=1")
}
})
t.Run("encoded path retains rawpath", func(t *testing.T) {
base := mustParseURL(t, "https://host/api")
got, err := ResolveRelativeURL(base, "/us%20ers")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got.Path != "/api/us ers" {
t.Fatalf("Path = %q, want %q", got.Path, "/api/us ers")
}
if got.RawPath != "/api/us%20ers" {
t.Fatalf("RawPath = %q, want %q (retained because it differs from Path)", got.RawPath, "/api/us%20ers")
}
})
t.Run("base is not mutated", func(t *testing.T) {
base := mustParseURL(t, "https://host/api")
_, _ = ResolveRelativeURL(base, "/users")
if base.Path != "/api" {
t.Fatalf("base.Path mutated to %q", base.Path)
}
})
}