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