1package repository
2
3import (
4 "math/rand"
5 "os"
6 "testing"
7
8 "github.com/ProtonMail/go-crypto/openpgp"
9 "github.com/stretchr/testify/require"
10
11 "github.com/git-bug/git-bug/util/lamport"
12)
13
14type RepoCreator func(t testing.TB, bare bool) TestedRepo
15
16// Test suite for a Repo implementation
17func RepoTest(t *testing.T, creator RepoCreator) {
18 for bare, name := range map[bool]string{
19 false: "Plain",
20 true: "Bare",
21 } {
22 t.Run(name, func(t *testing.T) {
23 repo := creator(t, bare)
24
25 t.Run("Data", func(t *testing.T) {
26 RepoDataTest(t, repo)
27 RepoDataSignatureTest(t, repo)
28 })
29
30 t.Run("Config", func(t *testing.T) {
31 RepoConfigTest(t, repo)
32 })
33
34 t.Run("Storage", func(t *testing.T) {
35 RepoStorageTest(t, repo)
36 })
37
38 t.Run("Index", func(t *testing.T) {
39 RepoIndexTest(t, repo)
40 })
41
42 t.Run("Clocks", func(t *testing.T) {
43 RepoClockTest(t, repo)
44 })
45 })
46 }
47}
48
49// helper to test a RepoConfig
50func RepoConfigTest(t *testing.T, repo RepoConfig) {
51 testConfig(t, repo.LocalConfig())
52}
53
54func RepoStorageTest(t *testing.T, repo RepoStorage) {
55 storage := repo.LocalStorage()
56
57 err := storage.MkdirAll("foo/bar", 0755)
58 require.NoError(t, err)
59
60 f, err := storage.Create("foo/bar/foofoo")
61 require.NoError(t, err)
62
63 _, err = f.Write([]byte("hello"))
64 require.NoError(t, err)
65
66 err = f.Close()
67 require.NoError(t, err)
68
69 // remove all
70 err = storage.RemoveAll(".")
71 require.NoError(t, err)
72
73 fi, err := storage.ReadDir(".")
74 // a real FS would remove the root directory with RemoveAll and subsequent call would fail
75 // a memory FS would still have a virtual root and subsequent call would succeed
76 // not ideal, but will do for now
77 if err == nil {
78 require.Empty(t, fi)
79 } else {
80 require.True(t, os.IsNotExist(err))
81 }
82}
83
84func randomHash() Hash {
85 var letterRunes = "abcdef0123456789"
86 b := make([]byte, idLengthSHA256)
87 for i := range b {
88 b[i] = letterRunes[rand.Intn(len(letterRunes))]
89 }
90 return Hash(b)
91}
92
93// helper to test a RepoData
94func RepoDataTest(t *testing.T, repo RepoData) {
95 // Blob
96
97 data := randomData()
98
99 blobHash1, err := repo.StoreData(data)
100 require.NoError(t, err)
101 require.True(t, blobHash1.IsValid())
102
103 blob1Read, err := repo.ReadData(blobHash1)
104 require.NoError(t, err)
105 require.Equal(t, data, blob1Read)
106
107 _, err = repo.ReadData(randomHash())
108 require.ErrorIs(t, err, ErrNotFound)
109
110 // Tree
111
112 blobHash2, err := repo.StoreData(randomData())
113 require.NoError(t, err)
114 blobHash3, err := repo.StoreData(randomData())
115 require.NoError(t, err)
116
117 tree1 := []TreeEntry{
118 {
119 ObjectType: Blob,
120 Hash: blobHash1,
121 Name: "blob1",
122 },
123 {
124 ObjectType: Blob,
125 Hash: blobHash2,
126 Name: "blob2",
127 },
128 }
129
130 treeHash1, err := repo.StoreTree(tree1)
131 require.NoError(t, err)
132 require.True(t, treeHash1.IsValid())
133
134 tree1Read, err := repo.ReadTree(treeHash1)
135 require.NoError(t, err)
136 require.ElementsMatch(t, tree1, tree1Read)
137
138 tree2 := []TreeEntry{
139 {
140 ObjectType: Tree,
141 Hash: treeHash1,
142 Name: "tree1",
143 },
144 {
145 ObjectType: Blob,
146 Hash: blobHash3,
147 Name: "blob3",
148 },
149 }
150
151 treeHash2, err := repo.StoreTree(tree2)
152 require.NoError(t, err)
153 require.True(t, treeHash2.IsValid())
154
155 tree2Read, err := repo.ReadTree(treeHash2)
156 require.NoError(t, err)
157 require.ElementsMatch(t, tree2, tree2Read)
158
159 _, err = repo.ReadTree(randomHash())
160 require.ErrorIs(t, err, ErrNotFound)
161
162 // Commit
163
164 commit1, err := repo.StoreCommit(treeHash1)
165 require.NoError(t, err)
166 require.True(t, commit1.IsValid())
167
168 // commit with a parent
169 commit2, err := repo.StoreCommit(treeHash2, commit1)
170 require.NoError(t, err)
171 require.True(t, commit2.IsValid())
172
173 // ReadTree should accept tree and commit hashes
174 tree1read, err := repo.ReadTree(commit1)
175 require.NoError(t, err)
176 require.Equal(t, tree1read, tree1)
177
178 c2, err := repo.ReadCommit(commit2)
179 require.NoError(t, err)
180 c2expected := Commit{Hash: commit2, Parents: []Hash{commit1}, TreeHash: treeHash2}
181 require.Equal(t, c2expected, c2)
182
183 _, err = repo.ReadCommit(randomHash())
184 require.ErrorIs(t, err, ErrNotFound)
185
186 // Ref
187
188 exist1, err := repo.RefExist("refs/bugs/ref1")
189 require.NoError(t, err)
190 require.False(t, exist1)
191
192 err = repo.UpdateRef("refs/bugs/ref1", commit2)
193 require.NoError(t, err)
194
195 exist1, err = repo.RefExist("refs/bugs/ref1")
196 require.NoError(t, err)
197 require.True(t, exist1)
198
199 h, err := repo.ResolveRef("refs/bugs/ref1")
200 require.NoError(t, err)
201 require.Equal(t, commit2, h)
202
203 ls, err := repo.ListRefs("refs/bugs")
204 require.NoError(t, err)
205 require.ElementsMatch(t, []string{"refs/bugs/ref1"}, ls)
206
207 err = repo.CopyRef("refs/bugs/ref1", "refs/bugs/ref2")
208 require.NoError(t, err)
209
210 ls, err = repo.ListRefs("refs/bugs")
211 require.NoError(t, err)
212 require.ElementsMatch(t, []string{"refs/bugs/ref1", "refs/bugs/ref2"}, ls)
213
214 commits, err := repo.ListCommits("refs/bugs/ref2")
215 require.NoError(t, err)
216 require.Equal(t, []Hash{commit1, commit2}, commits)
217
218 _, err = repo.ResolveRef("/refs/bugs/refnotexist")
219 require.ErrorIs(t, err, ErrNotFound)
220
221 err = repo.CopyRef("/refs/bugs/refnotexist", "refs/foo")
222 require.ErrorIs(t, err, ErrNotFound)
223
224 // Cleanup
225
226 err = repo.RemoveRef("refs/bugs/ref1")
227 require.NoError(t, err)
228
229 // RemoveRef is idempotent
230 err = repo.RemoveRef("refs/bugs/ref1")
231 require.NoError(t, err)
232}
233
234func RepoDataSignatureTest(t *testing.T, repo RepoData) {
235 data := randomData()
236
237 blobHash, err := repo.StoreData(data)
238 require.NoError(t, err)
239
240 treeHash, err := repo.StoreTree([]TreeEntry{
241 {
242 ObjectType: Blob,
243 Hash: blobHash,
244 Name: "blob",
245 },
246 })
247 require.NoError(t, err)
248
249 pgpEntity1, err := openpgp.NewEntity("", "", "", nil)
250 require.NoError(t, err)
251 keyring1 := openpgp.EntityList{pgpEntity1}
252
253 pgpEntity2, err := openpgp.NewEntity("", "", "", nil)
254 require.NoError(t, err)
255 keyring2 := openpgp.EntityList{pgpEntity2}
256
257 commitHash1, err := repo.StoreSignedCommit(treeHash, pgpEntity1)
258 require.NoError(t, err)
259
260 commit1, err := repo.ReadCommit(commitHash1)
261 require.NoError(t, err)
262
263 _, err = openpgp.CheckDetachedSignature(keyring1, commit1.SignedData, commit1.Signature, nil)
264 require.NoError(t, err)
265
266 _, err = openpgp.CheckDetachedSignature(keyring2, commit1.SignedData, commit1.Signature, nil)
267 require.Error(t, err)
268
269 commitHash2, err := repo.StoreSignedCommit(treeHash, pgpEntity1, commitHash1)
270 require.NoError(t, err)
271
272 commit2, err := repo.ReadCommit(commitHash2)
273 require.NoError(t, err)
274
275 _, err = openpgp.CheckDetachedSignature(keyring1, commit2.SignedData, commit2.Signature, nil)
276 require.NoError(t, err)
277
278 _, err = openpgp.CheckDetachedSignature(keyring2, commit2.SignedData, commit2.Signature, nil)
279 require.Error(t, err)
280}
281
282func RepoIndexTest(t *testing.T, repo RepoIndex) {
283 idx, err := repo.GetIndex("a")
284 require.NoError(t, err)
285
286 // simple indexing
287 err = idx.IndexOne("id1", []string{"foo", "bar", "foobar barfoo"})
288 require.NoError(t, err)
289
290 // batched indexing
291 indexer, closer := idx.IndexBatch()
292 err = indexer("id2", []string{"hello", "foo bar"})
293 require.NoError(t, err)
294 err = indexer("id3", []string{"Hola", "Esta bien"})
295 require.NoError(t, err)
296 err = closer()
297 require.NoError(t, err)
298
299 // search
300 res, err := idx.Search([]string{"foobar"})
301 require.NoError(t, err)
302 require.ElementsMatch(t, []string{"id1"}, res)
303
304 res, err = idx.Search([]string{"foo"})
305 require.NoError(t, err)
306 require.ElementsMatch(t, []string{"id1", "id2"}, res)
307
308 // re-indexing an item replace previous versions
309 err = idx.IndexOne("id2", []string{"hello"})
310 require.NoError(t, err)
311
312 res, err = idx.Search([]string{"foo"})
313 require.NoError(t, err)
314 require.ElementsMatch(t, []string{"id1"}, res)
315
316 err = idx.Clear()
317 require.NoError(t, err)
318
319 res, err = idx.Search([]string{"foo"})
320 require.NoError(t, err)
321 require.Empty(t, res)
322}
323
324// helper to test a RepoClock
325func RepoClockTest(t *testing.T, repo RepoClock) {
326 allClocks, err := repo.AllClocks()
327 require.NoError(t, err)
328 require.Len(t, allClocks, 0)
329
330 clock, err := repo.GetOrCreateClock("foo")
331 require.NoError(t, err)
332 require.Equal(t, lamport.Time(1), clock.Time())
333
334 time, err := clock.Increment()
335 require.NoError(t, err)
336 require.Equal(t, lamport.Time(2), time)
337 require.Equal(t, lamport.Time(2), clock.Time())
338
339 clock2, err := repo.GetOrCreateClock("foo")
340 require.NoError(t, err)
341 require.Equal(t, lamport.Time(2), clock2.Time())
342
343 clock3, err := repo.GetOrCreateClock("bar")
344 require.NoError(t, err)
345 require.Equal(t, lamport.Time(1), clock3.Time())
346
347 allClocks, err = repo.AllClocks()
348 require.NoError(t, err)
349 require.Equal(t, map[string]lamport.Clock{
350 "foo": clock,
351 "bar": clock3,
352 }, allClocks)
353}
354
355func randomData() []byte {
356 var letterRunes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
357 b := make([]byte, 32)
358 for i := range b {
359 b[i] = letterRunes[rand.Intn(len(letterRunes))]
360 }
361 return b
362}