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 TestGithubPushPull(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.CreateGoGitTestRepo(false)
141 defer repository.CleanupTestRepos(repo)
142
143 backend, err := cache.NewRepoCache(repo)
144 require.NoError(t, err)
145
146 // set author identity
147 login := "identity-test"
148 author, err := backend.NewIdentity("test identity", "test@test.org")
149 require.NoError(t, err)
150 author.SetMetadata(metaKeyGithubLogin, login)
151 err = author.Commit()
152 require.NoError(t, err)
153
154 err = backend.SetUserIdentity(author)
155 require.NoError(t, err)
156
157 defer backend.Close()
158 interrupt.RegisterCleaner(backend.Close)
159
160 token := auth.NewToken(target, envToken)
161 token.SetMetadata(auth.MetaKeyLogin, login)
162 err = auth.Store(repo, token)
163 require.NoError(t, err)
164
165 tests := testCases(t, backend)
166
167 // generate project name
168 projectName := generateRepoName()
169
170 // create target Github repository
171 err = createRepository(projectName, envToken)
172 require.NoError(t, err)
173
174 fmt.Println("created repository", projectName)
175
176 // Let Github handle the repo creation and update all their internal caches.
177 // Avoid HTTP error 404 retrieving repository node id
178 time.Sleep(10 * time.Second)
179
180 // Make sure to remove the Github repository when the test end
181 defer func(t *testing.T) {
182 if err := deleteRepository(projectName, envUser, envToken); err != nil {
183 t.Fatal(err)
184 }
185 fmt.Println("deleted repository:", projectName)
186 }(t)
187
188 interrupt.RegisterCleaner(func() error {
189 return deleteRepository(projectName, envUser, envToken)
190 })
191
192 ctx := context.Background()
193
194 // initialize exporter
195 exporter := &githubExporter{}
196 err = exporter.Init(ctx, backend, core.Configuration{
197 confKeyOwner: envUser,
198 confKeyProject: projectName,
199 confKeyDefaultLogin: login,
200 })
201 require.NoError(t, err)
202
203 start := time.Now()
204
205 // export all bugs
206 exportEvents, err := exporter.ExportAll(ctx, backend, time.Time{})
207 require.NoError(t, err)
208
209 for result := range exportEvents {
210 require.NoError(t, result.Err)
211 }
212 require.NoError(t, err)
213
214 fmt.Printf("test repository exported in %f seconds\n", time.Since(start).Seconds())
215
216 repoTwo := repository.CreateGoGitTestRepo(false)
217 defer repository.CleanupTestRepos(repoTwo)
218
219 // create a second backend
220 backendTwo, err := cache.NewRepoCache(repoTwo)
221 require.NoError(t, err)
222
223 importer := &githubImporter{}
224 err = importer.Init(ctx, backend, core.Configuration{
225 confKeyOwner: envUser,
226 confKeyProject: projectName,
227 confKeyDefaultLogin: login,
228 })
229 require.NoError(t, err)
230
231 // import all exported bugs to the second backend
232 importEvents, err := importer.ImportAll(ctx, backendTwo, time.Time{})
233 require.NoError(t, err)
234
235 for result := range importEvents {
236 require.NoError(t, result.Err)
237 }
238
239 require.Len(t, backendTwo.AllBugsIds(), len(tests))
240
241 for _, tt := range tests {
242 t.Run(tt.name, func(t *testing.T) {
243 // for each operation a SetMetadataOperation will be added
244 // so number of operations should double
245 require.Len(t, tt.bug.Snapshot().Operations, tt.numOrOp*2)
246
247 // verify operation have correct metadata
248 for _, op := range tt.bug.Snapshot().Operations {
249 // Check if the originals operations (*not* SetMetadata) are tagged properly
250 if _, ok := op.(*bug.SetMetadataOperation); !ok {
251 _, haveIDMetadata := op.GetMetadata(metaKeyGithubId)
252 require.True(t, haveIDMetadata)
253
254 _, haveURLMetada := op.GetMetadata(metaKeyGithubUrl)
255 require.True(t, haveURLMetada)
256 }
257 }
258
259 // get bug github ID
260 bugGithubID, ok := tt.bug.Snapshot().GetCreateMetadata(metaKeyGithubId)
261 require.True(t, ok)
262
263 // retrieve bug from backendTwo
264 importedBug, err := backendTwo.ResolveBugCreateMetadata(metaKeyGithubId, bugGithubID)
265 require.NoError(t, err)
266
267 // verify bug have same number of original operations
268 require.Len(t, importedBug.Snapshot().Operations, tt.numOrOp)
269
270 // verify bugs are tagged with origin=github
271 issueOrigin, ok := importedBug.Snapshot().GetCreateMetadata(core.MetaKeyOrigin)
272 require.True(t, ok)
273 require.Equal(t, issueOrigin, target)
274
275 // TODO: maybe more tests to ensure bug final state
276 })
277 }
278}
279
280func generateRepoName() string {
281 rand.Seed(time.Now().UnixNano())
282 var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
283 b := make([]rune, 8)
284 for i := range b {
285 b[i] = letterRunes[rand.Intn(len(letterRunes))]
286 }
287 return fmt.Sprintf("%s-%s", testRepoBaseName, string(b))
288}
289
290// create repository need a token with scope 'repo'
291func createRepository(project, token string) error {
292 // This function use the V3 Github API because repository creation is not supported yet on the V4 API.
293 url := fmt.Sprintf("%s/user/repos", githubV3Url)
294
295 params := struct {
296 Name string `json:"name"`
297 Description string `json:"description"`
298 Private bool `json:"private"`
299 HasIssues bool `json:"has_issues"`
300 }{
301 Name: project,
302 Description: "git-bug exporter temporary test repository",
303 Private: true,
304 HasIssues: true,
305 }
306
307 data, err := json.Marshal(params)
308 if err != nil {
309 return err
310 }
311
312 req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
313 if err != nil {
314 return err
315 }
316
317 // need the token for private repositories
318 req.Header.Set("Authorization", fmt.Sprintf("token %s", token))
319
320 client := &http.Client{
321 Timeout: defaultTimeout,
322 }
323
324 resp, err := client.Do(req)
325 if err != nil {
326 return err
327 }
328
329 return resp.Body.Close()
330}
331
332// delete repository need a token with scope 'delete_repo'
333func deleteRepository(project, owner, token string) error {
334 // This function use the V3 Github API because repository removal is not supported yet on the V4 API.
335 url := fmt.Sprintf("%s/repos/%s/%s", githubV3Url, owner, project)
336
337 req, err := http.NewRequest("DELETE", url, nil)
338 if err != nil {
339 return err
340 }
341
342 // need the token for private repositories
343 req.Header.Set("Authorization", fmt.Sprintf("token %s", token))
344
345 client := &http.Client{
346 Timeout: defaultTimeout,
347 }
348
349 resp, err := client.Do(req)
350 if err != nil {
351 return err
352 }
353
354 defer resp.Body.Close()
355
356 if resp.StatusCode != http.StatusNoContent {
357 return fmt.Errorf("error deleting repository")
358 }
359
360 return nil
361}