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