1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package app_test
4
5import (
6 "context"
7 "errors"
8 "os"
9 "os/exec"
10 "path/filepath"
11 "sync"
12 "testing"
13 "time"
14
15 "github.com/stretchr/testify/assert"
16 "github.com/stretchr/testify/require"
17
18 "bnema.dev/ermite/internal/adapters/eventbus"
19 "bnema.dev/ermite/internal/adapters/gitcli"
20 "bnema.dev/ermite/internal/adapters/kvstore"
21 "bnema.dev/ermite/internal/app"
22 "bnema.dev/ermite/internal/domain"
23)
24
25type fixedClock struct{}
26
27func (fixedClock) Now() time.Time { return now }
28
29// Concurrent Create of one name over the real adapters: one wins, the others
30// get ErrAlreadyExists and leave nothing behind.
31func TestCreateConcurrentSameNameRealAdapters(t *testing.T) {
32 ctx := context.Background()
33 bin, err := exec.LookPath("git")
34 if err != nil {
35 unusable(t, "git not found")
36 }
37 data := t.TempDir()
38 for _, d := range []string{"repos/.staging", "trash", "run/git-home", "kv"} {
39 require.NoError(t, os.MkdirAll(filepath.Join(data, d), 0o700))
40 }
41 git := gitcli.New(gitcli.Config{Bin: bin, DataDir: data, Path: os.Getenv("PATH")})
42 if _, err := git.CheckVersion(ctx); err != nil {
43 unusable(t, "git is not usable by ermite: %v", err)
44 }
45 store, err := kvstore.Open(ctx, filepath.Join(data, "kv"))
46 require.NoError(t, err)
47 t.Cleanup(func() { require.NoError(t, store.Close(context.Background())) })
48
49 repos := app.NewRepos(store, git, eventbus.New(), fixedClock{})
50 n := name(t, "tools")
51
52 const workers = 8
53 errs := make(chan error, workers)
54 var wg sync.WaitGroup
55 for range workers {
56 wg.Add(1)
57 go func() {
58 defer wg.Done()
59 _, err := repos.Create(ctx, domain.Root(), app.CreateParams{Name: n})
60 errs <- err
61 }()
62 }
63 wg.Wait()
64 close(errs)
65
66 ok, exists := 0, 0
67 for err := range errs {
68 switch {
69 case err == nil:
70 ok++
71 case errors.Is(err, domain.ErrAlreadyExists):
72 exists++
73 default:
74 t.Fatalf("unexpected error: %v", err)
75 }
76 }
77 assert.Equal(t, 1, ok)
78 assert.Equal(t, workers-1, exists)
79
80 entries, err := os.ReadDir(filepath.Join(data, "repos"))
81 require.NoError(t, err)
82 var dirs []string
83 for _, e := range entries {
84 if e.Name() != ".staging" {
85 dirs = append(dirs, e.Name())
86 }
87 }
88 assert.Equal(t, []string{"tools.git"}, dirs)
89
90 all, err := store.List(ctx)
91 require.NoError(t, err)
92 assert.Len(t, all, 1)
93
94 staging, err := os.ReadDir(filepath.Join(data, "repos", ".staging"))
95 require.NoError(t, err)
96 assert.Empty(t, staging)
97}
98
99// unusable skips the test, or fails it in CI so that CI cannot pass vacuously.
100func unusable(t *testing.T, format string, args ...any) {
101 t.Helper()
102 if os.Getenv("CI") != "" {
103 t.Fatalf(format, args...)
104 }
105 t.Skipf(format, args...)
106}