export_test.go

  1package github
  2
  3import (
  4	"bytes"
  5	"encoding/json"
  6	"fmt"
  7	"math/rand"
  8	"net/http"
  9	"os"
 10	"testing"
 11	"time"
 12
 13	"github.com/stretchr/testify/require"
 14
 15	"github.com/MichaelMure/git-bug/bridge/core"
 16	"github.com/MichaelMure/git-bug/bug"
 17	"github.com/MichaelMure/git-bug/cache"
 18	"github.com/MichaelMure/git-bug/repository"
 19	"github.com/MichaelMure/git-bug/util/interrupt"
 20)
 21
 22const (
 23	testRepoBaseName = "git-bug-test-github-exporter"
 24)
 25
 26type testCase struct {
 27	name    string
 28	bug     *cache.BugCache
 29	numOrOp int // number of original operations
 30}
 31
 32func testCases(t *testing.T, repo *cache.RepoCache, identity *cache.IdentityCache) []*testCase {
 33	// simple bug
 34	simpleBug, _, err := repo.NewBug("simple bug", "new bug")
 35	require.NoError(t, err)
 36
 37	// bug with comments
 38	bugWithComments, _, err := repo.NewBug("bug with comments", "new bug")
 39	require.NoError(t, err)
 40
 41	_, err = bugWithComments.AddComment("new comment")
 42	require.NoError(t, err)
 43
 44	// bug with label changes
 45	bugLabelChange, _, err := repo.NewBug("bug label change", "new bug")
 46	require.NoError(t, err)
 47
 48	_, _, err = bugLabelChange.ChangeLabels([]string{"bug"}, nil)
 49	require.NoError(t, err)
 50
 51	_, _, err = bugLabelChange.ChangeLabels([]string{"core"}, nil)
 52	require.NoError(t, err)
 53
 54	_, _, err = bugLabelChange.ChangeLabels(nil, []string{"bug"})
 55	require.NoError(t, err)
 56
 57	// bug with comments editions
 58	bugWithCommentEditions, createOp, err := repo.NewBug("bug with comments editions", "new bug")
 59	require.NoError(t, err)
 60
 61	createOpHash, err := createOp.Hash()
 62	require.NoError(t, err)
 63
 64	_, err = bugWithCommentEditions.EditComment(createOpHash, "first comment edited")
 65	require.NoError(t, err)
 66
 67	commentOp, err := bugWithCommentEditions.AddComment("first comment")
 68	require.NoError(t, err)
 69
 70	commentOpHash, err := commentOp.Hash()
 71	require.NoError(t, err)
 72
 73	_, err = bugWithCommentEditions.EditComment(commentOpHash, "first comment edited")
 74	require.NoError(t, err)
 75
 76	// bug status changed
 77	bugStatusChanged, _, err := repo.NewBug("bug status changed", "new bug")
 78	require.NoError(t, err)
 79
 80	_, err = bugStatusChanged.Close()
 81	require.NoError(t, err)
 82
 83	_, err = bugStatusChanged.Open()
 84	require.NoError(t, err)
 85
 86	// bug title changed
 87	bugTitleEdited, _, err := repo.NewBug("bug title edited", "new bug")
 88	require.NoError(t, err)
 89
 90	_, err = bugTitleEdited.SetTitle("bug title edited again")
 91	require.NoError(t, err)
 92
 93	return []*testCase{
 94		&testCase{
 95			name:    "simple bug",
 96			bug:     simpleBug,
 97			numOrOp: 1,
 98		},
 99		&testCase{
100			name:    "bug with comments",
101			bug:     bugWithComments,
102			numOrOp: 2,
103		},
104		&testCase{
105			name:    "bug label change",
106			bug:     bugLabelChange,
107			numOrOp: 4,
108		},
109		&testCase{
110			name:    "bug with comment editions",
111			bug:     bugWithCommentEditions,
112			numOrOp: 4,
113		},
114		&testCase{
115			name:    "bug changed status",
116			bug:     bugStatusChanged,
117			numOrOp: 3,
118		},
119		&testCase{
120			name:    "bug title edited",
121			bug:     bugTitleEdited,
122			numOrOp: 2,
123		},
124	}
125}
126
127func TestPushPull(t *testing.T) {
128	// repo owner
129	user := os.Getenv("GITHUB_TEST_USER")
130
131	// token must have 'repo' and 'delete_repo' scopes
132	token := os.Getenv("GITHUB_TOKEN_ADMIN")
133	if token == "" {
134		t.Skip("Env var GITHUB_TOKEN_ADMIN missing")
135	}
136
137	// create repo backend
138	repo := repository.CreateTestRepo(false)
139	defer repository.CleanupTestRepos(t, repo)
140
141	backend, err := cache.NewRepoCache(repo)
142	require.NoError(t, err)
143
144	// set author identity
145	author, err := backend.NewIdentity("test identity", "test@test.org")
146	require.NoError(t, err)
147
148	err = backend.SetUserIdentity(author)
149	require.NoError(t, err)
150
151	defer backend.Close()
152	interrupt.RegisterCleaner(backend.Close)
153
154	tests := testCases(t, backend, author)
155
156	// generate project name
157	projectName := generateRepoName()
158
159	// create target Github repository
160	err = createRepository(projectName, token)
161	require.NoError(t, err)
162
163	fmt.Println("created repository", projectName)
164
165	// Make sure to remove the Github repository when the test end
166	defer func(t *testing.T) {
167		if err := deleteRepository(projectName, user, token); err != nil {
168			t.Fatal(err)
169		}
170		fmt.Println("deleted repository:", projectName)
171	}(t)
172
173	interrupt.RegisterCleaner(func() error {
174		return deleteRepository(projectName, user, token)
175	})
176
177	// initialize exporter
178	exporter := &githubExporter{}
179	err = exporter.Init(core.Configuration{
180		keyOwner:   user,
181		keyProject: projectName,
182		keyToken:   token,
183	})
184	require.NoError(t, err)
185
186	start := time.Now()
187
188	// export all bugs
189	events, err := exporter.ExportAll(backend, time.Time{})
190	require.NoError(t, err)
191
192	for result := range events {
193		require.NoError(t, result.Err)
194	}
195	require.NoError(t, err)
196
197	fmt.Printf("test repository exported in %f seconds\n", time.Since(start).Seconds())
198
199	repoTwo := repository.CreateTestRepo(false)
200	defer repository.CleanupTestRepos(t, repoTwo)
201
202	// create a second backend
203	backendTwo, err := cache.NewRepoCache(repoTwo)
204	require.NoError(t, err)
205
206	importer := &githubImporter{}
207	err = importer.Init(core.Configuration{
208		keyOwner:   user,
209		keyProject: projectName,
210		keyToken:   token,
211	})
212	require.NoError(t, err)
213
214	// import all exported bugs to the second backend
215	err = importer.ImportAll(backendTwo, time.Time{})
216	require.NoError(t, err)
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}