Files
dbbackup/internal/retention/gfs_test.go
Alexander Renz d0d83b61ef feat: add dry-run mode, GFS retention policies, and notifications
- 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
2025-12-13 19:00:54 +01:00

193 lines
4.7 KiB
Go

package retention
import (
"testing"
"time"
"dbbackup/internal/metadata"
)
func TestParseWeekday(t *testing.T) {
tests := []struct {
input string
expected int
}{
{"Sunday", 0},
{"sunday", 0},
{"Sun", 0},
{"Monday", 1},
{"mon", 1},
{"Tuesday", 2},
{"Wed", 3},
{"Thursday", 4},
{"Friday", 5},
{"Saturday", 6},
{"sat", 6},
{"invalid", 0},
{"", 0},
}
for _, tc := range tests {
result := ParseWeekday(tc.input)
if result != tc.expected {
t.Errorf("ParseWeekday(%q) = %d, expected %d", tc.input, result, tc.expected)
}
}
}
func TestClassifyBackup(t *testing.T) {
policy := GFSPolicy{
WeeklyDay: 0,
MonthlyDay: 1,
}
tests := []struct {
name string
time time.Time
expected []Tier
}{
{
name: "Regular weekday",
time: time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC),
expected: []Tier{TierDaily},
},
{
name: "Sunday weekly",
time: time.Date(2024, 1, 14, 10, 0, 0, 0, time.UTC),
expected: []Tier{TierDaily, TierWeekly},
},
{
name: "First of month",
time: time.Date(2024, 2, 1, 10, 0, 0, 0, time.UTC),
expected: []Tier{TierDaily, TierMonthly},
},
{
name: "First of January yearly",
time: time.Date(2024, 1, 1, 10, 0, 0, 0, time.UTC),
expected: []Tier{TierDaily, TierMonthly, TierYearly},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := ClassifyBackup(tc.time, policy)
if len(result) != len(tc.expected) {
t.Errorf("ClassifyBackup() returned %d tiers, expected %d", len(result), len(tc.expected))
return
}
for i, tier := range result {
if tier != tc.expected[i] {
t.Errorf("tier[%d] = %v, expected %v", i, tier, tc.expected[i])
}
}
})
}
}
func TestApplyGFSPolicyToBackups(t *testing.T) {
now := time.Now()
backups := []*metadata.BackupMetadata{
{BackupFile: "backup_day1.dump", Timestamp: now.AddDate(0, 0, -1), SizeBytes: 1000},
{BackupFile: "backup_day2.dump", Timestamp: now.AddDate(0, 0, -2), SizeBytes: 1000},
{BackupFile: "backup_day3.dump", Timestamp: now.AddDate(0, 0, -3), SizeBytes: 1000},
{BackupFile: "backup_day4.dump", Timestamp: now.AddDate(0, 0, -4), SizeBytes: 1000},
{BackupFile: "backup_day5.dump", Timestamp: now.AddDate(0, 0, -5), SizeBytes: 1000},
{BackupFile: "backup_day6.dump", Timestamp: now.AddDate(0, 0, -6), SizeBytes: 1000},
{BackupFile: "backup_day7.dump", Timestamp: now.AddDate(0, 0, -7), SizeBytes: 1000},
{BackupFile: "backup_day8.dump", Timestamp: now.AddDate(0, 0, -8), SizeBytes: 1000},
{BackupFile: "backup_day9.dump", Timestamp: now.AddDate(0, 0, -9), SizeBytes: 1000},
{BackupFile: "backup_day10.dump", Timestamp: now.AddDate(0, 0, -10), SizeBytes: 1000},
}
policy := GFSPolicy{
Enabled: true,
Daily: 5,
Weekly: 2,
Monthly: 1,
Yearly: 1,
WeeklyDay: 0,
MonthlyDay: 1,
DryRun: true,
}
result, err := ApplyGFSPolicyToBackups(backups, policy)
if err != nil {
t.Fatalf("ApplyGFSPolicyToBackups() error = %v", err)
}
if result.TotalKept < policy.Daily {
t.Errorf("TotalKept = %d, expected at least %d", result.TotalKept, policy.Daily)
}
if result.TotalBackups != len(backups) {
t.Errorf("TotalBackups = %d, expected %d", result.TotalBackups, len(backups))
}
if len(result.ToKeep)+len(result.ToDelete) != result.TotalBackups {
t.Errorf("ToKeep(%d) + ToDelete(%d) != TotalBackups(%d)",
len(result.ToKeep), len(result.ToDelete), result.TotalBackups)
}
}
func TestGFSPolicyWithEmptyBackups(t *testing.T) {
policy := DefaultGFSPolicy()
policy.DryRun = true
result, err := ApplyGFSPolicyToBackups([]*metadata.BackupMetadata{}, policy)
if err != nil {
t.Fatalf("ApplyGFSPolicyToBackups() error = %v", err)
}
if result.TotalBackups != 0 {
t.Errorf("TotalBackups = %d, expected 0", result.TotalBackups)
}
if result.TotalKept != 0 {
t.Errorf("TotalKept = %d, expected 0", result.TotalKept)
}
}
func TestDefaultGFSPolicy(t *testing.T) {
policy := DefaultGFSPolicy()
if !policy.Enabled {
t.Error("DefaultGFSPolicy should be enabled")
}
if policy.Daily != 7 {
t.Errorf("Daily = %d, expected 7", policy.Daily)
}
if policy.Weekly != 4 {
t.Errorf("Weekly = %d, expected 4", policy.Weekly)
}
if policy.Monthly != 12 {
t.Errorf("Monthly = %d, expected 12", policy.Monthly)
}
if policy.Yearly != 3 {
t.Errorf("Yearly = %d, expected 3", policy.Yearly)
}
}
func TestTierString(t *testing.T) {
tests := []struct {
tier Tier
expected string
}{
{TierDaily, "daily"},
{TierWeekly, "weekly"},
{TierMonthly, "monthly"},
{TierYearly, "yearly"},
{Tier(99), "unknown"},
}
for _, tc := range tests {
result := tc.tier.String()
if result != tc.expected {
t.Errorf("Tier(%d).String() = %q, expected %q", tc.tier, result, tc.expected)
}
}
}