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