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