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	// 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}