v3.3.2 9697f9d

backup_storage_test.go RawHistoryFolder

1package filesystem
2
3import (
4	"bytes"
5	"context"
6	"io"
7	"os"
8	"path/filepath"
9	"testing"
10	"time"
11
12	"github.com/bnema/gordon/internal/domain"
13	"github.com/stretchr/testify/assert"
14	"github.com/stretchr/testify/require"
15)
16
17func TestBackupStorage_StoreAndGet(t *testing.T) {
18	storage, err := NewBackupStorage(t.TempDir(), testLogger())
19	require.NoError(t, err)
20
21	now := time.Date(2026, 2, 7, 11, 0, 0, 0, time.UTC)
22	payload := []byte("backup-content")
23	path, err := storage.Store(context.Background(), "app.example.com", "postgres", "postgres", domain.ScheduleDaily, now, bytes.NewReader(payload))
24	require.NoError(t, err)
25
26	rc, err := storage.Get(context.Background(), path)
27	require.NoError(t, err)
28	defer rc.Close()
29
30	data, err := io.ReadAll(rc)
31	require.NoError(t, err)
32	assert.Equal(t, payload, data)
33}
34
35func TestBackupStorage_StoreSameSecondBackupsUseDistinctFinalAndTempPaths(t *testing.T) {
36	rootDir := t.TempDir()
37	storage, err := NewBackupStorage(rootDir, testLogger())
38	require.NoError(t, err)
39
40	now := time.Date(2026, 2, 7, 11, 0, 0, 0, time.UTC)
41	firstPayload := []byte("first-backup")
42	secondPayload := []byte("second-backup")
43
44	firstPath, err := storage.Store(context.Background(), "app.example.com", "postgres", "postgres", domain.ScheduleDaily, now, bytes.NewReader(firstPayload))
45	require.NoError(t, err)
46	secondPath, err := storage.Store(context.Background(), "app.example.com", "postgres", "postgres", domain.ScheduleDaily, now, bytes.NewReader(secondPayload))
47	require.NoError(t, err)
48
49	assert.NotEqual(t, firstPath, secondPath)
50	assert.NoFileExists(t, firstPath+".tmp")
51	assert.NoFileExists(t, secondPath+".tmp")
52
53	firstRead, err := storage.Get(context.Background(), firstPath)
54	require.NoError(t, err)
55	defer firstRead.Close()
56	firstData, err := io.ReadAll(firstRead)
57	require.NoError(t, err)
58	assert.Equal(t, firstPayload, firstData)
59
60	secondRead, err := storage.Get(context.Background(), secondPath)
61	require.NoError(t, err)
62	defer secondRead.Close()
63	secondData, err := io.ReadAll(secondRead)
64	require.NoError(t, err)
65	assert.Equal(t, secondPayload, secondData)
66}
67
68func TestBackupStorage_PreservesCanonicalIdentityAndSeparatesSameNamedDatabases(t *testing.T) {
69	storage, err := NewBackupStorage(t.TempDir(), testLogger())
70	require.NoError(t, err)
71
72	started := time.Date(2026, 9, 12, 12, 0, 0, 0, time.UTC)
73	first, err := storage.Store(context.Background(), "shop", "api", "main", domain.ScheduleDaily, started, bytes.NewReader([]byte("api")))
74	require.NoError(t, err)
75	second, err := storage.Store(context.Background(), "shop", "worker", "main", domain.ScheduleDaily, started, bytes.NewReader([]byte("worker")))
76	require.NoError(t, err)
77	assert.NotEqual(t, first, second)
78
79	jobs, err := storage.List(context.Background(), "shop", nil)
80	require.NoError(t, err)
81	require.Len(t, jobs, 2)
82	assert.ElementsMatch(t, []string{"api", "worker"}, []string{jobs[0].Service, jobs[1].Service})
83	for _, job := range jobs {
84		assert.Equal(t, "shop", job.App)
85		assert.Equal(t, "main", job.DBName)
86	}
87}
88
89func TestBackupStorage_DoesNotMergePreCutoverRecords(t *testing.T) {
90	root := t.TempDir()
91	storage, err := NewBackupStorage(root, testLogger())
92	require.NoError(t, err)
93
94	legacyDir := filepath.Join(root, "shop", "main", "daily")
95	require.NoError(t, os.MkdirAll(legacyDir, 0750))
96	require.NoError(t, os.WriteFile(filepath.Join(legacyDir, "20260912T120000Z.bak"), []byte("legacy"), 0600))
97	_, err = storage.Store(context.Background(), "shop", "api", "main", domain.ScheduleDaily, time.Now(), bytes.NewReader([]byte("current")))
98	require.NoError(t, err)
99
100	jobs, err := storage.List(context.Background(), "shop", nil)
101	require.NoError(t, err)
102	require.Len(t, jobs, 1)
103	assert.Equal(t, "api", jobs[0].Service)
104}
105
106func TestBackupStorage_ListWithScheduleFilter(t *testing.T) {
107	storage, err := NewBackupStorage(t.TempDir(), testLogger())
108	require.NoError(t, err)
109
110	base := time.Date(2026, 2, 7, 10, 0, 0, 0, time.UTC)
111	_, err = storage.Store(context.Background(), "app.example.com", "postgres", "postgres", domain.ScheduleDaily, base, bytes.NewReader([]byte("d1")))
112	require.NoError(t, err)
113	_, err = storage.Store(context.Background(), "app.example.com", "postgres", "postgres", domain.ScheduleDaily, base.Add(time.Hour), bytes.NewReader([]byte("d2")))
114	require.NoError(t, err)
115	_, err = storage.Store(context.Background(), "app.example.com", "postgres", "postgres", domain.ScheduleWeekly, base.Add(2*time.Hour), bytes.NewReader([]byte("w1")))
116	require.NoError(t, err)
117
118	schedule := domain.ScheduleDaily
119	jobs, err := storage.List(context.Background(), "app.example.com", &schedule)
120	require.NoError(t, err)
121	require.Len(t, jobs, 2)
122	assert.Equal(t, domain.ScheduleDaily, jobs[0].Schedule)
123	assert.Equal(t, domain.ScheduleDaily, jobs[1].Schedule)
124	assert.True(t, jobs[0].StartedAt.After(jobs[1].StartedAt))
125}
126
127func TestBackupStorage_Delete(t *testing.T) {
128	storage, err := NewBackupStorage(t.TempDir(), testLogger())
129	require.NoError(t, err)
130
131	path, err := storage.Store(context.Background(), "app.example.com", "postgres", "postgres", domain.ScheduleDaily, time.Now().UTC(), bytes.NewReader([]byte("data")))
132	require.NoError(t, err)
133
134	err = storage.Delete(context.Background(), path)
135	require.NoError(t, err)
136
137	_, err = storage.Get(context.Background(), path)
138	assert.Error(t, err)
139}
140
141func TestBackupStorage_ApplyRetention(t *testing.T) {
142	storage, err := NewBackupStorage(t.TempDir(), testLogger())
143	require.NoError(t, err)
144
145	base := time.Date(2026, 2, 7, 6, 0, 0, 0, time.UTC)
146	for i := range 4 {
147		_, err := storage.Store(context.Background(), "app.example.com", "postgres", "postgres", domain.ScheduleDaily, base.Add(time.Duration(i)*time.Hour), bytes.NewReader([]byte("data")))
148		require.NoError(t, err)
149	}
150
151	deleted, err := storage.ApplyRetention(context.Background(), "app.example.com", domain.RetentionPolicy{Daily: 2})
152	require.NoError(t, err)
153	assert.Equal(t, 2, deleted)
154
155	schedule := domain.ScheduleDaily
156	jobs, err := storage.List(context.Background(), "app.example.com", &schedule)
157	require.NoError(t, err)
158	assert.Len(t, jobs, 2)
159}
160
161func TestBackupStorage_StoreSanitizesDotOnlyPathComponents(t *testing.T) {
162	storage, err := NewBackupStorage(t.TempDir(), testLogger())
163	require.NoError(t, err)
164
165	path, err := storage.Store(context.Background(), "..", "service", "...", domain.ScheduleDaily, time.Now().UTC(), bytes.NewReader([]byte("data")))
166	require.NoError(t, err)
167	assert.NotContains(t, path, "..")
168}
169
170func TestBackupStorage_StoreSanitizesSchedulePathComponent(t *testing.T) {
171	rootDir := t.TempDir()
172	storage, err := NewBackupStorage(rootDir, testLogger())
173	require.NoError(t, err)
174
175	path, err := storage.Store(context.Background(), "app.example.com", "postgres", "postgres", domain.BackupSchedule("../../escape"), time.Now().UTC(), bytes.NewReader([]byte("data")))
176	require.NoError(t, err)
177
178	rel, err := filepath.Rel(rootDir, path)
179	require.NoError(t, err)
180	assert.NotEqual(t, "..", rel)
181	assert.NotContains(t, rel, "../")
182	assert.NotContains(t, rel, "..\\")
183}

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