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