1package gitlab
2
3import (
4 "context"
5 "fmt"
6 "math/rand"
7 "os"
8 "strconv"
9 "testing"
10 "time"
11
12 "github.com/xanzy/go-gitlab"
13
14 "github.com/MichaelMure/git-bug/entity"
15 "github.com/MichaelMure/git-bug/entity/dag"
16
17 "github.com/stretchr/testify/require"
18
19 "github.com/MichaelMure/git-bug/bridge/core"
20 "github.com/MichaelMure/git-bug/bridge/core/auth"
21 "github.com/MichaelMure/git-bug/cache"
22 "github.com/MichaelMure/git-bug/repository"
23 "github.com/MichaelMure/git-bug/util/interrupt"
24)
25
26const (
27 testRepoBaseName = "git-bug-test-gitlab-exporter"
28)
29
30type testCase struct {
31 name string
32 bug *cache.BugCache
33 numOp int // number of original operations
34 numOpExp int // number of operations after export
35 numOpImp int // number of operations after import
36}
37
38func testCases(t *testing.T, repo *cache.RepoCache) []*testCase {
39 // simple bug
40 simpleBug, _, err := repo.NewBug("simple bug", "new bug")
41 require.NoError(t, err)
42
43 // bug with comments
44 bugWithComments, _, err := repo.NewBug("bug with comments", "new bug")
45 require.NoError(t, err)
46
47 _, err = bugWithComments.AddComment("new comment")
48 require.NoError(t, err)
49
50 // bug with label changes
51 bugLabelChange, _, err := repo.NewBug("bug label change", "new bug")
52 require.NoError(t, err)
53
54 _, _, err = bugLabelChange.ChangeLabels([]string{"bug"}, nil)
55 require.NoError(t, err)
56
57 _, _, err = bugLabelChange.ChangeLabels([]string{"core"}, nil)
58 require.NoError(t, err)
59
60 _, _, err = bugLabelChange.ChangeLabels(nil, []string{"bug"})
61 require.NoError(t, err)
62
63 // bug with comments editions
64 bugWithCommentEditions, createOp, err := repo.NewBug("bug with comments editions", "new bug")
65 require.NoError(t, err)
66
67 _, err = bugWithCommentEditions.EditComment(
68 entity.CombineIds(bugWithCommentEditions.Id(), createOp.Id()), "first comment edited")
69 require.NoError(t, err)
70
71 commentOp, err := bugWithCommentEditions.AddComment("first comment")
72 require.NoError(t, err)
73
74 _, err = bugWithCommentEditions.EditComment(
75 entity.CombineIds(bugWithCommentEditions.Id(), commentOp.Id()), "first comment edited")
76 require.NoError(t, err)
77
78 // bug status changed
79 bugStatusChanged, _, err := repo.NewBug("bug status changed", "new bug")
80 require.NoError(t, err)
81
82 _, err = bugStatusChanged.Close()
83 require.NoError(t, err)
84
85 _, err = bugStatusChanged.Open()
86 require.NoError(t, err)
87
88 // bug title changed
89 bugTitleEdited, _, err := repo.NewBug("bug title edited", "new bug")
90 require.NoError(t, err)
91
92 _, err = bugTitleEdited.SetTitle("bug title edited again")
93 require.NoError(t, err)
94
95 return []*testCase{
96 &testCase{
97 name: "simple bug",
98 bug: simpleBug,
99 numOp: 1,
100 numOpExp: 2,
101 numOpImp: 1,
102 },
103 &testCase{
104 name: "bug with comments",
105 bug: bugWithComments,
106 numOp: 2,
107 numOpExp: 4,
108 numOpImp: 2,
109 },
110 &testCase{
111 name: "bug label change",
112 bug: bugLabelChange,
113 numOp: 4,
114 numOpExp: 8,
115 numOpImp: 4,
116 },
117 &testCase{
118 name: "bug with comment editions",
119 bug: bugWithCommentEditions,
120 numOp: 4,
121 numOpExp: 8,
122 numOpImp: 2,
123 },
124 &testCase{
125 name: "bug changed status",
126 bug: bugStatusChanged,
127 numOp: 3,
128 numOpExp: 6,
129 numOpImp: 3,
130 },
131 &testCase{
132 name: "bug title edited",
133 bug: bugTitleEdited,
134 numOp: 2,
135 numOpExp: 4,
136 numOpImp: 2,
137 },
138 }
139}
140
141func TestGitlabPushPull(t *testing.T) {
142 // token must have 'repo' and 'delete_repo' scopes
143 envToken := os.Getenv("GITLAB_API_TOKEN")
144 if envToken == "" {
145 t.Skip("Env var GITLAB_API_TOKEN missing")
146 }
147
148 // create repo backend
149 repo := repository.CreateGoGitTestRepo(t, false)
150
151 backend, stderr := cache.NewTestRepoCache(t, repo)
152
153 // set author identity
154 login := "test-identity"
155 author, err := backend.NewIdentity("test identity", "test@test.org")
156 require.NoError(t, err)
157 author.SetMetadata(metaKeyGitlabLogin, login)
158 err = author.Commit()
159 require.NoError(t, err)
160
161 err = backend.SetUserIdentity(author)
162 require.NoError(t, err)
163
164 defer backend.Close()
165 interrupt.RegisterCleaner(backend.Close)
166
167 token := auth.NewToken(target, envToken)
168 token.SetMetadata(auth.MetaKeyLogin, login)
169 token.SetMetadata(auth.MetaKeyBaseURL, defaultBaseURL)
170 err = auth.Store(repo, token)
171 require.NoError(t, err)
172
173 tests := testCases(t, backend)
174
175 // generate project name
176 projectName := generateRepoName()
177
178 // create target Gitlab repository
179 projectID, err := createRepository(context.TODO(), projectName, token)
180 require.NoError(t, err)
181
182 fmt.Println("created repository", projectName)
183
184 // Make sure to remove the Gitlab repository when the test end
185 defer func(t *testing.T) {
186 if err := deleteRepository(context.TODO(), projectID, token); err != nil {
187 t.Fatal(err)
188 }
189 fmt.Println("deleted repository:", projectName)
190 }(t)
191
192 interrupt.RegisterCleaner(func() error {
193 return deleteRepository(context.TODO(), projectID, token)
194 })
195
196 ctx := context.Background()
197
198 // initialize exporter
199 exporter := &gitlabExporter{}
200 err = exporter.Init(ctx, backend, core.Configuration{
201 confKeyProjectID: strconv.Itoa(projectID),
202 confKeyGitlabBaseUrl: defaultBaseURL,
203 confKeyDefaultLogin: login,
204 })
205 require.NoError(t, err)
206
207 start := time.Now()
208
209 // export all bugs
210 exportEvents, err := exporter.ExportAll(ctx, backend, time.Time{})
211 require.NoError(t, err)
212
213 for result := range exportEvents {
214 require.NoError(t, result.Err)
215 }
216 require.NoError(t, err)
217 require.Empty(t, stderr.String())
218
219 fmt.Printf("test repository exported in %f seconds\n", time.Since(start).Seconds())
220
221 repoTwo := repository.CreateGoGitTestRepo(t, false)
222
223 // create a second backend
224 backendTwo, stderr := cache.NewTestRepoCache(t, repoTwo)
225
226 importer := &gitlabImporter{}
227 err = importer.Init(ctx, backend, core.Configuration{
228 confKeyProjectID: strconv.Itoa(projectID),
229 confKeyGitlabBaseUrl: defaultBaseURL,
230 confKeyDefaultLogin: login,
231 })
232 require.NoError(t, err)
233
234 // import all exported bugs to the second backend
235 importEvents, err := importer.ImportAll(ctx, backendTwo, time.Time{})
236 require.NoError(t, err)
237
238 for result := range importEvents {
239 require.NoError(t, result.Err)
240 }
241
242 require.Len(t, backendTwo.AllBugsIds(), len(tests))
243
244 for _, tt := range tests {
245 t.Run(tt.name, func(t *testing.T) {
246 // for each operation a SetMetadataOperation will be added
247 // so number of operations should double
248 require.Len(t, tt.bug.Snapshot().Operations, tt.numOpExp)
249
250 // verify operation have correct metadata
251 for _, op := range tt.bug.Snapshot().Operations {
252 // Check if the originals operations (*not* SetMetadata) are tagged properly
253 if _, ok := op.(dag.OperationDoesntChangeSnapshot); !ok {
254 _, haveIDMetadata := op.GetMetadata(metaKeyGitlabId)
255 require.True(t, haveIDMetadata)
256
257 _, haveURLMetada := op.GetMetadata(metaKeyGitlabUrl)
258 require.True(t, haveURLMetada)
259 }
260 }
261
262 // get bug gitlab ID
263 bugGitlabID, ok := tt.bug.Snapshot().GetCreateMetadata(metaKeyGitlabId)
264 require.True(t, ok)
265
266 // retrieve bug from backendTwo
267 importedBug, err := backendTwo.ResolveBugCreateMetadata(metaKeyGitlabId, bugGitlabID)
268 require.NoError(t, err)
269
270 // verify bug have same number of original operations
271 require.Len(t, importedBug.Snapshot().Operations, tt.numOpImp)
272
273 // verify bugs are tagged with origin=gitlab
274 issueOrigin, ok := importedBug.Snapshot().GetCreateMetadata(core.MetaKeyOrigin)
275 require.True(t, ok)
276 require.Equal(t, issueOrigin, target)
277
278 // TODO: maybe more tests to ensure bug final state
279 })
280 }
281
282 require.Empty(t, stderr.String())
283}
284
285func generateRepoName() string {
286 rand.Seed(time.Now().UnixNano())
287 var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
288 b := make([]rune, 8)
289 for i := range b {
290 b[i] = letterRunes[rand.Intn(len(letterRunes))]
291 }
292 return fmt.Sprintf("%s-%s", testRepoBaseName, string(b))
293}
294
295// create repository need a token with scope 'repo'
296func createRepository(ctx context.Context, name string, token *auth.Token) (int, error) {
297 client, err := buildClient(defaultBaseURL, token)
298 if err != nil {
299 return 0, err
300 }
301
302 project, _, err := client.Projects.CreateProject(
303 &gitlab.CreateProjectOptions{
304 Name: gitlab.String(name),
305 },
306 gitlab.WithContext(ctx),
307 )
308 if err != nil {
309 return 0, err
310 }
311
312 // fmt.Println("Project URL:", project.WebURL)
313
314 return project.ID, nil
315}
316
317// delete repository need a token with scope 'delete_repo'
318func deleteRepository(ctx context.Context, project int, token *auth.Token) error {
319 client, err := buildClient(defaultBaseURL, token)
320 if err != nil {
321 return err
322 }
323
324 _, err = client.Projects.DeleteProject(project, gitlab.WithContext(ctx))
325 return err
326}