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