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.Bugs().New("simple bug", "new bug")
41 require.NoError(t, err)
42
43 // bug with comments
44 bugWithComments, _, err := repo.Bugs().New("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.Bugs().New("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.Bugs().New("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 commentId, _, err := bugWithCommentEditions.AddComment("first comment")
72 require.NoError(t, err)
73
74 _, err = bugWithCommentEditions.EditComment(commentId, "first comment edited")
75 require.NoError(t, err)
76
77 // bug status changed
78 bugStatusChanged, _, err := repo.Bugs().New("bug status changed", "new bug")
79 require.NoError(t, err)
80
81 _, err = bugStatusChanged.Close()
82 require.NoError(t, err)
83
84 _, err = bugStatusChanged.Open()
85 require.NoError(t, err)
86
87 // bug title changed
88 bugTitleEdited, _, err := repo.Bugs().New("bug title edited", "new bug")
89 require.NoError(t, err)
90
91 _, err = bugTitleEdited.SetTitle("bug title edited again")
92 require.NoError(t, err)
93
94 return []*testCase{
95 &testCase{
96 name: "simple bug",
97 bug: simpleBug,
98 numOp: 1,
99 numOpExp: 2,
100 numOpImp: 1,
101 },
102 &testCase{
103 name: "bug with comments",
104 bug: bugWithComments,
105 numOp: 2,
106 numOpExp: 4,
107 numOpImp: 2,
108 },
109 &testCase{
110 name: "bug label change",
111 bug: bugLabelChange,
112 numOp: 4,
113 numOpExp: 8,
114 numOpImp: 4,
115 },
116 &testCase{
117 name: "bug with comment editions",
118 bug: bugWithCommentEditions,
119 numOp: 4,
120 numOpExp: 8,
121 numOpImp: 2,
122 },
123 &testCase{
124 name: "bug changed status",
125 bug: bugStatusChanged,
126 numOp: 3,
127 numOpExp: 6,
128 numOpImp: 3,
129 },
130 &testCase{
131 name: "bug title edited",
132 bug: bugTitleEdited,
133 numOp: 2,
134 numOpExp: 4,
135 numOpImp: 2,
136 },
137 }
138}
139
140func TestGitlabPushPull(t *testing.T) {
141 // token must have 'repo' and 'delete_repo' scopes
142 envToken := os.Getenv("GITLAB_API_TOKEN")
143 if envToken == "" {
144 t.Skip("Env var GITLAB_API_TOKEN missing")
145 }
146
147 // create repo backend
148 repo := repository.CreateGoGitTestRepo(t, false)
149
150 backend, err := cache.NewRepoCacheNoEvents(repo)
151 require.NoError(t, err)
152
153 // set author identity
154 login := "test-identity"
155 author, err := backend.Identities().New("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
218 fmt.Printf("test repository exported in %f seconds\n", time.Since(start).Seconds())
219
220 repoTwo := repository.CreateGoGitTestRepo(t, false)
221
222 // create a second backend
223 backendTwo, err := cache.NewRepoCacheNoEvents(repoTwo)
224 require.NoError(t, err)
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.Bugs().AllIds(), 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.Compile().Operations, tt.numOpExp)
249
250 // verify operation have correct metadata
251 for _, op := range tt.bug.Compile().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.Compile().GetCreateMetadata(metaKeyGitlabId)
264 require.True(t, ok)
265
266 // retrieve bug from backendTwo
267 importedBug, err := backendTwo.Bugs().ResolveBugCreateMetadata(metaKeyGitlabId, bugGitlabID)
268 require.NoError(t, err)
269
270 // verify bug have same number of original operations
271 require.Len(t, importedBug.Compile().Operations, tt.numOpImp)
272
273 // verify bugs are tagged with origin=gitlab
274 issueOrigin, ok := importedBug.Compile().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
283func generateRepoName() string {
284 rand.Seed(time.Now().UnixNano())
285 var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
286 b := make([]rune, 8)
287 for i := range b {
288 b[i] = letterRunes[rand.Intn(len(letterRunes))]
289 }
290 return fmt.Sprintf("%s-%s", testRepoBaseName, string(b))
291}
292
293// create repository need a token with scope 'repo'
294func createRepository(ctx context.Context, name string, token *auth.Token) (int, error) {
295 client, err := buildClient(defaultBaseURL, token)
296 if err != nil {
297 return 0, err
298 }
299
300 project, _, err := client.Projects.CreateProject(
301 &gitlab.CreateProjectOptions{
302 Name: gitlab.String(name),
303 },
304 gitlab.WithContext(ctx),
305 )
306 if err != nil {
307 return 0, err
308 }
309
310 // fmt.Println("Project URL:", project.WebURL)
311
312 return project.ID, nil
313}
314
315// delete repository need a token with scope 'delete_repo'
316func deleteRepository(ctx context.Context, project int, token *auth.Token) error {
317 client, err := buildClient(defaultBaseURL, token)
318 if err != nil {
319 return err
320 }
321
322 _, err = client.Projects.DeleteProject(project, gitlab.WithContext(ctx))
323 return err
324}