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