1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package kvstore_test
4
5import (
6 "errors"
7 "testing"
8 "time"
9
10 "github.com/stretchr/testify/require"
11
12 "bnema.dev/ermite/internal/domain"
13)
14
15func TestRepoMutateInterleavedWrites(t *testing.T) {
16 ctx := t.Context()
17 s := openStore(t, t.TempDir())
18 n := name(t, "proj")
19 original := domain.Repo{Name: n, Visibility: domain.VisibilityPublic, CreatedAt: created}
20 require.NoError(t, s.Create(ctx, original))
21 later := created.Add(2 * time.Hour)
22 calls := 0
23 got, err := s.Mutate(ctx, n, func(r *domain.Repo) (bool, error) {
24 calls++
25 // Test-only interleaving: inject a committed write after the outer read.
26 if calls == 1 {
27 _, err := s.Mutate(ctx, n, func(current *domain.Repo) (bool, error) {
28 current.Visibility = domain.VisibilityPrivate
29 current.ActiveAt = later
30 current.PushedAt = later
31 return true, nil
32 })
33 require.NoError(t, err)
34 }
35 r.Description = "updated"
36 at := created.Add(time.Hour)
37 if at.After(r.ActiveAt) {
38 r.ActiveAt = at
39 }
40 if at.After(r.PushedAt) {
41 r.PushedAt = at
42 }
43 return true, nil
44 })
45 require.NoError(t, err)
46 require.Equal(t, 2, calls)
47 require.Equal(t, domain.VisibilityPrivate, got.Visibility)
48 require.Equal(t, "updated", got.Description)
49 require.Equal(t, later, got.ActiveAt)
50 require.Equal(t, later, got.PushedAt)
51 stored, err := s.Get(ctx, n)
52 require.NoError(t, err)
53 require.Equal(t, got, stored)
54}
55
56func TestRepoMutateContracts(t *testing.T) {
57 ctx := t.Context()
58 s := openStore(t, t.TempDir())
59 n := name(t, "proj")
60 require.NoError(t, s.Create(ctx, domain.Repo{Name: n, Visibility: domain.VisibilityPublic, CreatedAt: created}))
61 original, err := s.Get(ctx, n)
62 require.NoError(t, err)
63 boom := errors.New("callback failed")
64 _, err = s.Mutate(ctx, n, func(r *domain.Repo) (bool, error) { r.Description = "discard"; return true, boom })
65 require.ErrorIs(t, err, boom)
66 got, err := s.Mutate(ctx, n, func(r *domain.Repo) (bool, error) { r.Description = "discard"; return false, nil })
67 require.NoError(t, err)
68 require.Equal(t, original, got)
69 for _, changed := range []bool{false, true} {
70 _, err = s.Mutate(ctx, n, func(r *domain.Repo) (bool, error) { r.Name = name(t, "other"); return changed, nil })
71 require.ErrorIs(t, err, domain.ErrInvalidName)
72 }
73 got, err = s.Get(ctx, n)
74 require.NoError(t, err)
75 require.Equal(t, original, got)
76 calls := 0
77 _, err = s.Mutate(ctx, n, func(r *domain.Repo) (bool, error) {
78 calls++
79 _, err := s.Mutate(ctx, n, func(current *domain.Repo) (bool, error) { current.Description = "concurrent"; return true, nil })
80 require.NoError(t, err)
81 return true, nil
82 })
83 require.ErrorIs(t, err, domain.ErrConflict)
84 require.Equal(t, 8, calls)
85 _, err = s.Mutate(ctx, n, func(r *domain.Repo) (bool, error) { require.NoError(t, s.Delete(ctx, n)); return true, nil })
86 require.ErrorIs(t, err, domain.ErrNotFound)
87}