main bac21c6

run_backup_test.go RawHistoryFolder

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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close