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}