- Add --dry-run/-n flag for backup commands with comprehensive preflight checks - Database connectivity validation - Required tools availability check - Storage target and permissions verification - Backup size estimation - Encryption and cloud storage configuration validation - Implement GFS (Grandfather-Father-Son) retention policies - Daily/Weekly/Monthly/Yearly tier classification - Configurable retention counts per tier - Custom weekly day and monthly day settings - ISO week handling for proper week boundaries - Add notification system with SMTP and webhook support - SMTP email notifications with TLS/STARTTLS - Webhook HTTP notifications with HMAC-SHA256 signing - Slack-compatible webhook payload format - Event types: backup/restore started/completed/failed, cleanup, verify, PITR - Configurable severity levels and retry logic - Update README.md with documentation for all new features
280 lines
6.7 KiB
Go
280 lines
6.7 KiB
Go
package notify
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestNewEvent(t *testing.T) {
|
|
event := NewEvent(EventBackupCompleted, SeverityInfo, "Backup completed")
|
|
|
|
if event.Type != EventBackupCompleted {
|
|
t.Errorf("Type = %v, expected %v", event.Type, EventBackupCompleted)
|
|
}
|
|
|
|
if event.Severity != SeverityInfo {
|
|
t.Errorf("Severity = %v, expected %v", event.Severity, SeverityInfo)
|
|
}
|
|
|
|
if event.Message != "Backup completed" {
|
|
t.Errorf("Message = %q, expected %q", event.Message, "Backup completed")
|
|
}
|
|
|
|
if event.Timestamp.IsZero() {
|
|
t.Error("Timestamp should not be zero")
|
|
}
|
|
}
|
|
|
|
func TestEventChaining(t *testing.T) {
|
|
event := NewEvent(EventBackupCompleted, SeverityInfo, "Backup completed").
|
|
WithDatabase("testdb").
|
|
WithBackupInfo("/backups/test.dump", 1024).
|
|
WithHostname("server1").
|
|
WithDetail("custom", "value")
|
|
|
|
if event.Database != "testdb" {
|
|
t.Errorf("Database = %q, expected %q", event.Database, "testdb")
|
|
}
|
|
|
|
if event.BackupFile != "/backups/test.dump" {
|
|
t.Errorf("BackupFile = %q, expected %q", event.BackupFile, "/backups/test.dump")
|
|
}
|
|
|
|
if event.BackupSize != 1024 {
|
|
t.Errorf("BackupSize = %d, expected %d", event.BackupSize, 1024)
|
|
}
|
|
|
|
if event.Hostname != "server1" {
|
|
t.Errorf("Hostname = %q, expected %q", event.Hostname, "server1")
|
|
}
|
|
|
|
if event.Details["custom"] != "value" {
|
|
t.Errorf("Details[custom] = %q, expected %q", event.Details["custom"], "value")
|
|
}
|
|
}
|
|
|
|
func TestFormatEventSubject(t *testing.T) {
|
|
tests := []struct {
|
|
eventType EventType
|
|
database string
|
|
contains string
|
|
}{
|
|
{EventBackupCompleted, "testdb", "Backup Completed"},
|
|
{EventBackupFailed, "testdb", "Backup Failed"},
|
|
{EventRestoreCompleted, "", "Restore Completed"},
|
|
{EventCleanupCompleted, "", "Cleanup Completed"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
event := NewEvent(tc.eventType, SeverityInfo, "test")
|
|
if tc.database != "" {
|
|
event.WithDatabase(tc.database)
|
|
}
|
|
|
|
subject := FormatEventSubject(event)
|
|
if subject == "" {
|
|
t.Errorf("FormatEventSubject() returned empty string for %v", tc.eventType)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFormatEventBody(t *testing.T) {
|
|
event := NewEvent(EventBackupCompleted, SeverityInfo, "Backup completed").
|
|
WithDatabase("testdb").
|
|
WithBackupInfo("/backups/test.dump", 1024).
|
|
WithHostname("server1")
|
|
|
|
body := FormatEventBody(event)
|
|
|
|
if body == "" {
|
|
t.Error("FormatEventBody() returned empty string")
|
|
}
|
|
|
|
// Should contain message
|
|
if body == "" || len(body) < 10 {
|
|
t.Error("Body should contain event information")
|
|
}
|
|
}
|
|
|
|
func TestDefaultConfig(t *testing.T) {
|
|
config := DefaultConfig()
|
|
|
|
if config.SMTPPort != 587 {
|
|
t.Errorf("SMTPPort = %d, expected 587", config.SMTPPort)
|
|
}
|
|
|
|
if !config.SMTPStartTLS {
|
|
t.Error("SMTPStartTLS should be true by default")
|
|
}
|
|
|
|
if config.WebhookMethod != "POST" {
|
|
t.Errorf("WebhookMethod = %q, expected POST", config.WebhookMethod)
|
|
}
|
|
|
|
if !config.OnSuccess {
|
|
t.Error("OnSuccess should be true by default")
|
|
}
|
|
|
|
if !config.OnFailure {
|
|
t.Error("OnFailure should be true by default")
|
|
}
|
|
|
|
if config.Retries != 3 {
|
|
t.Errorf("Retries = %d, expected 3", config.Retries)
|
|
}
|
|
}
|
|
|
|
func TestWebhookNotifierSend(t *testing.T) {
|
|
var receivedPayload WebhookPayload
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
t.Errorf("Method = %q, expected POST", r.Method)
|
|
}
|
|
|
|
if r.Header.Get("Content-Type") != "application/json" {
|
|
t.Errorf("Content-Type = %q, expected application/json", r.Header.Get("Content-Type"))
|
|
}
|
|
|
|
decoder := json.NewDecoder(r.Body)
|
|
if err := decoder.Decode(&receivedPayload); err != nil {
|
|
t.Errorf("Failed to decode payload: %v", err)
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer server.Close()
|
|
|
|
config := DefaultConfig()
|
|
config.WebhookEnabled = true
|
|
config.WebhookURL = server.URL
|
|
|
|
notifier := NewWebhookNotifier(config)
|
|
|
|
event := NewEvent(EventBackupCompleted, SeverityInfo, "Backup completed").
|
|
WithDatabase("testdb")
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
err := notifier.Send(ctx, event)
|
|
if err != nil {
|
|
t.Errorf("Send() error = %v", err)
|
|
}
|
|
|
|
if receivedPayload.Event.Database != "testdb" {
|
|
t.Errorf("Received database = %q, expected testdb", receivedPayload.Event.Database)
|
|
}
|
|
}
|
|
|
|
func TestWebhookNotifierDisabled(t *testing.T) {
|
|
config := DefaultConfig()
|
|
config.WebhookEnabled = false
|
|
|
|
notifier := NewWebhookNotifier(config)
|
|
|
|
if notifier.IsEnabled() {
|
|
t.Error("Notifier should be disabled")
|
|
}
|
|
|
|
event := NewEvent(EventBackupCompleted, SeverityInfo, "test")
|
|
err := notifier.Send(context.Background(), event)
|
|
if err != nil {
|
|
t.Errorf("Send() should not error when disabled: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSMTPNotifierDisabled(t *testing.T) {
|
|
config := DefaultConfig()
|
|
config.SMTPEnabled = false
|
|
|
|
notifier := NewSMTPNotifier(config)
|
|
|
|
if notifier.IsEnabled() {
|
|
t.Error("Notifier should be disabled")
|
|
}
|
|
|
|
event := NewEvent(EventBackupCompleted, SeverityInfo, "test")
|
|
err := notifier.Send(context.Background(), event)
|
|
if err != nil {
|
|
t.Errorf("Send() should not error when disabled: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestManagerNoNotifiers(t *testing.T) {
|
|
config := DefaultConfig()
|
|
config.SMTPEnabled = false
|
|
config.WebhookEnabled = false
|
|
|
|
manager := NewManager(config)
|
|
|
|
if manager.HasEnabledNotifiers() {
|
|
t.Error("Manager should have no enabled notifiers")
|
|
}
|
|
|
|
names := manager.EnabledNotifiers()
|
|
if len(names) != 0 {
|
|
t.Errorf("EnabledNotifiers() = %v, expected empty", names)
|
|
}
|
|
}
|
|
|
|
func TestManagerWithWebhook(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer server.Close()
|
|
|
|
config := DefaultConfig()
|
|
config.WebhookEnabled = true
|
|
config.WebhookURL = server.URL
|
|
|
|
manager := NewManager(config)
|
|
|
|
if !manager.HasEnabledNotifiers() {
|
|
t.Error("Manager should have enabled notifiers")
|
|
}
|
|
|
|
names := manager.EnabledNotifiers()
|
|
if len(names) != 1 || names[0] != "webhook" {
|
|
t.Errorf("EnabledNotifiers() = %v, expected [webhook]", names)
|
|
}
|
|
}
|
|
|
|
func TestNullManager(t *testing.T) {
|
|
manager := NullManager()
|
|
|
|
if manager.HasEnabledNotifiers() {
|
|
t.Error("NullManager should have no enabled notifiers")
|
|
}
|
|
|
|
// Should not panic
|
|
manager.BackupStarted("testdb")
|
|
manager.BackupCompleted("testdb", "/backup.dump", 1024, nil)
|
|
manager.BackupFailed("testdb", nil)
|
|
}
|
|
|
|
func TestFormatBytes(t *testing.T) {
|
|
tests := []struct {
|
|
input int64
|
|
expected string
|
|
}{
|
|
{0, "0 B"},
|
|
{500, "500 B"},
|
|
{1024, "1.0 KB"},
|
|
{1536, "1.5 KB"},
|
|
{1048576, "1.0 MB"},
|
|
{1073741824, "1.0 GB"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
result := formatBytes(tc.input)
|
|
if result != tc.expected {
|
|
t.Errorf("formatBytes(%d) = %q, expected %q", tc.input, result, tc.expected)
|
|
}
|
|
}
|
|
}
|