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:
parent
cb683597c6
commit
67a351fbd9
2 changed files with 441 additions and 0 deletions
|
|
@ -5,6 +5,8 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLoadOrCreateBootstrapToken_EmptyDataPath(t *testing.T) {
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue