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