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