test: Add tests for webhook template generation and bootstrap token functions

- Add comprehensive tests for generatePayloadFromTemplateWithService covering:
  - Valid JSON template rendering
  - Invalid template syntax error handling
  - Template execution errors
  - ntfy service (plain text, skips JSON validation)
  - Various services (discord, telegram, pagerduty) JSON validation
  - Template function usage (upper, title)
  - Numeric and boolean values in templates
  - Special characters and empty template edge cases

- Add tests for bootstrapTokenValid covering:
  - Nil router handling
  - Empty hash and empty token cases
  - Valid token validation
  - Invalid token rejection
  - Whitespace trimming

- Add tests for clearBootstrapToken covering:
  - Nil router safety
  - Token file deletion
  - Missing file handling
  - Empty path handling
This commit is contained in:
rcourtman 2025-12-01 22:07:40 +00:00
parent cb683597c6
commit 67a351fbd9
2 changed files with 441 additions and 0 deletions

View file

@ -5,6 +5,8 @@ import (
"path/filepath"
"runtime"
"testing"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
)
func TestLoadOrCreateBootstrapToken_EmptyDataPath(t *testing.T) {
@ -290,3 +292,164 @@ func TestGenerateBootstrapToken(t *testing.T) {
}
}
}
func TestBootstrapTokenValid(t *testing.T) {
t.Run("nil router returns false", func(t *testing.T) {
var r *Router = nil
if r.bootstrapTokenValid("anything") {
t.Error("expected false for nil router")
}
})
t.Run("empty hash returns false", func(t *testing.T) {
r := &Router{bootstrapTokenHash: ""}
if r.bootstrapTokenValid("anything") {
t.Error("expected false when bootstrapTokenHash is empty")
}
})
t.Run("empty token returns false", func(t *testing.T) {
// Generate a token and create a router with its hash
token, err := generateBootstrapToken()
if err != nil {
t.Fatalf("generateBootstrapToken() error: %v", err)
}
r := &Router{}
// Create a token and store its hash - use loadOrCreateBootstrapToken to get a hash indirectly
tmpDir := t.TempDir()
loadedToken, _, _, err := loadOrCreateBootstrapToken(tmpDir)
if err != nil {
t.Fatalf("loadOrCreateBootstrapToken() error: %v", err)
}
// Since we can't directly access HashAPIToken, we need to use initializeBootstrapToken
// Instead, let's test with a known hash by using the auth package
_ = token
_ = loadedToken
// For this test, we just need any non-empty hash
r.bootstrapTokenHash = "somehash"
if r.bootstrapTokenValid("") {
t.Error("expected false for empty token")
}
if r.bootstrapTokenValid(" ") {
t.Error("expected false for whitespace-only token")
}
})
t.Run("valid token returns true", func(t *testing.T) {
tmpDir := t.TempDir()
token, _, _, err := loadOrCreateBootstrapToken(tmpDir)
if err != nil {
t.Fatalf("loadOrCreateBootstrapToken() error: %v", err)
}
// Create a router that simulates having loaded the token
// We need to use the auth package to hash the token
cfg := &config.Config{DataPath: tmpDir}
r := &Router{config: cfg}
r.initializeBootstrapToken()
if !r.bootstrapTokenValid(token) {
t.Error("expected true for valid token")
}
})
t.Run("invalid token returns false", func(t *testing.T) {
tmpDir := t.TempDir()
_, _, _, err := loadOrCreateBootstrapToken(tmpDir)
if err != nil {
t.Fatalf("loadOrCreateBootstrapToken() error: %v", err)
}
cfg := &config.Config{DataPath: tmpDir}
r := &Router{config: cfg}
r.initializeBootstrapToken()
if r.bootstrapTokenValid("wrongtoken") {
t.Error("expected false for wrong token")
}
})
t.Run("token with whitespace is trimmed", func(t *testing.T) {
tmpDir := t.TempDir()
token, _, _, err := loadOrCreateBootstrapToken(tmpDir)
if err != nil {
t.Fatalf("loadOrCreateBootstrapToken() error: %v", err)
}
cfg := &config.Config{DataPath: tmpDir}
r := &Router{config: cfg}
r.initializeBootstrapToken()
// Token with leading/trailing whitespace should still validate
if !r.bootstrapTokenValid(" " + token + " ") {
t.Error("expected true for token with surrounding whitespace")
}
})
}
func TestClearBootstrapToken(t *testing.T) {
t.Run("nil router does not panic", func(t *testing.T) {
var r *Router = nil
// Should not panic
r.clearBootstrapToken()
})
t.Run("clears token hash and path", func(t *testing.T) {
tmpDir := t.TempDir()
_, _, tokenPath, err := loadOrCreateBootstrapToken(tmpDir)
if err != nil {
t.Fatalf("loadOrCreateBootstrapToken() error: %v", err)
}
r := &Router{
bootstrapTokenHash: "somehash",
bootstrapTokenPath: tokenPath,
}
r.clearBootstrapToken()
if r.bootstrapTokenHash != "" {
t.Errorf("expected empty bootstrapTokenHash, got %q", r.bootstrapTokenHash)
}
if r.bootstrapTokenPath != "" {
t.Errorf("expected empty bootstrapTokenPath, got %q", r.bootstrapTokenPath)
}
// Verify file was deleted
if _, err := os.Stat(tokenPath); !os.IsNotExist(err) {
t.Error("expected token file to be deleted")
}
})
t.Run("handles missing file gracefully", func(t *testing.T) {
r := &Router{
bootstrapTokenHash: "somehash",
bootstrapTokenPath: "/nonexistent/path/token",
}
// Should not panic, should clear the hash and path
r.clearBootstrapToken()
if r.bootstrapTokenHash != "" {
t.Errorf("expected empty bootstrapTokenHash, got %q", r.bootstrapTokenHash)
}
if r.bootstrapTokenPath != "" {
t.Errorf("expected empty bootstrapTokenPath, got %q", r.bootstrapTokenPath)
}
})
t.Run("handles empty path", func(t *testing.T) {
r := &Router{
bootstrapTokenHash: "somehash",
bootstrapTokenPath: "",
}
// Should not panic
r.clearBootstrapToken()
if r.bootstrapTokenHash != "" {
t.Errorf("expected empty bootstrapTokenHash, got %q", r.bootstrapTokenHash)
}
})
}

View file

@ -2210,3 +2210,281 @@ func TestCheckWebhookRateLimit(t *testing.T) {
}
})
}
func TestGeneratePayloadFromTemplateWithService(t *testing.T) {
t.Run("valid JSON template", func(t *testing.T) {
nm := &NotificationManager{}
data := WebhookPayloadData{
ResourceName: "test-vm",
Message: "CPU usage high",
Level: "warning",
}
template := `{"resource": "{{.ResourceName}}", "message": "{{.Message}}", "level": "{{.Level}}"}`
result, err := nm.generatePayloadFromTemplateWithService(template, data, "generic")
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
expected := `{"resource": "test-vm", "message": "CPU usage high", "level": "warning"}`
if string(result) != expected {
t.Fatalf("expected %q, got %q", expected, string(result))
}
})
t.Run("invalid template syntax", func(t *testing.T) {
nm := &NotificationManager{}
data := WebhookPayloadData{ResourceName: "test"}
// Missing closing brace in template
template := `{"resource": "{{.ResourceName}"}`
_, err := nm.generatePayloadFromTemplateWithService(template, data, "generic")
if err == nil {
t.Fatal("expected error for invalid template syntax")
}
if !strings.Contains(err.Error(), "invalid template") {
t.Fatalf("expected 'invalid template' in error, got %v", err)
}
})
t.Run("template execution error - missing method", func(t *testing.T) {
nm := &NotificationManager{}
data := WebhookPayloadData{ResourceName: "test"}
// Reference a non-existent field/method
template := `{"value": "{{.NonExistentMethod}}"}`
_, err := nm.generatePayloadFromTemplateWithService(template, data, "generic")
if err == nil {
t.Fatal("expected error for non-existent method")
}
if !strings.Contains(err.Error(), "template execution failed") {
t.Fatalf("expected 'template execution failed' in error, got %v", err)
}
})
t.Run("ntfy service skips JSON validation", func(t *testing.T) {
nm := &NotificationManager{}
data := WebhookPayloadData{
ResourceName: "server1",
Message: "Alert triggered",
}
// Plain text template (not valid JSON)
template := `Alert: {{.ResourceName}} - {{.Message}}`
result, err := nm.generatePayloadFromTemplateWithService(template, data, "ntfy")
if err != nil {
t.Fatalf("expected no error for ntfy plain text, got %v", err)
}
expected := "Alert: server1 - Alert triggered"
if string(result) != expected {
t.Fatalf("expected %q, got %q", expected, string(result))
}
})
t.Run("non-ntfy service validates JSON", func(t *testing.T) {
nm := &NotificationManager{}
data := WebhookPayloadData{ResourceName: "test"}
// Plain text (invalid JSON) for non-ntfy service
template := `Plain text: {{.ResourceName}}`
_, err := nm.generatePayloadFromTemplateWithService(template, data, "slack")
if err == nil {
t.Fatal("expected error for non-JSON output on slack service")
}
if !strings.Contains(err.Error(), "template produced invalid JSON") {
t.Fatalf("expected 'template produced invalid JSON' in error, got %v", err)
}
})
t.Run("discord service validates JSON", func(t *testing.T) {
nm := &NotificationManager{}
data := WebhookPayloadData{
ResourceName: "vm-100",
Message: "Memory threshold exceeded",
}
template := `{"content": "{{.ResourceName}}: {{.Message}}"}`
result, err := nm.generatePayloadFromTemplateWithService(template, data, "discord")
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
expected := `{"content": "vm-100: Memory threshold exceeded"}`
if string(result) != expected {
t.Fatalf("expected %q, got %q", expected, string(result))
}
})
t.Run("telegram service validates JSON", func(t *testing.T) {
nm := &NotificationManager{}
data := WebhookPayloadData{
ChatID: "12345",
Message: "Alert notification",
}
template := `{"chat_id": "{{.ChatID}}", "text": "{{.Message}}"}`
result, err := nm.generatePayloadFromTemplateWithService(template, data, "telegram")
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
expected := `{"chat_id": "12345", "text": "Alert notification"}`
if string(result) != expected {
t.Fatalf("expected %q, got %q", expected, string(result))
}
})
t.Run("template with numeric values", func(t *testing.T) {
nm := &NotificationManager{}
data := WebhookPayloadData{
ResourceName: "vm-100",
Value: 85.5,
Threshold: 80.0,
}
template := `{"resource": "{{.ResourceName}}", "value": {{.Value}}, "threshold": {{.Threshold}}}`
result, err := nm.generatePayloadFromTemplateWithService(template, data, "generic")
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
// Verify it's valid JSON
var parsed map[string]interface{}
if err := json.Unmarshal(result, &parsed); err != nil {
t.Fatalf("result is not valid JSON: %v", err)
}
if parsed["value"].(float64) != 85.5 {
t.Fatalf("expected value 85.5, got %v", parsed["value"])
}
})
t.Run("template with boolean values", func(t *testing.T) {
nm := &NotificationManager{}
data := WebhookPayloadData{
ResourceName: "test",
Acknowledged: true,
}
template := `{"resource": "{{.ResourceName}}", "acknowledged": {{.Acknowledged}}}`
result, err := nm.generatePayloadFromTemplateWithService(template, data, "generic")
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
var parsed map[string]interface{}
if err := json.Unmarshal(result, &parsed); err != nil {
t.Fatalf("result is not valid JSON: %v", err)
}
if parsed["acknowledged"].(bool) != true {
t.Fatalf("expected acknowledged true, got %v", parsed["acknowledged"])
}
})
t.Run("template with special characters in strings", func(t *testing.T) {
nm := &NotificationManager{}
data := WebhookPayloadData{
ResourceName: "test",
Message: `Line1\nLine2 with "quotes" and \t tabs`,
}
// Use printf to escape for JSON
template := `{"message": "{{.Message}}"}`
// This will produce invalid JSON because of unescaped characters
_, err := nm.generatePayloadFromTemplateWithService(template, data, "generic")
if err == nil {
t.Fatal("expected error for unescaped special characters in JSON")
}
if !strings.Contains(err.Error(), "template produced invalid JSON") {
t.Fatalf("expected 'template produced invalid JSON' in error, got %v", err)
}
})
t.Run("empty template", func(t *testing.T) {
nm := &NotificationManager{}
data := WebhookPayloadData{}
template := ""
_, err := nm.generatePayloadFromTemplateWithService(template, data, "generic")
if err == nil {
t.Fatal("expected error for empty template producing invalid JSON")
}
})
t.Run("template with template functions", func(t *testing.T) {
nm := &NotificationManager{}
data := WebhookPayloadData{
ResourceName: "test-server",
Level: "warning",
}
template := `{"resource": "{{upper .ResourceName}}", "level": "{{title .Level}}"}`
result, err := nm.generatePayloadFromTemplateWithService(template, data, "generic")
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
var parsed map[string]interface{}
if err := json.Unmarshal(result, &parsed); err != nil {
t.Fatalf("result is not valid JSON: %v", err)
}
if parsed["resource"] != "TEST-SERVER" {
t.Fatalf("expected 'TEST-SERVER', got %v", parsed["resource"])
}
if parsed["level"] != "Warning" {
t.Fatalf("expected 'Warning', got %v", parsed["level"])
}
})
t.Run("pagerduty service validates JSON", func(t *testing.T) {
nm := &NotificationManager{}
data := WebhookPayloadData{
ResourceName: "critical-service",
Message: "Service down",
Level: "critical",
}
template := `{"routing_key": "test", "event_action": "trigger", "payload": {"summary": "{{.Message}}"}}`
result, err := nm.generatePayloadFromTemplateWithService(template, data, "pagerduty")
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
var parsed map[string]interface{}
if err := json.Unmarshal(result, &parsed); err != nil {
t.Fatalf("result is not valid JSON: %v", err)
}
})
t.Run("generic service validates JSON", func(t *testing.T) {
nm := &NotificationManager{}
data := WebhookPayloadData{
ResourceName: "test",
}
template := `not valid json at all`
_, err := nm.generatePayloadFromTemplateWithService(template, data, "generic")
if err == nil {
t.Fatal("expected error for invalid JSON")
}
})
t.Run("unknown service still validates JSON", func(t *testing.T) {
nm := &NotificationManager{}
data := WebhookPayloadData{
ResourceName: "test",
}
// Valid JSON
template := `{"test": "{{.ResourceName}}"}`
result, err := nm.generatePayloadFromTemplateWithService(template, data, "unknown_service")
if err != nil {
t.Fatalf("expected no error for valid JSON on unknown service, got %v", err)
}
expected := `{"test": "test"}`
if string(result) != expected {
t.Fatalf("expected %q, got %q", expected, string(result))
}
})
}