mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 11:30:15 +00:00
924 lines
20 KiB
Go
924 lines
20 KiB
Go
package notifications
|
|
|
|
import (
|
|
"net"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestFormatWebhookDuration(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
duration time.Duration
|
|
expected string
|
|
}{
|
|
// Seconds range (< 1 minute)
|
|
{
|
|
name: "zero duration",
|
|
duration: 0,
|
|
expected: "0s",
|
|
},
|
|
{
|
|
name: "one second",
|
|
duration: time.Second,
|
|
expected: "1s",
|
|
},
|
|
{
|
|
name: "30 seconds",
|
|
duration: 30 * time.Second,
|
|
expected: "30s",
|
|
},
|
|
{
|
|
name: "59 seconds",
|
|
duration: 59 * time.Second,
|
|
expected: "59s",
|
|
},
|
|
{
|
|
name: "sub-second rounds down to 0s",
|
|
duration: 500 * time.Millisecond,
|
|
expected: "0s",
|
|
},
|
|
// Minutes range (>= 1 minute, < 1 hour)
|
|
{
|
|
name: "one minute exactly",
|
|
duration: time.Minute,
|
|
expected: "1m",
|
|
},
|
|
{
|
|
name: "90 seconds shows as 1m",
|
|
duration: 90 * time.Second,
|
|
expected: "1m",
|
|
},
|
|
{
|
|
name: "30 minutes",
|
|
duration: 30 * time.Minute,
|
|
expected: "30m",
|
|
},
|
|
{
|
|
name: "59 minutes",
|
|
duration: 59 * time.Minute,
|
|
expected: "59m",
|
|
},
|
|
// Hours range (>= 1 hour, < 24 hours)
|
|
{
|
|
name: "one hour exactly",
|
|
duration: time.Hour,
|
|
expected: "1h 0m",
|
|
},
|
|
{
|
|
name: "1 hour 30 minutes",
|
|
duration: 90 * time.Minute,
|
|
expected: "1h 30m",
|
|
},
|
|
{
|
|
name: "12 hours",
|
|
duration: 12 * time.Hour,
|
|
expected: "12h 0m",
|
|
},
|
|
{
|
|
name: "23 hours 59 minutes",
|
|
duration: 23*time.Hour + 59*time.Minute,
|
|
expected: "23h 59m",
|
|
},
|
|
// Days range (>= 24 hours)
|
|
{
|
|
name: "one day exactly",
|
|
duration: 24 * time.Hour,
|
|
expected: "1d 0h",
|
|
},
|
|
{
|
|
name: "1.5 days",
|
|
duration: 36 * time.Hour,
|
|
expected: "1d 12h",
|
|
},
|
|
{
|
|
name: "7 days",
|
|
duration: 7 * 24 * time.Hour,
|
|
expected: "7d 0h",
|
|
},
|
|
{
|
|
name: "30 days",
|
|
duration: 30 * 24 * time.Hour,
|
|
expected: "30d 0h",
|
|
},
|
|
{
|
|
name: "30 days 23 hours",
|
|
duration: 30*24*time.Hour + 23*time.Hour,
|
|
expected: "30d 23h",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := formatWebhookDuration(tt.duration)
|
|
if result != tt.expected {
|
|
t.Errorf("formatWebhookDuration(%v) = %q, want %q", tt.duration, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsNumeric(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected bool
|
|
}{
|
|
// Valid numeric strings
|
|
{
|
|
name: "single digit",
|
|
input: "0",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "positive integer",
|
|
input: "12345",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "large number",
|
|
input: "9876543210",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "all zeros",
|
|
input: "0000",
|
|
expected: true,
|
|
},
|
|
// Invalid strings
|
|
{
|
|
name: "empty string",
|
|
input: "",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "negative sign",
|
|
input: "-123",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "decimal point",
|
|
input: "12.34",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "letters",
|
|
input: "abc",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "mixed alphanumeric",
|
|
input: "123abc",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "spaces",
|
|
input: "123 456",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "leading space",
|
|
input: " 123",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "trailing space",
|
|
input: "123 ",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "plus sign",
|
|
input: "+123",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "underscore",
|
|
input: "123_456",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "comma separator",
|
|
input: "1,234",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "only whitespace",
|
|
input: " ",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "unicode digits",
|
|
input: "١٢٣", // Arabic-Indic digits
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := isNumeric(tt.input)
|
|
if result != tt.expected {
|
|
t.Errorf("isNumeric(%q) = %v, want %v", tt.input, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExtractTelegramChatID(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
webhookURL string
|
|
expected string
|
|
expectError bool
|
|
errorSubstr string
|
|
}{
|
|
// Valid chat IDs
|
|
{
|
|
name: "valid positive chat ID",
|
|
webhookURL: "https://api.telegram.org/bot123456:ABC/sendMessage?chat_id=12345",
|
|
expected: "12345",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "valid negative chat ID (group chat)",
|
|
webhookURL: "https://api.telegram.org/bot123456:ABC/sendMessage?chat_id=-987654321",
|
|
expected: "-987654321",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "chat_id with other params",
|
|
webhookURL: "https://api.telegram.org/bot123456:ABC/sendMessage?parse_mode=HTML&chat_id=12345&disable_notification=true",
|
|
expected: "12345",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "large chat ID",
|
|
webhookURL: "https://api.telegram.org/bot123456:ABC/sendMessage?chat_id=1234567890123",
|
|
expected: "1234567890123",
|
|
expectError: false,
|
|
},
|
|
// Missing chat_id parameter
|
|
{
|
|
name: "missing chat_id parameter",
|
|
webhookURL: "https://api.telegram.org/bot123456:ABC/sendMessage",
|
|
expectError: true,
|
|
errorSubstr: "missing chat_id parameter",
|
|
},
|
|
{
|
|
name: "empty chat_id value",
|
|
webhookURL: "https://api.telegram.org/bot123456:ABC/sendMessage?chat_id=",
|
|
expectError: true,
|
|
errorSubstr: "chat_id parameter is empty",
|
|
},
|
|
{
|
|
name: "chat_id with no value - detected as missing",
|
|
webhookURL: "https://api.telegram.org/bot123456:ABC/sendMessage?chat_id",
|
|
expectError: true,
|
|
errorSubstr: "missing chat_id parameter", // "chat_id=" not present
|
|
},
|
|
// Invalid chat_id values
|
|
{
|
|
name: "non-numeric chat_id",
|
|
webhookURL: "https://api.telegram.org/bot123456:ABC/sendMessage?chat_id=abc123",
|
|
expectError: true,
|
|
errorSubstr: "chat_id must be numeric",
|
|
},
|
|
{
|
|
name: "chat_id with spaces",
|
|
webhookURL: "https://api.telegram.org/bot123456:ABC/sendMessage?chat_id=123%20456",
|
|
expectError: true,
|
|
errorSubstr: "chat_id must be numeric",
|
|
},
|
|
{
|
|
name: "chat_id with special chars",
|
|
webhookURL: "https://api.telegram.org/bot123456:ABC/sendMessage?chat_id=@channel",
|
|
expectError: true,
|
|
errorSubstr: "chat_id must be numeric",
|
|
},
|
|
{
|
|
name: "negative with non-numeric suffix",
|
|
webhookURL: "https://api.telegram.org/bot123456:ABC/sendMessage?chat_id=-abc",
|
|
expectError: true,
|
|
errorSubstr: "chat_id must be numeric",
|
|
},
|
|
{
|
|
name: "just a minus sign",
|
|
webhookURL: "https://api.telegram.org/bot123456:ABC/sendMessage?chat_id=-",
|
|
expectError: true,
|
|
errorSubstr: "chat_id must be numeric",
|
|
},
|
|
// Invalid URLs
|
|
{
|
|
name: "invalid URL format",
|
|
webhookURL: "not-a-url",
|
|
expectError: true,
|
|
errorSubstr: "missing chat_id parameter",
|
|
},
|
|
{
|
|
name: "empty URL",
|
|
webhookURL: "",
|
|
expectError: true,
|
|
errorSubstr: "missing chat_id parameter",
|
|
},
|
|
{
|
|
name: "URL with control characters",
|
|
webhookURL: "https://api.telegram.org/bot\x00/sendMessage?chat_id=12345",
|
|
expectError: true,
|
|
errorSubstr: "invalid URL format",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, err := extractTelegramChatID(tt.webhookURL)
|
|
|
|
if tt.expectError {
|
|
if err == nil {
|
|
t.Errorf("extractTelegramChatID(%q) expected error containing %q, got nil",
|
|
tt.webhookURL, tt.errorSubstr)
|
|
} else if tt.errorSubstr != "" && !contains(err.Error(), tt.errorSubstr) {
|
|
t.Errorf("extractTelegramChatID(%q) error = %q, want error containing %q",
|
|
tt.webhookURL, err.Error(), tt.errorSubstr)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("extractTelegramChatID(%q) unexpected error: %v", tt.webhookURL, err)
|
|
}
|
|
if result != tt.expected {
|
|
t.Errorf("extractTelegramChatID(%q) = %q, want %q", tt.webhookURL, result, tt.expected)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsPrivateIP(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ip string
|
|
expected bool
|
|
}{
|
|
// RFC1918 10.0.0.0/8
|
|
{
|
|
name: "10.0.0.0 private",
|
|
ip: "10.0.0.0",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "10.0.0.1 private",
|
|
ip: "10.0.0.1",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "10.255.255.255 private",
|
|
ip: "10.255.255.255",
|
|
expected: true,
|
|
},
|
|
// RFC1918 172.16.0.0/12
|
|
{
|
|
name: "172.16.0.1 private",
|
|
ip: "172.16.0.1",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "172.31.255.255 private",
|
|
ip: "172.31.255.255",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "172.15.0.1 public (just below range)",
|
|
ip: "172.15.0.1",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "172.32.0.1 public (just above range)",
|
|
ip: "172.32.0.1",
|
|
expected: false,
|
|
},
|
|
// RFC1918 192.168.0.0/16
|
|
{
|
|
name: "192.168.0.1 private",
|
|
ip: "192.168.0.1",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "192.168.255.255 private",
|
|
ip: "192.168.255.255",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "192.167.0.1 public (just below range)",
|
|
ip: "192.167.0.1",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "192.169.0.1 public (just above range)",
|
|
ip: "192.169.0.1",
|
|
expected: false,
|
|
},
|
|
// Loopback 127.0.0.0/8
|
|
{
|
|
name: "127.0.0.1 loopback",
|
|
ip: "127.0.0.1",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "127.0.0.0 loopback",
|
|
ip: "127.0.0.0",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "127.255.255.255 loopback",
|
|
ip: "127.255.255.255",
|
|
expected: true,
|
|
},
|
|
// Link-local 169.254.0.0/16
|
|
{
|
|
name: "169.254.0.1 link-local",
|
|
ip: "169.254.0.1",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "169.254.255.255 link-local",
|
|
ip: "169.254.255.255",
|
|
expected: true,
|
|
},
|
|
// Public IPv4
|
|
{
|
|
name: "8.8.8.8 public DNS",
|
|
ip: "8.8.8.8",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "1.1.1.1 public DNS",
|
|
ip: "1.1.1.1",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "93.184.216.34 example.com",
|
|
ip: "93.184.216.34",
|
|
expected: false,
|
|
},
|
|
// IPv6 loopback
|
|
{
|
|
name: "::1 IPv6 loopback",
|
|
ip: "::1",
|
|
expected: true,
|
|
},
|
|
// IPv6 link-local
|
|
{
|
|
name: "fe80::1 IPv6 link-local",
|
|
ip: "fe80::1",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "fe80::1234:5678:abcd:ef00 IPv6 link-local",
|
|
ip: "fe80::1234:5678:abcd:ef00",
|
|
expected: true,
|
|
},
|
|
// IPv6 unique local (fc00::/7)
|
|
{
|
|
name: "fc00::1 IPv6 unique local",
|
|
ip: "fc00::1",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "fd00::1 IPv6 unique local",
|
|
ip: "fd00::1",
|
|
expected: true,
|
|
},
|
|
// Public IPv6
|
|
{
|
|
name: "2001:4860:4860::8888 Google DNS IPv6",
|
|
ip: "2001:4860:4860::8888",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "2606:4700:4700::1111 Cloudflare DNS IPv6",
|
|
ip: "2606:4700:4700::1111",
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ip := net.ParseIP(tt.ip)
|
|
if ip == nil {
|
|
t.Fatalf("failed to parse IP %q", tt.ip)
|
|
}
|
|
result := isPrivateIP(ip)
|
|
if result != tt.expected {
|
|
t.Errorf("isPrivateIP(%q) = %v, want %v", tt.ip, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsNumericIP(t *testing.T) {
|
|
// NOTE: isNumericIP uses a simple heuristic that only checks for digits, dots, and colons.
|
|
// It intentionally doesn't validate full IPv6 with hex digits (a-f).
|
|
// This is acceptable because its purpose is to warn about HTTPS with IP addresses,
|
|
// and the common case is IPv4 or simple IPv6 like ::1.
|
|
tests := []struct {
|
|
name string
|
|
host string
|
|
expected bool
|
|
}{
|
|
// Valid IPv4 addresses
|
|
{
|
|
name: "localhost IPv4",
|
|
host: "127.0.0.1",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "public IPv4",
|
|
host: "8.8.8.8",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "private IPv4",
|
|
host: "192.168.1.1",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "all zeros IPv4",
|
|
host: "0.0.0.0",
|
|
expected: true,
|
|
},
|
|
// IPv6 addresses (only numeric ones detected)
|
|
{
|
|
name: "IPv6 loopback short",
|
|
host: "::1",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "IPv6 all zeros",
|
|
host: "::",
|
|
expected: true,
|
|
},
|
|
// IPv6 with hex letters - implementation returns false (known limitation)
|
|
{
|
|
name: "IPv6 full with hex - returns false (heuristic limitation)",
|
|
host: "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
|
|
expected: false, // contains hex letters
|
|
},
|
|
{
|
|
name: "IPv6 compressed with hex - returns false (heuristic limitation)",
|
|
host: "2001:db8::1",
|
|
expected: false, // contains hex letters
|
|
},
|
|
// Hostnames (not numeric)
|
|
{
|
|
name: "domain name",
|
|
host: "example.com",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "localhost hostname",
|
|
host: "localhost",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "subdomain",
|
|
host: "api.example.com",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "hyphenated hostname",
|
|
host: "my-server.local",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "hostname with numbers",
|
|
host: "server1.example.com",
|
|
expected: false,
|
|
},
|
|
// Edge cases
|
|
{
|
|
name: "empty string",
|
|
host: "",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "only dots - looks like IP pattern",
|
|
host: "...",
|
|
expected: true, // contains only dots with dots present
|
|
},
|
|
{
|
|
name: "only colons",
|
|
host: ":::",
|
|
expected: true, // contains only colons with colons present
|
|
},
|
|
{
|
|
name: "single number - no separator",
|
|
host: "123",
|
|
expected: false, // no dots or colons
|
|
},
|
|
{
|
|
name: "number with dot",
|
|
host: "123.",
|
|
expected: true, // contains digit and dot
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := isNumericIP(tt.host)
|
|
if result != tt.expected {
|
|
t.Errorf("isNumericIP(%q) = %v, want %v", tt.host, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsEmptyInterface(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value interface{}
|
|
expected bool
|
|
}{
|
|
// Nil
|
|
{
|
|
name: "nil value",
|
|
value: nil,
|
|
expected: true,
|
|
},
|
|
// Empty strings
|
|
{
|
|
name: "empty string",
|
|
value: "",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "whitespace only",
|
|
value: " ",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "tabs and spaces",
|
|
value: "\t \t",
|
|
expected: true,
|
|
},
|
|
// Non-empty strings
|
|
{
|
|
name: "non-empty string",
|
|
value: "hello",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "string with leading space",
|
|
value: " hello",
|
|
expected: false,
|
|
},
|
|
// Other types (not empty)
|
|
{
|
|
name: "integer zero",
|
|
value: 0,
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "integer non-zero",
|
|
value: 42,
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "boolean false",
|
|
value: false,
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "boolean true",
|
|
value: true,
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "empty slice",
|
|
value: []string{},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "empty map",
|
|
value: map[string]string{},
|
|
expected: false,
|
|
},
|
|
// fmt.Stringer types
|
|
{
|
|
name: "stringer with empty string",
|
|
value: emptyStringer{},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "stringer with whitespace",
|
|
value: whitespaceStringer{},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "stringer with content",
|
|
value: contentStringer{},
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := isEmptyInterface(tt.value)
|
|
if result != tt.expected {
|
|
t.Errorf("isEmptyInterface(%v) = %v, want %v", tt.value, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test helper types for fmt.Stringer interface
|
|
type emptyStringer struct{}
|
|
|
|
func (emptyStringer) String() string { return "" }
|
|
|
|
type whitespaceStringer struct{}
|
|
|
|
func (whitespaceStringer) String() string { return " \t " }
|
|
|
|
type contentStringer struct{}
|
|
|
|
func (contentStringer) String() string { return "content" }
|
|
|
|
func TestEnsurePushoverCustomFieldAliases(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input map[string]interface{}
|
|
expected map[string]interface{}
|
|
}{
|
|
{
|
|
name: "nil input returns nil",
|
|
input: nil,
|
|
expected: nil,
|
|
},
|
|
{
|
|
name: "empty map returns empty",
|
|
input: map[string]interface{}{},
|
|
expected: map[string]interface{}{},
|
|
},
|
|
{
|
|
name: "token already set, no alias",
|
|
input: map[string]interface{}{
|
|
"token": "my-token",
|
|
},
|
|
expected: map[string]interface{}{
|
|
"token": "my-token",
|
|
},
|
|
},
|
|
{
|
|
name: "user already set, no alias",
|
|
input: map[string]interface{}{
|
|
"user": "my-user",
|
|
},
|
|
expected: map[string]interface{}{
|
|
"user": "my-user",
|
|
},
|
|
},
|
|
{
|
|
name: "app_token aliased to token",
|
|
input: map[string]interface{}{
|
|
"app_token": "legacy-token",
|
|
},
|
|
expected: map[string]interface{}{
|
|
"app_token": "legacy-token",
|
|
"token": "legacy-token",
|
|
},
|
|
},
|
|
{
|
|
name: "user_token aliased to user",
|
|
input: map[string]interface{}{
|
|
"user_token": "legacy-user",
|
|
},
|
|
expected: map[string]interface{}{
|
|
"user_token": "legacy-user",
|
|
"user": "legacy-user",
|
|
},
|
|
},
|
|
{
|
|
name: "both aliases applied",
|
|
input: map[string]interface{}{
|
|
"app_token": "legacy-token",
|
|
"user_token": "legacy-user",
|
|
},
|
|
expected: map[string]interface{}{
|
|
"app_token": "legacy-token",
|
|
"user_token": "legacy-user",
|
|
"token": "legacy-token",
|
|
"user": "legacy-user",
|
|
},
|
|
},
|
|
{
|
|
name: "existing token takes precedence over app_token",
|
|
input: map[string]interface{}{
|
|
"token": "primary-token",
|
|
"app_token": "legacy-token",
|
|
},
|
|
expected: map[string]interface{}{
|
|
"token": "primary-token",
|
|
"app_token": "legacy-token",
|
|
},
|
|
},
|
|
{
|
|
name: "existing user takes precedence over user_token",
|
|
input: map[string]interface{}{
|
|
"user": "primary-user",
|
|
"user_token": "legacy-user",
|
|
},
|
|
expected: map[string]interface{}{
|
|
"user": "primary-user",
|
|
"user_token": "legacy-user",
|
|
},
|
|
},
|
|
{
|
|
name: "empty token uses app_token alias",
|
|
input: map[string]interface{}{
|
|
"token": "",
|
|
"app_token": "legacy-token",
|
|
},
|
|
expected: map[string]interface{}{
|
|
"token": "legacy-token",
|
|
"app_token": "legacy-token",
|
|
},
|
|
},
|
|
{
|
|
name: "whitespace token uses app_token alias",
|
|
input: map[string]interface{}{
|
|
"token": " ",
|
|
"app_token": "legacy-token",
|
|
},
|
|
expected: map[string]interface{}{
|
|
"token": "legacy-token",
|
|
"app_token": "legacy-token",
|
|
},
|
|
},
|
|
{
|
|
name: "empty legacy values not aliased",
|
|
input: map[string]interface{}{
|
|
"app_token": "",
|
|
"user_token": " ",
|
|
},
|
|
expected: map[string]interface{}{
|
|
"app_token": "",
|
|
"user_token": " ",
|
|
},
|
|
},
|
|
{
|
|
name: "other fields preserved",
|
|
input: map[string]interface{}{
|
|
"app_token": "legacy-token",
|
|
"priority": 2,
|
|
"sound": "pushover",
|
|
},
|
|
expected: map[string]interface{}{
|
|
"app_token": "legacy-token",
|
|
"token": "legacy-token",
|
|
"priority": 2,
|
|
"sound": "pushover",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := ensurePushoverCustomFieldAliases(tt.input)
|
|
|
|
if tt.expected == nil {
|
|
if result != nil {
|
|
t.Errorf("ensurePushoverCustomFieldAliases() = %v, want nil", result)
|
|
}
|
|
return
|
|
}
|
|
|
|
if len(result) != len(tt.expected) {
|
|
t.Errorf("ensurePushoverCustomFieldAliases() length = %d, want %d", len(result), len(tt.expected))
|
|
}
|
|
|
|
for key, expectedValue := range tt.expected {
|
|
actualValue, ok := result[key]
|
|
if !ok {
|
|
t.Errorf("ensurePushoverCustomFieldAliases() missing key %q", key)
|
|
continue
|
|
}
|
|
if actualValue != expectedValue {
|
|
t.Errorf("ensurePushoverCustomFieldAliases()[%q] = %v, want %v", key, actualValue, expectedValue)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Helper function for string containment check
|
|
func contains(s, substr string) bool {
|
|
return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
|
|
(len(s) > 0 && len(substr) > 0 && findSubstring(s, substr)))
|
|
}
|
|
|
|
func findSubstring(s, substr string) bool {
|
|
for i := 0; i <= len(s)-len(substr); i++ {
|
|
if s[i:i+len(substr)] == substr {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|