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