package api import ( "strings" "testing" "github.com/alexei/tinyforge/internal/store" ) func TestValidateWebhookURL(t *testing.T) { cases := []struct { name string url string wantErr string // substring; empty = pass }{ {"https valid", "https://example.com/hook", ""}, {"http valid", "http://example.com:8080/hook", ""}, {"RFC1918 private LAN allowed", "http://192.168.1.50:9090/hook", ""}, {"loopback rejected", "http://127.0.0.1:8090/hook", "loopback"}, {"ipv6 loopback rejected", "http://[::1]:9000/hook", "loopback"}, {"link-local rejected", "http://169.254.169.254/latest/meta-data", "reserved"}, {"unspecified rejected", "http://0.0.0.0:9000/hook", "reserved"}, {"file scheme rejected", "file:///etc/passwd", "http:// or https://"}, {"missing host rejected", "https://", "missing host"}, {"malformed url rejected", "://nope", "invalid URL"}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { got := validateWebhookURL(c.url) if c.wantErr == "" { if got != "" { t.Fatalf("expected pass, got error: %q", got) } return } if !strings.Contains(got, c.wantErr) { t.Fatalf("error mismatch:\n got: %q\n want substring: %q", got, c.wantErr) } }) } } func TestValidateTrigger(t *testing.T) { cases := []struct { name string in store.EventTrigger want string // substring of error; empty = pass }{ { name: "missing name", in: store.EventTrigger{ActionTarget: "https://x.example.com/h"}, want: "name is required", }, { name: "missing target", in: store.EventTrigger{Name: "n"}, want: "action_target is required", }, { name: "bad scheme", in: store.EventTrigger{Name: "n", ActionTarget: "ftp://x.example.com/h"}, want: "http:// or https://", }, { name: "loopback target", in: store.EventTrigger{Name: "n", ActionTarget: "http://127.0.0.1/hook"}, want: "loopback", }, { name: "unsupported action_type", in: store.EventTrigger{Name: "n", ActionType: "email", ActionTarget: "https://x.example.com/h"}, want: "action_type must be", }, { name: "invalid regex", in: store.EventTrigger{ Name: "n", ActionTarget: "https://x.example.com/h", FilterMessageRegex: "([unclosed", }, want: "filter_message_regex invalid", }, { name: "all valid", in: store.EventTrigger{ Name: "n", ActionTarget: "https://x.example.com/h", FilterSeverity: "warn,error", FilterMessageRegex: `\bpanic\b`, }, want: "", }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { got := validateTrigger(c.in) if c.want == "" { if got != "" { t.Fatalf("expected pass, got error: %q", got) } return } if !strings.Contains(got, c.want) { t.Fatalf("error mismatch:\n got: %q\n want substring: %q", got, c.want) } }) } } func TestRedactTriggerSecret(t *testing.T) { withSecret := store.EventTrigger{Name: "n", ActionSecret: "shh-real-secret"} got := redactTriggerSecret(withSecret) if got.ActionSecret != actionSecretPlaceholder { t.Errorf("expected placeholder, got %q", got.ActionSecret) } if withSecret.ActionSecret != "shh-real-secret" { t.Errorf("original mutated: %q", withSecret.ActionSecret) } noSecret := store.EventTrigger{Name: "n", ActionSecret: ""} got2 := redactTriggerSecret(noSecret) if got2.ActionSecret != "" { t.Errorf("empty secret should stay empty, got %q", got2.ActionSecret) } } func TestDerefString(t *testing.T) { if derefString(nil) != "" { t.Error("nil should deref to empty string") } s := "value" if derefString(&s) != "value" { t.Error("non-nil should deref to value") } } func TestFirstNonEmpty(t *testing.T) { if firstNonEmpty("a", "b") != "a" { t.Error("non-empty first wins") } if firstNonEmpty("", "b") != "b" { t.Error("fallback when first empty") } }