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