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