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 TestExporter(t *testing.T) {
165 user := os.Getenv("TEST_USER")
166 token := os.Getenv("GITHUB_TOKEN_ADMIN")
167 if token == "" {
168 t.Skip("Env var GITHUB_TOKEN_ADMIN missing")
169 }
170
171 repo := repository.CreateTestRepo(false)
172 defer repository.CleanupTestRepos(t, repo)
173
174 backend, err := cache.NewRepoCache(repo)
175 require.NoError(t, err)
176
177 author, err := backend.NewIdentity("test identity", "test@test.org")
178 if err != nil {
179 t.Fatal(err)
180 }
181
182 err = backend.SetUserIdentity(author)
183 if err != nil {
184 t.Fatal(err)
185 }
186
187 defer backend.Close()
188 interrupt.RegisterCleaner(backend.Close)
189
190 tests, err := testCases(backend, author)
191 if err != nil {
192 t.Fatal(err)
193 }
194
195 // generate project name
196 projectName := generateRepoName()
197
198 // create repository
199 if err := createRepository(projectName, token); err != nil {
200 t.Fatal(err)
201 }
202 fmt.Println("created repository", projectName)
203
204 // delete repository before ending tests
205 defer func(t *testing.T) {
206 if err := deleteRepository(projectName, user, token); err != nil {
207 t.Fatal(err)
208 }
209 fmt.Println("deleted repository:", projectName)
210 }(t)
211
212 // initialize exporter
213 exporter := &githubExporter{}
214 err = exporter.Init(core.Configuration{
215 keyOwner: user,
216 keyProject: projectName,
217 keyToken: token,
218 })
219 require.NoError(t, err)
220
221 start := time.Now()
222
223 // export all bugs
224 err = exporter.ExportAll(backend, time.Time{})
225 require.NoError(t, err)
226
227 fmt.Printf("test repository exported in %f seconds\n", time.Since(start).Seconds())
228
229 repoTwo := repository.CreateTestRepo(false)
230 defer repository.CleanupTestRepos(t, repoTwo)
231
232 // create a second backend
233 backendTwo, err := cache.NewRepoCache(repoTwo)
234 require.NoError(t, err)
235
236 importer := &githubImporter{}
237 err = importer.Init(core.Configuration{
238 keyOwner: user,
239 keyProject: projectName,
240 keyToken: token,
241 })
242 require.NoError(t, err)
243
244 // import all exported bugs to the second backend
245 err = importer.ImportAll(backendTwo, time.Time{})
246 require.NoError(t, err)
247
248 require.Len(t, backendTwo.AllBugsIds(), len(tests))
249
250 for _, tt := range tests {
251 t.Run(tt.name, func(t *testing.T) {
252 // for each operation a SetMetadataOperation will be added
253 // so number of operations should double
254 require.Len(t, tt.bug.Snapshot().Operations, tt.numOrOp*2)
255
256 for _, op := range tt.bug.Snapshot().Operations {
257 if _, ok := op.(*bug.SetMetadataOperation); !ok {
258 _, haveIDMetadata := op.GetMetadata(keyGithubId)
259 require.True(t, haveIDMetadata)
260
261 _, haveURLMetada := op.GetMetadata(keyGithubUrl)
262 require.True(t, haveURLMetada)
263 }
264 }
265
266 bugGithubID, ok := tt.bug.Snapshot().Operations[0].GetMetadata(keyGithubId)
267 require.True(t, ok)
268
269 importedBug, err := backendTwo.ResolveBugCreateMetadata(keyGithubId, bugGithubID)
270 require.NoError(t, err)
271
272 require.Len(t, importedBug.Snapshot().Operations, tt.numOrOp)
273
274 issueOrigin, ok := importedBug.Snapshot().Operations[0].GetMetadata(keyOrigin)
275 require.True(t, ok)
276 require.Equal(t, issueOrigin, target)
277
278 for _, _ = range importedBug.Snapshot().Operations {
279 // test operations or last bug state ?
280 }
281 })
282 }
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
295func createRepository(project, token string) error {
296 url := fmt.Sprintf("%s/user/repos", githubV3Url)
297
298 params := struct {
299 Name string `json:"name"`
300 Description string `json:"description"`
301 Private bool `json:"private"`
302 HasIssues bool `json:"has_issues"`
303 }{
304 Name: project,
305 Description: "git-bug exporter temporary test repository",
306 Private: true,
307 HasIssues: true,
308 }
309
310 data, err := json.Marshal(params)
311 if err != nil {
312 return err
313 }
314
315 req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
316 if err != nil {
317 return err
318 }
319
320 // need the token for private repositories
321 req.Header.Set("Authorization", fmt.Sprintf("token %s", token))
322
323 client := &http.Client{
324 Timeout: defaultTimeout,
325 }
326
327 resp, err := client.Do(req)
328 if err != nil {
329 return err
330 }
331
332 return resp.Body.Close()
333}
334
335func deleteRepository(project, owner, token string) error {
336 url := fmt.Sprintf("%s/repos/%s/%s", githubV3Url, owner, project)
337
338 req, err := http.NewRequest("DELETE", url, nil)
339 if err != nil {
340 return err
341 }
342
343 // need the token for private repositories
344 req.Header.Set("Authorization", fmt.Sprintf("token %s", token))
345
346 client := &http.Client{
347 Timeout: defaultTimeout,
348 }
349
350 resp, err := client.Do(req)
351 if err != nil {
352 return err
353 }
354
355 defer resp.Body.Close()
356
357 if resp.StatusCode != http.StatusNoContent {
358 return fmt.Errorf("error deleting repository")
359 }
360
361 return nil
362}