test: Add error path tests for renderWebhookURL and UpdateAllowedPrivateCIDRs

Add comprehensive error handling tests for two pure functions:

renderWebhookURL (8 new test cases):
- Empty/whitespace URL template validation
- Invalid template syntax (unclosed braces, undefined functions)
- Template producing empty URL
- Missing scheme or host in rendered URL

UpdateAllowedPrivateCIDRs (expanded from 8 to 29 cases):
- Invalid IP addresses (garbage, out of range, malformed)
- Invalid CIDR notation (prefix too large, negative, non-numeric)
- Malformed strings (double slash, invalid IP with valid prefix)
- Success cases for valid IPv4/IPv6 CIDRs and bare IPs
This commit is contained in:
rcourtman 2025-12-01 12:58:15 +00:00
parent d548287105
commit 2d75350dfa
2 changed files with 249 additions and 15 deletions

View file

@ -507,6 +507,122 @@ func TestRenderWebhookURL_InvalidTemplate(t *testing.T) {
}
}
func TestRenderWebhookURL_ErrorPaths(t *testing.T) {
tests := []struct {
name string
urlTemplate string
data WebhookPayloadData
wantErr string
}{
{
name: "empty URL template",
urlTemplate: "",
data: WebhookPayloadData{},
wantErr: "webhook URL cannot be empty",
},
{
name: "whitespace-only URL template",
urlTemplate: " \t\n ",
data: WebhookPayloadData{},
wantErr: "webhook URL cannot be empty",
},
{
name: "invalid template syntax",
urlTemplate: "https://example.com/{{.Unclosed",
data: WebhookPayloadData{},
wantErr: "invalid webhook URL template",
},
{
name: "template execution error - undefined function",
urlTemplate: "https://example.com/{{undefined_func .Message}}",
data: WebhookPayloadData{Message: "test"},
wantErr: "invalid webhook URL template",
},
{
name: "template produces empty URL",
urlTemplate: "{{if false}}https://example.com{{end}}",
data: WebhookPayloadData{},
wantErr: "webhook URL template produced empty URL",
},
{
name: "template renders to missing scheme",
urlTemplate: "{{.Message}}/path",
data: WebhookPayloadData{Message: "example.com"},
wantErr: "missing scheme or host",
},
{
name: "template renders to missing host",
urlTemplate: "{{.Message}}://",
data: WebhookPayloadData{Message: "https"},
wantErr: "missing scheme or host",
},
{
name: "template renders to relative path",
urlTemplate: "/{{.Message}}/webhook",
data: WebhookPayloadData{Message: "api"},
wantErr: "missing scheme or host",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := renderWebhookURL(tt.urlTemplate, tt.data)
if err == nil {
t.Fatalf("expected error containing %q, got nil (result: %q)", tt.wantErr, result)
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("expected error containing %q, got: %v", tt.wantErr, err)
}
})
}
}
func TestRenderWebhookURL_SuccessCases(t *testing.T) {
tests := []struct {
name string
urlTemplate string
data WebhookPayloadData
want string
}{
{
name: "static URL - no template",
urlTemplate: "https://example.com/webhook",
data: WebhookPayloadData{},
want: "https://example.com/webhook",
},
{
name: "URL with whitespace trimmed",
urlTemplate: " https://example.com/webhook ",
data: WebhookPayloadData{},
want: "https://example.com/webhook",
},
{
name: "URL with template variable in path",
urlTemplate: "https://example.com/{{.ResourceType}}/alert",
data: WebhookPayloadData{ResourceType: "vm"},
want: "https://example.com/vm/alert",
},
{
name: "URL with urlquery encoding",
urlTemplate: "https://example.com?msg={{urlquery .Message}}",
data: WebhookPayloadData{Message: "hello world"},
want: "https://example.com?msg=hello+world",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := renderWebhookURL(tt.urlTemplate, tt.data)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result != tt.want {
t.Fatalf("expected %q, got %q", tt.want, result)
}
})
}
}
func TestSendTestNotificationApprise(t *testing.T) {
nm := NewNotificationManager("")
nm.SetEmailConfig(EmailConfig{Enabled: false})

View file

@ -2,64 +2,182 @@ package notifications
import (
"net"
"strings"
"testing"
)
func TestUpdateAllowedPrivateCIDRs(t *testing.T) {
nm := NewNotificationManager("")
tests := []struct {
name string
cidrs string
wantErr bool
wantErr string // empty string means no error expected
}{
// Success cases
{
name: "empty string clears allowlist",
cidrs: "",
wantErr: false,
wantErr: "",
},
{
name: "single valid CIDR",
cidrs: "192.168.1.0/24",
wantErr: false,
wantErr: "",
},
{
name: "multiple valid CIDRs",
cidrs: "192.168.1.0/24,10.0.0.0/8",
wantErr: false,
wantErr: "",
},
{
name: "CIDR with spaces",
cidrs: "192.168.1.0/24, 10.0.0.0/8",
wantErr: false,
wantErr: "",
},
{
name: "bare IPv4 address",
cidrs: "192.168.1.1",
wantErr: false,
wantErr: "",
},
{
name: "bare IPv6 address",
cidrs: "fe80::1",
wantErr: false,
wantErr: "",
},
{
name: "invalid CIDR",
name: "valid IPv6 CIDR",
cidrs: "fe80::/10",
wantErr: "",
},
{
name: "loopback IPv6",
cidrs: "::1",
wantErr: "",
},
{
name: "multiple valid CIDRs with mixed IP versions",
cidrs: "192.168.1.0/24, 10.0.0.0/8, fe80::/10",
wantErr: "",
},
{
name: "mixed bare IPs and CIDRs",
cidrs: "192.168.1.1, 10.0.0.0/8",
wantErr: "",
},
{
name: "whitespace handling",
cidrs: " 192.168.1.0/24 , 10.0.0.1 ",
wantErr: "",
},
{
name: "empty entries skipped",
cidrs: "192.168.1.0/24,,10.0.0.1",
wantErr: "",
},
// Error cases - invalid IP addresses (bare IPs without CIDR notation)
{
name: "invalid IP address - garbage text",
cidrs: "not-a-cidr",
wantErr: true,
wantErr: "invalid IP address: not-a-cidr",
},
{
name: "invalid IP address",
name: "invalid IP address - out of range octets",
cidrs: "999.999.999.999",
wantErr: true,
wantErr: "invalid IP address: 999.999.999.999",
},
{
name: "invalid IP address in list",
cidrs: "192.168.1.0/24, invalid, 10.0.0.1",
wantErr: "invalid IP address: invalid",
},
{
name: "IP with too many octets",
cidrs: "192.168.1.1.1",
wantErr: "invalid IP address: 192.168.1.1.1",
},
{
name: "IP with negative octet",
cidrs: "192.168.-1.0",
wantErr: "invalid IP address: 192.168.-1.0",
},
{
name: "IP with octet out of range",
cidrs: "192.168.256.0",
wantErr: "invalid IP address: 192.168.256.0",
},
// Error cases - invalid CIDR notation
{
name: "CIDR prefix too large for IPv4",
cidrs: "192.168.1.0/33",
wantErr: "invalid CIDR range 192.168.1.0/33",
},
{
name: "CIDR prefix too large for IPv6",
cidrs: "fe80::/129",
wantErr: "invalid CIDR range fe80::/129",
},
{
name: "CIDR prefix way too large",
cidrs: "192.168.1.0/999",
wantErr: "invalid CIDR range 192.168.1.0/999",
},
{
name: "CIDR with negative prefix",
cidrs: "192.168.1.0/-1",
wantErr: "invalid CIDR range 192.168.1.0/-1",
},
{
name: "CIDR with non-numeric prefix",
cidrs: "192.168.1.0/abc",
wantErr: "invalid CIDR range 192.168.1.0/abc",
},
{
name: "CIDR with empty prefix",
cidrs: "192.168.1.0/",
wantErr: "invalid CIDR range 192.168.1.0/",
},
{
name: "CIDR with floating point prefix",
cidrs: "192.168.1.0/24.5",
wantErr: "invalid CIDR range 192.168.1.0/24.5",
},
// Error cases - malformed strings
{
name: "double slash in CIDR",
cidrs: "192.168.1.0//24",
wantErr: "invalid CIDR range 192.168.1.0//24",
},
{
name: "CIDR with invalid IP part",
cidrs: "999.999.999.999/24",
wantErr: "invalid CIDR range 999.999.999.999/24",
},
{
name: "valid CIDR followed by invalid CIDR",
cidrs: "192.168.1.0/24, bad/cidr",
wantErr: "invalid CIDR range bad/cidr",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
nm := NewNotificationManager("")
defer nm.Stop()
err := nm.UpdateAllowedPrivateCIDRs(tt.cidrs)
if (err != nil) != tt.wantErr {
t.Errorf("UpdateAllowedPrivateCIDRs() error = %v, wantErr %v", err, tt.wantErr)
if tt.wantErr == "" {
if err != nil {
t.Errorf("UpdateAllowedPrivateCIDRs() unexpected error = %v", err)
}
} else {
if err == nil {
t.Errorf("UpdateAllowedPrivateCIDRs() expected error containing %q, got nil", tt.wantErr)
} else if !strings.Contains(err.Error(), tt.wantErr) {
t.Errorf("UpdateAllowedPrivateCIDRs() error = %v, want error containing %q", err, tt.wantErr)
}
}
})
}