export_test.go

  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(keyGithubId)
237					require.True(t, haveIDMetadata)
238
239					_, haveURLMetada := op.GetMetadata(keyGithubUrl)
240					require.True(t, haveURLMetada)
241				}
242			}
243
244			// get bug github ID
245			bugGithubID, ok := tt.bug.Snapshot().GetCreateMetadata(keyGithubId)
246			require.True(t, ok)
247
248			// retrieve bug from backendTwo
249			importedBug, err := backendTwo.ResolveBugCreateMetadata(keyGithubId, 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(keyOrigin)
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}