1package app
2
3import (
4 "testing"
5
6 "github.com/spf13/viper"
7 "github.com/stretchr/testify/assert"
8 "github.com/stretchr/testify/require"
9
10 "github.com/bnema/gordon/internal/domain"
11)
12
13func TestValidateBackupRetentionRejectsNegativeValues(t *testing.T) {
14 tests := []struct {
15 name string
16 set func(*Config)
17 err string
18 }{
19 {
20 name: "hourly",
21 set: func(cfg *Config) {
22 cfg.Backups.Databases.Retention.Hourly = -1
23 },
24 err: "backups.databases.retention.hourly",
25 },
26 {
27 name: "daily",
28 set: func(cfg *Config) {
29 cfg.Backups.Databases.Retention.Daily = -1
30 },
31 err: "backups.databases.retention.daily",
32 },
33 {
34 name: "weekly",
35 set: func(cfg *Config) {
36 cfg.Backups.Databases.Retention.Weekly = -1
37 },
38 err: "backups.databases.retention.weekly",
39 },
40 {
41 name: "monthly",
42 set: func(cfg *Config) {
43 cfg.Backups.Databases.Retention.Monthly = -1
44 },
45 err: "backups.databases.retention.monthly",
46 },
47 }
48
49 for _, tt := range tests {
50 t.Run(tt.name, func(t *testing.T) {
51 var cfg Config
52 tt.set(&cfg)
53
54 _, err := validateBackupRetention(cfg)
55 require.Error(t, err)
56 assert.Contains(t, err.Error(), tt.err)
57 })
58 }
59}
60
61func TestValidateBackupRetentionAcceptsZeroAndPositiveValues(t *testing.T) {
62 t.Run("positive values", func(t *testing.T) {
63 var cfg Config
64 cfg.Backups.Databases.Retention.Hourly = 1
65 cfg.Backups.Databases.Retention.Daily = 7
66 cfg.Backups.Databases.Retention.Weekly = 4
67 cfg.Backups.Databases.Retention.Monthly = 12
68
69 retention, err := validateBackupRetention(cfg)
70 require.NoError(t, err)
71 assert.Equal(t, 1, retention.Hourly)
72 assert.Equal(t, 7, retention.Daily)
73 assert.Equal(t, 4, retention.Weekly)
74 assert.Equal(t, 12, retention.Monthly)
75 })
76
77 t.Run("zero values", func(t *testing.T) {
78 var cfg Config
79 cfg.Backups.Databases.Retention.Hourly = 0
80 cfg.Backups.Databases.Retention.Daily = 0
81 cfg.Backups.Databases.Retention.Weekly = 0
82 cfg.Backups.Databases.Retention.Monthly = 0
83
84 retention, err := validateBackupRetention(cfg)
85 require.NoError(t, err)
86 assert.Equal(t, 0, retention.Hourly)
87 assert.Equal(t, 0, retention.Daily)
88 assert.Equal(t, 0, retention.Weekly)
89 assert.Equal(t, 0, retention.Monthly)
90 })
91}
92
93func TestValidateBackupRetentionAcceptsLegacyBackupsConfig(t *testing.T) {
94 var cfg Config
95 cfg.Backups.Retention.Daily = 3
96
97 retention, err := validateBackupRetention(cfg)
98
99 require.NoError(t, err)
100 assert.Equal(t, 3, retention.Daily)
101}
102
103func TestDatabaseBackupSettingsPreferEnabledLegacySchedule(t *testing.T) {
104 var cfg Config
105 cfg.Backups.Enabled = true
106 cfg.Backups.Schedule = string(domain.ScheduleHourly)
107 cfg.Backups.Databases.Schedule = string(domain.ScheduleDaily)
108
109 settings := databaseBackupSettings(cfg)
110
111 assert.True(t, settings.Enabled)
112 assert.Equal(t, string(domain.ScheduleHourly), settings.Schedule)
113}
114
115func TestValidateVolumeBackupConfig(t *testing.T) {
116 t.Run("disabled accepts defaults", func(t *testing.T) {
117 var cfg Config
118
119 volumeCfg, err := validateVolumeBackupConfig(cfg)
120 require.NoError(t, err)
121 assert.False(t, volumeCfg.Enabled)
122 assert.Equal(t, domain.VolumeBackupCompressionGzip, volumeCfg.Compression)
123 assert.Equal(t, 0, volumeCfg.Retention.Keep)
124 assert.Equal(t, 2, volumeCfg.MaxConcurrency)
125 assert.Equal(t, "alpine:3.20", volumeCfg.HelperImage)
126 assert.Equal(t, "gordon", volumeCfg.VolumePrefix)
127 })
128
129 t.Run("enabled requires bucket and region", func(t *testing.T) {
130 var cfg Config
131 cfg.Backups.Volumes.Enabled = true
132 cfg.Backups.Volumes.Retention.Keep = 14
133
134 _, err := validateVolumeBackupConfig(cfg)
135 require.Error(t, err)
136 assert.Contains(t, err.Error(), "backups.volumes.s3.bucket")
137
138 cfg.Backups.Volumes.S3.Bucket = "gordon-backups"
139 _, err = validateVolumeBackupConfig(cfg)
140 require.Error(t, err)
141 assert.Contains(t, err.Error(), "backups.volumes.s3.region")
142 })
143
144 t.Run("enabled accepts s3 config", func(t *testing.T) {
145 var cfg Config
146 cfg.Backups.Volumes.Enabled = true
147 cfg.Backups.Volumes.Interval = "6h"
148 cfg.Backups.Volumes.Timeout = "30m"
149 cfg.Backups.Volumes.Compression = "gzip"
150 cfg.Backups.Volumes.MaxConcurrency = 3
151 cfg.Backups.Volumes.HelperImage = "example/helper:1"
152 cfg.Volumes.Prefix = "acme"
153 cfg.Backups.Volumes.S3.Bucket = "gordon-backups"
154 cfg.Backups.Volumes.S3.Region = "eu-west-3"
155 cfg.Backups.Volumes.S3.Prefix = "prod/gordon"
156 cfg.Backups.Volumes.Retention.Keep = 7
157
158 volumeCfg, err := validateVolumeBackupConfig(cfg)
159 require.NoError(t, err)
160 assert.True(t, volumeCfg.Enabled)
161 assert.Equal(t, domain.VolumeBackupCompressionGzip, volumeCfg.Compression)
162 assert.Equal(t, 7, volumeCfg.Retention.Keep)
163 assert.Equal(t, 3, volumeCfg.MaxConcurrency)
164 assert.Equal(t, "acme", volumeCfg.VolumePrefix)
165 assert.Equal(t, "prod/gordon", volumeCfg.S3Prefix)
166 })
167
168 t.Run("storage class is normalized and passed through", func(t *testing.T) {
169 var cfg Config
170 cfg.Backups.Volumes.S3.StorageClass = " onezone_ia "
171
172 volumeCfg, err := validateVolumeBackupConfig(cfg)
173 require.NoError(t, err)
174 assert.Equal(t, "ONEZONE_IA", volumeCfg.S3StorageClass)
175 })
176
177 t.Run("empty storage class keeps the bucket default", func(t *testing.T) {
178 var cfg Config
179
180 volumeCfg, err := validateVolumeBackupConfig(cfg)
181 require.NoError(t, err)
182 assert.Empty(t, volumeCfg.S3StorageClass)
183 })
184
185 t.Run("storage class rejects unsafe values", func(t *testing.T) {
186 var cfg Config
187 cfg.Backups.Volumes.S3.StorageClass = "one zone"
188
189 _, err := validateVolumeBackupConfig(cfg)
190 require.Error(t, err)
191 assert.Contains(t, err.Error(), "backups.volumes.s3.storage_class")
192 })
193
194 t.Run("zstd requires helper image with zstd", func(t *testing.T) {
195 var cfg Config
196 cfg.Backups.Volumes.Compression = "zstd"
197
198 _, err := validateVolumeBackupConfig(cfg)
199
200 require.Error(t, err)
201 assert.Contains(t, err.Error(), "helper_image")
202 })
203
204 t.Run("enabled rejects zero retention", func(t *testing.T) {
205 var cfg Config
206 cfg.Backups.Volumes.Enabled = true
207 cfg.Backups.Volumes.S3.Bucket = "gordon-backups"
208 cfg.Backups.Volumes.S3.Region = "eu-west-3"
209 cfg.Backups.Volumes.Retention.Keep = 0
210
211 _, err := validateVolumeBackupConfig(cfg)
212 require.Error(t, err)
213 assert.Contains(t, err.Error(), "backups.volumes.retention.keep")
214 })
215}
216
217func TestLoadConfigSetsBackupDefaults(t *testing.T) {
218 v := viper.New()
219
220 require.NoError(t, loadConfig(v, ""))
221
222 assert.Equal(t, string(domain.ScheduleDaily), v.GetString("backups.databases.schedule"))
223 assert.Equal(t, "24h", v.GetString("backups.volumes.interval"))
224 assert.Equal(t, string(domain.VolumeBackupCompressionGzip), v.GetString("backups.volumes.compression"))
225 assert.Equal(t, 14, v.GetInt("backups.volumes.retention.keep"))
226}
227
228func TestResolveBackupSchedule(t *testing.T) {
229 t.Run("valid values", func(t *testing.T) {
230 tests := []struct {
231 input string
232 want domain.BackupSchedule
233 }{
234 {input: "hourly", want: domain.ScheduleHourly},
235 {input: "daily", want: domain.ScheduleDaily},
236 {input: "weekly", want: domain.ScheduleWeekly},
237 {input: "monthly", want: domain.ScheduleMonthly},
238 {input: " DAILY ", want: domain.ScheduleDaily},
239 }
240
241 for _, tt := range tests {
242 t.Run(tt.input, func(t *testing.T) {
243 got, err := resolveBackupSchedule(tt.input)
244 require.NoError(t, err)
245 assert.Equal(t, tt.want, got)
246 })
247 }
248 })
249
250 t.Run("invalid value", func(t *testing.T) {
251 _, err := resolveBackupSchedule("every-minute")
252 require.Error(t, err)
253 assert.Contains(t, err.Error(), "backups.databases.schedule")
254 })
255}