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