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