1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package app_test
4
5import (
6 "context"
7 "errors"
8 "testing"
9
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/mock"
12 "github.com/stretchr/testify/require"
13
14 "bnema.dev/ermite/internal/app"
15 "bnema.dev/ermite/internal/domain"
16 "bnema.dev/ermite/internal/ports/mocks"
17)
18
19type mrUnit struct {
20 meta *mocks.RepoMetaStore
21 store *mocks.MergeRequestStore
22 repos *mocks.GitRepos
23 git *mocks.MergeGit
24 mrs *app.MergeRequests
25 repo domain.Repo
26}
27
28func newMRUnit(t *testing.T) *mrUnit {
29 t.Helper()
30 u := &mrUnit{
31 meta: mocks.NewRepoMetaStore(t),
32 store: mocks.NewMergeRequestStore(t),
33 repos: mocks.NewGitRepos(t),
34 git: mocks.NewMergeGit(t),
35 repo: domain.Repo{Name: name(t, "proj"), Visibility: domain.VisibilityPrivate, Settings: domain.DefaultRepoSettings()},
36 }
37 clock := mocks.NewClock(t)
38 clock.EXPECT().Now().Return(now).Maybe()
39 u.mrs = app.NewMergeRequests(app.MergeRequestsConfig{
40 Meta: u.meta, Store: u.store, Repos: u.repos, Git: u.git,
41 Signer: mocks.NewCommitSigner(t), Bus: mocks.NewEventBus(t), Clock: clock, Maint: &scheduler{},
42 })
43 return u
44}
45
46// GO-004: a failure to read Git is an error for the log, not a field violation
47// that would show its text to the caller.
48func TestCreateReturnsGitFailuresAsErrors(t *testing.T) {
49 boom := errors.New("fatal: /srv/ermite/repos/proj.git: permission denied")
50 params := app.CreateMergeRequestParams{Title: "t", Source: "feat", Target: "main"}
51
52 t.Run("reading a branch", func(t *testing.T) {
53 u := newMRUnit(t)
54 u.meta.EXPECT().Get(mock.Anything, u.repo.Name).Return(u.repo, nil)
55 u.git.EXPECT().BranchHead(mock.Anything, u.repo.Name, "feat").Return(domain.ObjectID{}, boom)
56 _, err := u.mrs.Create(context.Background(), domain.Root(), u.repo.Name, params)
57 require.ErrorIs(t, err, boom)
58 var fe *domain.InvalidFieldsError
59 assert.False(t, errors.As(err, &fe), "not a field violation: %v", err)
60 assert.NotErrorIs(t, err, domain.ErrInvalid)
61 })
62
63 t.Run("reading the default branch", func(t *testing.T) {
64 u := newMRUnit(t)
65 u.meta.EXPECT().Get(mock.Anything, u.repo.Name).Return(u.repo, nil)
66 u.repos.EXPECT().DefaultBranch(mock.Anything, u.repo.Name).Return("", false, boom)
67 _, err := u.mrs.Create(context.Background(), domain.Root(), u.repo.Name, app.CreateMergeRequestParams{Title: "t", Source: "feat"})
68 require.ErrorIs(t, err, boom)
69 var fe *domain.InvalidFieldsError
70 assert.False(t, errors.As(err, &fe), "not a field violation: %v", err)
71 })
72
73 t.Run("a branch that does not exist stays a violation", func(t *testing.T) {
74 u := newMRUnit(t)
75 u.meta.EXPECT().Get(mock.Anything, u.repo.Name).Return(u.repo, nil)
76 u.git.EXPECT().BranchHead(mock.Anything, u.repo.Name, "feat").Return(domain.ObjectID{}, domain.ErrNotFound)
77 u.git.EXPECT().BranchHead(mock.Anything, u.repo.Name, "main").Return(someID(t), nil)
78 _, err := u.mrs.Create(context.Background(), domain.Root(), u.repo.Name, params)
79 assert.Contains(t, fieldsOf(t, err), "source_branch")
80 })
81}
82
83func someID(t *testing.T) domain.ObjectID {
84 t.Helper()
85 return oidFrom(t, oidA)
86}
87
88// GO-003: the settings are read again under the merge lock; a failure of that
89// read is returned, and a repository deleted meanwhile is not found.
90func TestMergeReturnsTheFailureToReadTheSettings(t *testing.T) {
91 boom := errors.New("kv: closed")
92 for label, tc := range map[string]struct{ err, want error }{
93 "read failure": {boom, boom},
94 "deleted meanwhile": {domain.ErrNotFound, domain.ErrNotFound},
95 } {
96 t.Run(label, func(t *testing.T) {
97 u := newMRUnit(t)
98 u.meta.EXPECT().Get(mock.Anything, u.repo.Name).Return(u.repo, nil).Once()
99 u.meta.EXPECT().Get(mock.Anything, u.repo.Name).Return(domain.Repo{}, tc.err).Once()
100 u.store.EXPECT().Get(mock.Anything, u.repo.Name, int64(1)).Return(domain.MergeRequest{
101 Repo: u.repo.Name, Number: 1, State: domain.MRStateOpen, Source: "feat", Target: "main",
102 }, nil)
103 _, err := u.mrs.Merge(context.Background(), domain.Root(), u.repo.Name, 1, app.MergeParams{HeadID: someID(t)})
104 require.ErrorIs(t, err, tc.want)
105 if tc.want == boom {
106 assert.NotErrorIs(t, err, domain.ErrNotFound)
107 }
108 })
109 }
110}