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 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.NewBug("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.NewBug("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.NewRepoCache(repo)
145 require.NoError(t, err)
146
147 // set author identity
148 login := "identity-test"
149 author, err := backend.NewIdentity("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 token := auth.NewToken(target, envToken)
162 token.SetMetadata(auth.MetaKeyLogin, login)
163 err = auth.Store(repo, token)
164 require.NoError(t, err)
165
166 tests := testCases(t, backend)
167
168 // generate project name
169 projectName := generateRepoName()
170
171 // create target Github repository
172 err = createRepository(projectName, envToken)
173 require.NoError(t, err)
174
175 fmt.Println("created repository", projectName)
176
177 // Let Github handle the repo creation and update all their internal caches.
178 // Avoid HTTP error 404 retrieving repository node id
179 time.Sleep(10 * time.Second)
180
181 // Make sure to remove the Github repository when the test end
182 defer func(t *testing.T) {
183 if err := deleteRepository(projectName, envUser, envToken); err != nil {
184 t.Fatal(err)
185 }
186 fmt.Println("deleted repository:", projectName)
187 }(t)
188
189 interrupt.RegisterCleaner(func() error {
190 return deleteRepository(projectName, envUser, envToken)
191 })
192
193 ctx := context.Background()
194
195 // initialize exporter
196 exporter := &githubExporter{}
197 err = exporter.Init(ctx, backend, core.Configuration{
198 confKeyOwner: envUser,
199 confKeyProject: projectName,
200 confKeyDefaultLogin: login,
201 })
202 require.NoError(t, err)
203
204 start := time.Now()
205
206 // export all bugs
207 exportEvents, err := exporter.ExportAll(ctx, backend, time.Time{})
208 require.NoError(t, err)
209
210 for result := range exportEvents {
211 require.NoError(t, result.Err)
212 }
213 require.NoError(t, err)
214
215 fmt.Printf("test repository exported in %f seconds\n", time.Since(start).Seconds())
216
217 repoTwo := repository.CreateGoGitTestRepo(t, false)
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.(dag.OperationDoesntChangeSnapshot); !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}