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