1// SPDX-License-Identifier: AGPL-3.0-or-later
2package kvstore
3
4import (
5 "context"
6 "testing"
7 "time"
8
9 kv "bnema.dev/ermite-kv"
10 "bnema.dev/ermite/internal/domain"
11 "bnema.dev/ermite/internal/ports"
12 "github.com/stretchr/testify/require"
13)
14
15func TestReleaseLatestIndexBackfill(t *testing.T) {
16 ctx := t.Context()
17 dir := t.TempDir()
18 s, err := Open(ctx, dir)
19 require.NoError(t, err)
20 var repos []domain.RepoName
21 for _, raw := range []string{"backfill", "empty", "newest"} {
22 repo, err := domain.ParseRepoName(raw)
23 require.NoError(t, err)
24 repos = append(repos, repo)
25 }
26 for _, repo := range repos {
27 require.NoError(t, s.Create(ctx, domain.Repo{Name: repo, Visibility: domain.VisibilityPublic}))
28 }
29 var b kv.Batch
30 for i, repo := range repos {
31 for j := 1; j <= 5; j++ {
32 rel := domain.Release{ID: int64(i*10 + j), Repo: repo, Tag: string(rune('a' + j)), CreatedAt: time.Unix(int64(j), 0)}
33 rel.Draft = j == 5 || i == 1
34 rel.Prerelease = j == 4
35 rel.MakeLatest = j == 1 && i == 0
36 b.Put(relKey(repo, rel.ID), encodeRelease(rel))
37 }
38 // A stale key must also be removed for a repo with no eligible release.
39 b.Put(relLatestKey(repo), []byte("999"))
40 }
41 b.Delete([]byte(releaseLatestIndexMarker))
42 _, err = s.db.Apply(ctx, &b)
43 require.NoError(t, err)
44 require.NoError(t, s.Close(context.Background()))
45 s, err = Open(ctx, dir)
46 require.NoError(t, err)
47 defer func() { require.NoError(t, s.Close(context.Background())) }()
48 for i, want := range []int64{1, 0, 23} {
49 got, err := s.Releases().GetLatestRelease(ctx, repos[i])
50 if want == 0 {
51 require.ErrorIs(t, err, domain.ErrNotFound)
52 } else {
53 require.NoError(t, err)
54 require.Equal(t, want, got.ID)
55 }
56 }
57 require.NoError(t, s.buildReleaseLatestIndex(ctx))
58 got, err := s.Releases().GetLatestRelease(ctx, repos[0])
59 require.NoError(t, err)
60 require.EqualValues(t, 1, got.ID)
61 require.NoError(t, s.Delete(ctx, repos[0]))
62 _, err = s.Releases().GetLatestRelease(ctx, repos[0])
63 require.ErrorIs(t, err, domain.ErrNotFound)
64}
65
66func TestGetLatestReleaseAtomicUpdate(t *testing.T) {
67 ctx := t.Context()
68 s, err := Open(ctx, t.TempDir())
69 require.NoError(t, err)
70 defer func() { require.NoError(t, s.Close(context.Background())) }()
71 repo, err := domain.ParseRepoName("atomic")
72 require.NoError(t, err)
73 require.NoError(t, s.Create(ctx, domain.Repo{Name: repo, Visibility: domain.VisibilityPublic}))
74 a := domain.Release{ID: 1, Repo: repo, Tag: "a"}
75 b := domain.Release{ID: 2, Repo: repo, Tag: "b"}
76 store := s.Releases()
77 require.NoError(t, store.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, PutReleases: []domain.Release{a, b}, SetLatest: true, LatestID: a.ID}))
78 a, err = store.GetLatestRelease(ctx, repo)
79 require.NoError(t, err)
80 require.EqualValues(t, 1, a.ID)
81 _, version, err := store.ReleaseUsage(ctx, repo)
82 require.NoError(t, err)
83 a.Prerelease = true
84 require.NoError(t, store.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, UsageVersion: version, PutReleases: []domain.Release{a}, SetLatest: true, LatestID: b.ID}))
85 got, err := store.GetLatestRelease(ctx, repo)
86 require.NoError(t, err)
87 require.EqualValues(t, 2, got.ID)
88 // An asset-only/usage write leaves the index untouched, including its version.
89 before, ok := s.db.Get(ctx, relLatestKey(repo))
90 require.True(t, ok)
91 _, version, err = store.ReleaseUsage(ctx, repo)
92 require.NoError(t, err)
93 require.NoError(t, store.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, UsageVersion: version, Used: 10}))
94 after, ok := s.db.Get(ctx, relLatestKey(repo))
95 require.True(t, ok)
96 require.Equal(t, before, after)
97}