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