1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package app
4
5import (
6 "testing"
7 "time"
8
9 "connectrpc.com/connect"
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/require"
12 "google.golang.org/protobuf/types/known/fieldmaskpb"
13
14 pb "bnema.dev/ermite-api/gen/ermite"
15 "bnema.dev/ermite/internal/domain"
16 "bnema.dev/ermite/internal/ports"
17)
18
19// This test exercises the composition root through use cases and domain
20// events; TestAutoMergeEndToEndAPI does the same through the RPC API.
21func TestAutoMergeEndToEnd(t *testing.T) {
22 c := newCIHarness(t, nil)
23 runner := c.registerRunner("alpha", "ubuntu-latest")
24 c.push("main", map[string]string{".github/workflows/ci.yml": `name: ci
25on:
26 push:
27 branches: [feat]
28 pull_request:
29jobs:
30 build:
31 runs-on: ubuntu-latest
32 steps:
33 - run: echo hello
34`}, "")
35 head := c.push("feat", map[string]string{"change.txt": "feature\n"}, "")
36 number := c.openMR(c.root, "feat")
37 repo := c.name("proj")
38 allow := true
39 _, err := c.forge.Repos.Update(c.ctx, domain.Root(), repo, ports.UpdateRepoParams{AllowAutoMerge: &allow})
40 require.NoError(t, err)
41 headID, err := domain.ParseObjectID(head)
42 require.NoError(t, err)
43 events := c.forge.Bus.Subscribe(c.ctx)
44 enabled, err := c.forge.MergeRequests.EnableAutoMerge(c.ctx, domain.Owner(), repo, number, ports.EnableAutoMergeParams{HeadID: headID})
45 require.NoError(t, err)
46 require.NotNil(t, enabled.AutoMerge)
47 assert.Equal(t, head, enabled.AutoMerge.HeadID.String())
48 enabledDeadline := time.After(5 * time.Second)
49waitEnabled:
50 for {
51 select {
52 case ev := <-events:
53 if enabled, ok := ev.(domain.MergeRequestAutoMergeEnabled); ok && enabled.MergeRequest.Repo == repo && enabled.MergeRequest.Number == number {
54 break waitEnabled
55 }
56 case <-enabledDeadline:
57 t.Fatal("no enabled event")
58 }
59 }
60 c.finish(runner)
61 // A successful push check alone must not merge while the pull run is queued.
62 got, err := c.forge.MergeRequests.Get(c.ctx, domain.Root(), repo, number, ports.GetMergeRequestOptions{})
63 require.NoError(t, err)
64 assert.True(t, got.MergeRequest.IsOpen())
65 c.finish(runner)
66 deadline := time.After(10 * time.Second)
67 for {
68 select {
69 case ev := <-events:
70 if merged, ok := ev.(domain.MergeRequestMerged); ok && merged.MergeRequest.Number == number {
71 assert.True(t, merged.Auto)
72 assert.True(t, merged.MergeRequest.AutoMerged)
73 assert.Nil(t, merged.MergeRequest.AutoMerge)
74 return
75 }
76 case <-deadline:
77 t.Fatal("no automatic merged event")
78 }
79 }
80}
81
82// TestAutoMergeEndToEndAPI enables auto-merge through the API client, lets CI
83// succeed, and reads the Watch stream: enabled, then merged with auto=true.
84func TestAutoMergeEndToEndAPI(t *testing.T) {
85 c := newCIHarness(t, nil)
86 runner := c.registerRunner("alpha", "ubuntu-latest")
87 c.push("main", map[string]string{".github/workflows/ci.yml": `name: ci
88on:
89 push:
90 branches: [feat]
91 pull_request:
92jobs:
93 build:
94 runs-on: ubuntu-latest
95 steps:
96 - run: echo hello
97`}, "")
98 head := c.push("feat", map[string]string{"change.txt": "feature\n"}, "")
99 number := c.openMR(c.root, "feat")
100
101 // Off by default: the server refuses.
102 _, err := c.root.mr.EnableAutoMerge(c.ctx, connect.NewRequest(&pb.EnableAutoMergeRequest{Repo: c.repo, Number: number, HeadId: head}))
103 require.Error(t, err)
104 assert.Equal(t, connect.CodeFailedPrecondition, connect.CodeOf(err))
105
106 ver, err := c.root.server.GetVersion(c.ctx, connect.NewRequest(&pb.GetVersionRequest{}))
107 require.NoError(t, err)
108 assert.Contains(t, ver.Msg.GetFeatures(), "merge_requests.auto_merge")
109
110 up, err := c.root.repo.UpdateRepo(c.ctx, connect.NewRequest(&pb.UpdateRepoRequest{
111 Repo: c.repo,
112 Settings: &pb.RepoSettings{AllowAutoMerge: true},
113 UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"settings.allow_auto_merge"}},
114 }))
115 require.NoError(t, err)
116 assert.True(t, up.Msg.GetRepo().GetSettings().GetAllowAutoMerge())
117
118 w := c.watch(c.root, "", "home")
119 res, err := c.root.mr.EnableAutoMerge(c.ctx, connect.NewRequest(&pb.EnableAutoMergeRequest{
120 Repo: c.repo, Number: number, Mode: pb.MergeMode_MERGE_MODE_SQUASH, HeadId: head,
121 }))
122 require.NoError(t, err)
123 am := res.Msg.GetMergeRequest().GetAutoMerge()
124 require.NotNil(t, am)
125 assert.Equal(t, pb.MergeMode_MERGE_MODE_SQUASH, am.GetMode())
126 assert.Equal(t, res.Msg.GetMergeRequest().GetHeadId(), am.GetHeadId())
127 assert.NotEmpty(t, am.GetEnabledBy())
128 assert.NotNil(t, am.GetEnabledAt())
129
130 got, err := c.root.mr.GetMergeRequest(c.ctx, connect.NewRequest(&pb.GetMergeRequestRequest{Repo: c.repo, Number: number}))
131 require.NoError(t, err)
132 assert.NotNil(t, got.Msg.GetMergeRequest().GetAutoMerge(), "Get reports the pending auto-merge")
133
134 c.finish(runner)
135 c.finish(runner)
136
137 var sawEnabled bool
138 deadline := time.After(15 * time.Second)
139 for {
140 select {
141 case ev, ok := <-w.events:
142 require.True(t, ok, "the stream ended: %v", w.err)
143 if e := ev.GetMergeRequestAutoMergeEnabled(); e != nil {
144 assert.EqualValues(t, number, e.GetMergeRequest().GetNumber())
145 assert.NotNil(t, e.GetMergeRequest().GetAutoMerge())
146 sawEnabled = true
147 }
148 if m := ev.GetMergeRequestMerged(); m != nil && m.GetMergeRequest().GetNumber() == number {
149 assert.True(t, sawEnabled, "enabled comes before merged")
150 assert.True(t, m.GetAuto())
151 assert.True(t, m.GetMergeRequest().GetAutoMerged())
152 assert.Nil(t, m.GetMergeRequest().GetAutoMerge())
153 final, err := c.root.mr.GetMergeRequest(c.ctx, connect.NewRequest(&pb.GetMergeRequestRequest{Repo: c.repo, Number: number}))
154 require.NoError(t, err)
155 assert.Equal(t, pb.MergeRequestState_MERGE_REQUEST_STATE_MERGED, final.Msg.GetMergeRequest().GetState())
156 assert.True(t, final.Msg.GetMergeRequest().GetAutoMerged())
157 return
158 }
159 case <-deadline:
160 t.Fatal("no automatic merged event")
161 }
162 }
163}