1// SPDX-License-Identifier: AGPL-3.0-or-later
2package kvstore_test
3
4import (
5 "context"
6 "sync"
7 "testing"
8
9 kv "bnema.dev/ermite-kv"
10 "github.com/stretchr/testify/require"
11
12 "bnema.dev/ermite/internal/domain"
13 "bnema.dev/ermite/internal/ports"
14)
15
16func TestReleaseStorage(t *testing.T) {
17 ctx := context.Background()
18 dir := t.TempDir()
19 s := openStore(t, dir)
20 repo := name(t, "release-test")
21 require.NoError(t, s.Create(ctx, domain.Repo{Name: repo, Visibility: domain.VisibilityPublic, CreatedAt: created, Settings: domain.DefaultRepoSettings()}))
22 st := s.Releases()
23 id, err := st.NextReleaseID(ctx)
24 require.NoError(t, err)
25 r := domain.Release{ID: id, Repo: repo, Tag: "v1", Title: "title", Notes: "notes", MakeLatest: true, TargetCommitish: "abc", Author: "owner", CreatedAt: created, PublishedAt: created, UpdatedAt: created}
26 assetID, err := st.NextReleaseID(ctx)
27 require.NoError(t, err)
28 a := domain.ReleaseAsset{ID: assetID, ReleaseID: id, Repo: repo, Name: "binary", Label: "label", ContentType: "application/octet-stream", Size: 3, Digest: "sha256:abc", State: domain.ReleaseAssetUploading, Uploader: "owner", Charged: 3, MultipartID: "multipart", CreatedAt: created, UpdatedAt: created}
29 w := ports.ReleaseWrite{Repo: repo, Used: 3, PutReleases: []domain.Release{r}, PutAssets: []domain.ReleaseAsset{a}}
30 require.NoError(t, st.ApplyReleases(ctx, w))
31 got, err := st.GetReleaseByTag(ctx, repo, "v1")
32 require.NoError(t, err)
33 r.Version = got.Version
34 require.Equal(t, r, got)
35 ar, err := st.GetAssetByName(ctx, repo, id, "binary")
36 require.NoError(t, err)
37 a.Version = ar.Version
38 require.Equal(t, a, ar)
39 rr, err := st.ReleaseRepo(ctx, id)
40 require.NoError(t, err)
41 require.Equal(t, repo, rr)
42 rs, err := st.ListReleases(ctx, repo)
43 require.NoError(t, err)
44 require.Equal(t, []domain.Release{r}, rs)
45 as, err := st.ListAssets(ctx, repo, id)
46 require.NoError(t, err)
47 require.Equal(t, []domain.ReleaseAsset{a}, as)
48 all, err := st.AllAssets(ctx)
49 require.NoError(t, err)
50 require.Equal(t, as, all)
51 require.ErrorIs(t, st.ApplyReleases(ctx, w), domain.ErrConflict)
52 used, version, err := st.ReleaseUsage(ctx, repo)
53 require.NoError(t, err)
54 require.EqualValues(t, 3, used)
55 duplicate := r
56 duplicate.ID += 10
57 duplicate.Version = 0
58 require.ErrorIs(t, st.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, UsageVersion: version, PutReleases: []domain.Release{duplicate}}), domain.ErrAlreadyExists)
59 da := a
60 da.ID += 10
61 da.Version = 0
62 require.ErrorIs(t, st.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, UsageVersion: version, PutAssets: []domain.ReleaseAsset{da}}), domain.ErrAlreadyExists)
63 stale := r
64 stale.Version++
65 require.ErrorIs(t, st.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, UsageVersion: version, PutReleases: []domain.Release{stale}}), domain.ErrConflict)
66 staleAsset := a
67 staleAsset.Version++
68 require.ErrorIs(t, st.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, UsageVersion: version, DeleteAssets: []domain.ReleaseAsset{staleAsset}}), domain.ErrConflict)
69 r.Tag = "v2"
70 a.Name = "renamed"
71 require.NoError(t, st.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, UsageVersion: version, Used: 3, PutReleases: []domain.Release{r}, PutAssets: []domain.ReleaseAsset{a}}))
72 _, err = st.GetReleaseByTag(ctx, repo, "v1")
73 require.ErrorIs(t, err, domain.ErrNotFound)
74 _, err = st.GetAssetByName(ctx, repo, id, "binary")
75 require.ErrorIs(t, err, domain.ErrNotFound)
76 renamedRelease, err := st.GetReleaseByTag(ctx, repo, "v2")
77 require.NoError(t, err)
78 renamedAsset, err := st.GetAssetByName(ctx, repo, id, "renamed")
79 require.NoError(t, err)
80 _, version, err = st.ReleaseUsage(ctx, repo)
81 require.NoError(t, err)
82 require.NoError(t, st.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, UsageVersion: version, DeleteReleases: []domain.Release{renamedRelease}, DeleteAssets: []domain.ReleaseAsset{renamedAsset}}))
83 _, err = st.GetRelease(ctx, repo, id)
84 require.ErrorIs(t, err, domain.ErrNotFound)
85 _, err = st.GetAsset(ctx, a.ID)
86 require.ErrorIs(t, err, domain.ErrNotFound)
87 _, version, err = st.ReleaseUsage(ctx, repo)
88 require.NoError(t, err)
89 r.Version, a.Version = 0, 0
90 require.NoError(t, st.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, UsageVersion: version, Used: 3, PutReleases: []domain.Release{r}, PutAssets: []domain.ReleaseAsset{a}}))
91 require.NoError(t, s.Delete(ctx, repo))
92 _, err = st.GetRelease(ctx, repo, id)
93 require.ErrorIs(t, err, domain.ErrNotFound)
94 _, err = st.ReleaseRepo(ctx, id)
95 require.ErrorIs(t, err, domain.ErrNotFound)
96 _, err = st.GetAsset(ctx, a.ID)
97 require.ErrorIs(t, err, domain.ErrNotFound)
98 all, err = st.AllAssets(ctx)
99 require.NoError(t, err)
100 require.Empty(t, all)
101 used, version, err = st.ReleaseUsage(ctx, repo)
102 require.NoError(t, err)
103 require.Zero(t, used)
104 require.Zero(t, version)
105 require.ErrorIs(t, st.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo}), domain.ErrNotFound)
106 require.NoError(t, s.Close(ctx))
107 db, err := kv.Open(ctx, dir, kv.Options{})
108 require.NoError(t, err)
109 defer func() { _ = db.Close(ctx) }()
110 for _, prefix := range []string{"rel/", "reltag/", "relid/", "relasset/", "relassets/", "reluse/"} {
111 for key := range db.Snapshot(ctx).Prefix(ctx, []byte(prefix)) {
112 t.Errorf("repository deletion left release key %q", key)
113 }
114 }
115}
116func TestConcurrentReleaseIDs(t *testing.T) {
117 st := openStore(t, t.TempDir()).Releases()
118 var wg sync.WaitGroup
119 ids := make(chan int64, 32)
120 for range 32 {
121 wg.Go(func() { id, err := st.NextReleaseID(context.Background()); require.NoError(t, err); ids <- id })
122 }
123 wg.Wait()
124 close(ids)
125 seen := map[int64]bool{}
126 for id := range ids {
127 require.False(t, seen[id])
128 seen[id] = true
129 }
130 require.Len(t, seen, 32)
131}