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