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, err := repo.NewBug("bug with comments editions", "new bug")
73 if err != nil {
74 return nil, err
75 }
76
77 createOpHash, err := bugWithCommentEditions.Snapshot().Operations[0].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 // initialize exporter
218 exporter := &githubExporter{}
219 err = exporter.Init(core.Configuration{
220 keyOwner: user,
221 keyProject: projectName,
222 keyToken: token,
223 })
224 require.NoError(t, err)
225
226 start := time.Now()
227
228 // export all bugs
229 err = exporter.ExportAll(backend, time.Time{})
230 require.NoError(t, err)
231
232 fmt.Printf("test repository exported in %f seconds\n", time.Since(start).Seconds())
233
234 repoTwo := repository.CreateTestRepo(false)
235 defer repository.CleanupTestRepos(t, repoTwo)
236
237 // create a second backend
238 backendTwo, err := cache.NewRepoCache(repoTwo)
239 require.NoError(t, err)
240
241 importer := &githubImporter{}
242 err = importer.Init(core.Configuration{
243 keyOwner: user,
244 keyProject: projectName,
245 keyToken: token,
246 })
247 require.NoError(t, err)
248
249 // import all exported bugs to the second backend
250 err = importer.ImportAll(backendTwo, time.Time{})
251 require.NoError(t, err)
252
253 require.Len(t, backendTwo.AllBugsIds(), len(tests))
254
255 for _, tt := range tests {
256 t.Run(tt.name, func(t *testing.T) {
257 // for each operation a SetMetadataOperation will be added
258 // so number of operations should double
259 require.Len(t, tt.bug.Snapshot().Operations, tt.numOrOp*2)
260
261 // verify operation have correcte metadata
262 for _, op := range tt.bug.Snapshot().Operations {
263 // Check if the originals operations (*not* SetMetadata) are tagged properly
264 if _, ok := op.(*bug.SetMetadataOperation); !ok {
265 _, haveIDMetadata := op.GetMetadata(keyGithubId)
266 require.True(t, haveIDMetadata)
267
268 _, haveURLMetada := op.GetMetadata(keyGithubUrl)
269 require.True(t, haveURLMetada)
270 }
271 }
272
273 // get bug github ID
274 bugGithubID, ok := tt.bug.Snapshot().Operations[0].GetMetadata(keyGithubId)
275 require.True(t, ok)
276
277 // retrive bug from backendTwo
278 importedBug, err := backendTwo.ResolveBugCreateMetadata(keyGithubId, bugGithubID)
279 require.NoError(t, err)
280
281 // verify bug have same number of original operations
282 require.Len(t, importedBug.Snapshot().Operations, tt.numOrOp)
283
284 // verify bugs are taged with origin=github
285 issueOrigin, ok := importedBug.Snapshot().Operations[0].GetMetadata(keyOrigin)
286 require.True(t, ok)
287 require.Equal(t, issueOrigin, target)
288
289 //TODO: maybe more tests to ensure bug final state
290 })
291 }
292}
293
294func generateRepoName() string {
295 rand.Seed(time.Now().UnixNano())
296 var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
297 b := make([]rune, 8)
298 for i := range b {
299 b[i] = letterRunes[rand.Intn(len(letterRunes))]
300 }
301 return fmt.Sprintf("%s-%s", testRepoBaseName, string(b))
302}
303
304// create repository need a token with scope 'repo'
305func createRepository(project, token string) error {
306// This function use the V3 Github API because repository creation is not supported yet on the V4 API.
307 url := fmt.Sprintf("%s/user/repos", githubV3Url)
308
309 params := struct {
310 Name string `json:"name"`
311 Description string `json:"description"`
312 Private bool `json:"private"`
313 HasIssues bool `json:"has_issues"`
314 }{
315 Name: project,
316 Description: "git-bug exporter temporary test repository",
317 Private: true,
318 HasIssues: true,
319 }
320
321 data, err := json.Marshal(params)
322 if err != nil {
323 return err
324 }
325
326 req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
327 if err != nil {
328 return err
329 }
330
331 // need the token for private repositories
332 req.Header.Set("Authorization", fmt.Sprintf("token %s", token))
333
334 client := &http.Client{
335 Timeout: defaultTimeout,
336 }
337
338 resp, err := client.Do(req)
339 if err != nil {
340 return err
341 }
342
343 return resp.Body.Close()
344}
345
346// delete repository need a token with scope 'delete_repo'
347func deleteRepository(project, owner, token string) error {
348// This function use the V3 Github API because repository removal is not supported yet on the V4 API.
349 url := fmt.Sprintf("%s/repos/%s/%s", githubV3Url, owner, project)
350
351 req, err := http.NewRequest("DELETE", url, nil)
352 if err != nil {
353 return err
354 }
355
356 // need the token for private repositories
357 req.Header.Set("Authorization", fmt.Sprintf("token %s", token))
358
359 client := &http.Client{
360 Timeout: defaultTimeout,
361 }
362
363 resp, err := client.Do(req)
364 if err != nil {
365 return err
366 }
367
368 defer resp.Body.Close()
369
370 if resp.StatusCode != http.StatusNoContent {
371 return fmt.Errorf("error deleting repository")
372 }
373
374 return nil
375}