1package github
2
3import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "math/rand"
8 "net/http"
9 "os"
10 "testing"
11 "time"
12
13 "github.com/stretchr/testify/require"
14
15 "github.com/MichaelMure/git-bug/bridge/core"
16 "github.com/MichaelMure/git-bug/bug"
17 "github.com/MichaelMure/git-bug/cache"
18 "github.com/MichaelMure/git-bug/repository"
19 "github.com/MichaelMure/git-bug/util/interrupt"
20)
21
22const (
23 testRepoBaseName = "git-bug-test-github-exporter"
24)
25
26type testCase struct {
27 name string
28 bug *cache.BugCache
29 numOrOp int // number of original operations
30}
31
32func testCases(repo *cache.RepoCache, identity *cache.IdentityCache) ([]*testCase, error) {
33 // simple bug
34 simpleBug, _, err := repo.NewBug("simple bug", "new bug")
35 if err != nil {
36 return nil, err
37 }
38
39 // bug with comments
40 bugWithComments, _, err := repo.NewBug("bug with comments", "new bug")
41 if err != nil {
42 return nil, err
43 }
44
45 _, err = bugWithComments.AddComment("new comment")
46 if err != nil {
47 return nil, err
48 }
49
50 // bug with label changes
51 bugLabelChange, _, err := repo.NewBug("bug label change", "new bug")
52 if err != nil {
53 return nil, err
54 }
55
56 _, _, err = bugLabelChange.ChangeLabels([]string{"bug"}, nil)
57 if err != nil {
58 return nil, err
59 }
60
61 _, _, err = bugLabelChange.ChangeLabels([]string{"core"}, nil)
62 if err != nil {
63 return nil, err
64 }
65
66 _, _, err = bugLabelChange.ChangeLabels(nil, []string{"bug"})
67 if err != nil {
68 return nil, err
69 }
70
71 // bug with comments editions
72 bugWithCommentEditions, createOp, err := repo.NewBug("bug with comments editions", "new bug")
73 if err != nil {
74 return nil, err
75 }
76
77 createOpHash, err := createOp.Hash()
78 if err != nil {
79 return nil, err
80 }
81
82 _, err = bugWithCommentEditions.EditComment(createOpHash, "first comment edited")
83 if err != nil {
84 return nil, err
85 }
86
87 commentOp, err := bugWithCommentEditions.AddComment("first comment")
88 if err != nil {
89 return nil, err
90 }
91
92 commentOpHash, err := commentOp.Hash()
93 if err != nil {
94 return nil, err
95 }
96
97 _, err = bugWithCommentEditions.EditComment(commentOpHash, "first comment edited")
98 if err != nil {
99 return nil, err
100 }
101
102 // bug status changed
103 bugStatusChanged, _, err := repo.NewBug("bug status changed", "new bug")
104 if err != nil {
105 return nil, err
106 }
107
108 _, err = bugStatusChanged.Close()
109 if err != nil {
110 return nil, err
111 }
112
113 _, err = bugStatusChanged.Open()
114 if err != nil {
115 return nil, err
116 }
117
118 // bug title changed
119 bugTitleEdited, _, err := repo.NewBug("bug title edited", "new bug")
120 if err != nil {
121 return nil, err
122 }
123
124 _, err = bugTitleEdited.SetTitle("bug title edited again")
125 if err != nil {
126 return nil, err
127 }
128
129 return []*testCase{
130 &testCase{
131 name: "simple bug",
132 bug: simpleBug,
133 numOrOp: 1,
134 },
135 &testCase{
136 name: "bug with comments",
137 bug: bugWithComments,
138 numOrOp: 2,
139 },
140 &testCase{
141 name: "bug label change",
142 bug: bugLabelChange,
143 numOrOp: 4,
144 },
145 &testCase{
146 name: "bug with comment editions",
147 bug: bugWithCommentEditions,
148 numOrOp: 4,
149 },
150 &testCase{
151 name: "bug changed status",
152 bug: bugStatusChanged,
153 numOrOp: 3,
154 },
155 &testCase{
156 name: "bug title edited",
157 bug: bugTitleEdited,
158 numOrOp: 2,
159 },
160 }, nil
161
162}
163
164func TestPushPull(t *testing.T) {
165 // repo owner
166 user := os.Getenv("TEST_USER")
167
168 // token must have 'repo' and 'delete_repo' scopes
169 token := os.Getenv("GITHUB_TOKEN_ADMIN")
170 if token == "" {
171 t.Skip("Env var GITHUB_TOKEN_ADMIN missing")
172 }
173
174 // create repo backend
175 repo := repository.CreateTestRepo(false)
176 defer repository.CleanupTestRepos(t, repo)
177
178 backend, err := cache.NewRepoCache(repo)
179 require.NoError(t, err)
180
181 // set author identity
182 author, err := backend.NewIdentity("test identity", "test@test.org")
183 if err != nil {
184 t.Fatal(err)
185 }
186
187 err = backend.SetUserIdentity(author)
188 if err != nil {
189 t.Fatal(err)
190 }
191
192 defer backend.Close()
193 interrupt.RegisterCleaner(backend.Close)
194
195 tests, err := testCases(backend, author)
196 if err != nil {
197 t.Fatal(err)
198 }
199
200 // generate project name
201 projectName := generateRepoName()
202
203 // create target Github repository
204 if err := createRepository(projectName, token); err != nil {
205 t.Fatal(err)
206 }
207 fmt.Println("created repository", projectName)
208
209 // Make sure to remove the Github repository when the test end
210 defer func(t *testing.T) {
211 if err := deleteRepository(projectName, user, token); err != nil {
212 t.Fatal(err)
213 }
214 fmt.Println("deleted repository:", projectName)
215 }(t)
216
217 interrupt.RegisterCleaner(func() error {
218 return deleteRepository(projectName, user, token)
219 })
220
221 // initialize exporter
222 exporter := &githubExporter{}
223 err = exporter.Init(core.Configuration{
224 keyOwner: user,
225 keyProject: projectName,
226 keyToken: token,
227 })
228 require.NoError(t, err)
229
230 start := time.Now()
231
232 // export all bugs
233 err = exporter.ExportAll(backend, time.Time{})
234 require.NoError(t, err)
235
236 fmt.Printf("test repository exported in %f seconds\n", time.Since(start).Seconds())
237
238 repoTwo := repository.CreateTestRepo(false)
239 defer repository.CleanupTestRepos(t, repoTwo)
240
241 // create a second backend
242 backendTwo, err := cache.NewRepoCache(repoTwo)
243 require.NoError(t, err)
244
245 importer := &githubImporter{}
246 err = importer.Init(core.Configuration{
247 keyOwner: user,
248 keyProject: projectName,
249 keyToken: token,
250 })
251 require.NoError(t, err)
252
253 // import all exported bugs to the second backend
254 err = importer.ImportAll(backendTwo, time.Time{})
255 require.NoError(t, err)
256
257 require.Len(t, backendTwo.AllBugsIds(), len(tests))
258
259 for _, tt := range tests {
260 t.Run(tt.name, func(t *testing.T) {
261 // for each operation a SetMetadataOperation will be added
262 // so number of operations should double
263 require.Len(t, tt.bug.Snapshot().Operations, tt.numOrOp*2)
264
265 // verify operation have correcte metadata
266 for _, op := range tt.bug.Snapshot().Operations {
267 // Check if the originals operations (*not* SetMetadata) are tagged properly
268 if _, ok := op.(*bug.SetMetadataOperation); !ok {
269 _, haveIDMetadata := op.GetMetadata(keyGithubId)
270 require.True(t, haveIDMetadata)
271
272 _, haveURLMetada := op.GetMetadata(keyGithubUrl)
273 require.True(t, haveURLMetada)
274 }
275 }
276
277 // get bug github ID
278 bugGithubID, ok := tt.bug.Snapshot().GetCreateMetadata(keyGithubId)
279 require.True(t, ok)
280
281 // retrive bug from backendTwo
282 importedBug, err := backendTwo.ResolveBugCreateMetadata(keyGithubId, bugGithubID)
283 require.NoError(t, err)
284
285 // verify bug have same number of original operations
286 require.Len(t, importedBug.Snapshot().Operations, tt.numOrOp)
287
288 // verify bugs are taged with origin=github
289 issueOrigin, ok := importedBug.Snapshot().GetCreateMetadata(keyOrigin)
290 require.True(t, ok)
291 require.Equal(t, issueOrigin, target)
292
293 //TODO: maybe more tests to ensure bug final state
294 })
295 }
296}
297
298func generateRepoName() string {
299 rand.Seed(time.Now().UnixNano())
300 var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
301 b := make([]rune, 8)
302 for i := range b {
303 b[i] = letterRunes[rand.Intn(len(letterRunes))]
304 }
305 return fmt.Sprintf("%s-%s", testRepoBaseName, string(b))
306}
307
308// create repository need a token with scope 'repo'
309func createRepository(project, token string) error {
310 // This function use the V3 Github API because repository creation is not supported yet on the V4 API.
311 url := fmt.Sprintf("%s/user/repos", githubV3Url)
312
313 params := struct {
314 Name string `json:"name"`
315 Description string `json:"description"`
316 Private bool `json:"private"`
317 HasIssues bool `json:"has_issues"`
318 }{
319 Name: project,
320 Description: "git-bug exporter temporary test repository",
321 Private: true,
322 HasIssues: true,
323 }
324
325 data, err := json.Marshal(params)
326 if err != nil {
327 return err
328 }
329
330 req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
331 if err != nil {
332 return err
333 }
334
335 // need the token for private repositories
336 req.Header.Set("Authorization", fmt.Sprintf("token %s", token))
337
338 client := &http.Client{
339 Timeout: defaultTimeout,
340 }
341
342 resp, err := client.Do(req)
343 if err != nil {
344 return err
345 }
346
347 return resp.Body.Close()
348}
349
350// delete repository need a token with scope 'delete_repo'
351func deleteRepository(project, owner, token string) error {
352 // This function use the V3 Github API because repository removal is not supported yet on the V4 API.
353 url := fmt.Sprintf("%s/repos/%s/%s", githubV3Url, owner, project)
354
355 req, err := http.NewRequest("DELETE", url, nil)
356 if err != nil {
357 return err
358 }
359
360 // need the token for private repositories
361 req.Header.Set("Authorization", fmt.Sprintf("token %s", token))
362
363 client := &http.Client{
364 Timeout: defaultTimeout,
365 }
366
367 resp, err := client.Do(req)
368 if err != nil {
369 return err
370 }
371
372 defer resp.Body.Close()
373
374 if resp.StatusCode != http.StatusNoContent {
375 return fmt.Errorf("error deleting repository")
376 }
377
378 return nil
379}