1// Copyright 2019 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5package integrations
6
7import (
8	"context"
9	"fmt"
10	"net/http"
11	"net/http/httptest"
12	"net/url"
13	"os"
14	"testing"
15	"time"
16
17	"code.gitea.io/gitea/models/perm"
18	repo_model "code.gitea.io/gitea/models/repo"
19	"code.gitea.io/gitea/modules/json"
20	"code.gitea.io/gitea/modules/queue"
21	api "code.gitea.io/gitea/modules/structs"
22	"code.gitea.io/gitea/services/forms"
23
24	"github.com/stretchr/testify/assert"
25)
26
27type APITestContext struct {
28	Reponame     string
29	Session      *TestSession
30	Token        string
31	Username     string
32	ExpectedCode int
33}
34
35func NewAPITestContext(t *testing.T, username, reponame string) APITestContext {
36	session := loginUser(t, username)
37	token := getTokenForLoggedInUser(t, session)
38	return APITestContext{
39		Session:  session,
40		Token:    token,
41		Username: username,
42		Reponame: reponame,
43	}
44}
45
46func (ctx APITestContext) GitPath() string {
47	return fmt.Sprintf("%s/%s.git", ctx.Username, ctx.Reponame)
48}
49
50func doAPICreateRepository(ctx APITestContext, empty bool, callback ...func(*testing.T, api.Repository)) func(*testing.T) {
51	return func(t *testing.T) {
52		createRepoOption := &api.CreateRepoOption{
53			AutoInit:    !empty,
54			Description: "Temporary repo",
55			Name:        ctx.Reponame,
56			Private:     true,
57			Template:    true,
58			Gitignores:  "",
59			License:     "WTFPL",
60			Readme:      "Default",
61		}
62		req := NewRequestWithJSON(t, "POST", "/api/v1/user/repos?token="+ctx.Token, createRepoOption)
63		if ctx.ExpectedCode != 0 {
64			ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
65			return
66		}
67		resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
68
69		var repository api.Repository
70		DecodeJSON(t, resp, &repository)
71		if len(callback) > 0 {
72			callback[0](t, repository)
73		}
74	}
75}
76
77func doAPIEditRepository(ctx APITestContext, editRepoOption *api.EditRepoOption, callback ...func(*testing.T, api.Repository)) func(*testing.T) {
78	return func(t *testing.T) {
79		req := NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), ctx.Token), editRepoOption)
80		if ctx.ExpectedCode != 0 {
81			ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
82			return
83		}
84		resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
85
86		var repository api.Repository
87		DecodeJSON(t, resp, &repository)
88		if len(callback) > 0 {
89			callback[0](t, repository)
90		}
91	}
92}
93
94func doAPIAddCollaborator(ctx APITestContext, username string, mode perm.AccessMode) func(*testing.T) {
95	return func(t *testing.T) {
96		permission := "read"
97
98		if mode == perm.AccessModeAdmin {
99			permission = "admin"
100		} else if mode > perm.AccessModeRead {
101			permission = "write"
102		}
103		addCollaboratorOption := &api.AddCollaboratorOption{
104			Permission: &permission,
105		}
106		req := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/collaborators/%s?token=%s", ctx.Username, ctx.Reponame, username, ctx.Token), addCollaboratorOption)
107		if ctx.ExpectedCode != 0 {
108			ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
109			return
110		}
111		ctx.Session.MakeRequest(t, req, http.StatusNoContent)
112	}
113}
114
115func doAPIForkRepository(ctx APITestContext, username string, callback ...func(*testing.T, api.Repository)) func(*testing.T) {
116	return func(t *testing.T) {
117		createForkOption := &api.CreateForkOption{}
118		req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks?token=%s", username, ctx.Reponame, ctx.Token), createForkOption)
119		if ctx.ExpectedCode != 0 {
120			ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
121			return
122		}
123		resp := ctx.Session.MakeRequest(t, req, http.StatusAccepted)
124		var repository api.Repository
125		DecodeJSON(t, resp, &repository)
126		if len(callback) > 0 {
127			callback[0](t, repository)
128		}
129	}
130}
131
132func doAPIGetRepository(ctx APITestContext, callback ...func(*testing.T, api.Repository)) func(*testing.T) {
133	return func(t *testing.T) {
134		urlStr := fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", ctx.Username, ctx.Reponame, ctx.Token)
135
136		req := NewRequest(t, "GET", urlStr)
137		if ctx.ExpectedCode != 0 {
138			ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
139			return
140		}
141		resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
142
143		var repository api.Repository
144		DecodeJSON(t, resp, &repository)
145		if len(callback) > 0 {
146			callback[0](t, repository)
147		}
148	}
149}
150
151func doAPIDeleteRepository(ctx APITestContext) func(*testing.T) {
152	return func(t *testing.T) {
153		urlStr := fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", ctx.Username, ctx.Reponame, ctx.Token)
154
155		req := NewRequest(t, "DELETE", urlStr)
156		if ctx.ExpectedCode != 0 {
157			ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
158			return
159		}
160		ctx.Session.MakeRequest(t, req, http.StatusNoContent)
161	}
162}
163
164func doAPICreateUserKey(ctx APITestContext, keyname, keyFile string, callback ...func(*testing.T, api.PublicKey)) func(*testing.T) {
165	return func(t *testing.T) {
166		urlStr := fmt.Sprintf("/api/v1/user/keys?token=%s", ctx.Token)
167
168		dataPubKey, err := os.ReadFile(keyFile + ".pub")
169		assert.NoError(t, err)
170		req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateKeyOption{
171			Title: keyname,
172			Key:   string(dataPubKey),
173		})
174		if ctx.ExpectedCode != 0 {
175			ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
176			return
177		}
178		resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
179		var publicKey api.PublicKey
180		DecodeJSON(t, resp, &publicKey)
181		if len(callback) > 0 {
182			callback[0](t, publicKey)
183		}
184	}
185}
186
187func doAPIDeleteUserKey(ctx APITestContext, keyID int64) func(*testing.T) {
188	return func(t *testing.T) {
189		urlStr := fmt.Sprintf("/api/v1/user/keys/%d?token=%s", keyID, ctx.Token)
190
191		req := NewRequest(t, "DELETE", urlStr)
192		if ctx.ExpectedCode != 0 {
193			ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
194			return
195		}
196		ctx.Session.MakeRequest(t, req, http.StatusNoContent)
197	}
198}
199
200func doAPICreateDeployKey(ctx APITestContext, keyname, keyFile string, readOnly bool) func(*testing.T) {
201	return func(t *testing.T) {
202		urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/keys?token=%s", ctx.Username, ctx.Reponame, ctx.Token)
203
204		dataPubKey, err := os.ReadFile(keyFile + ".pub")
205		assert.NoError(t, err)
206		req := NewRequestWithJSON(t, "POST", urlStr, api.CreateKeyOption{
207			Title:    keyname,
208			Key:      string(dataPubKey),
209			ReadOnly: readOnly,
210		})
211
212		if ctx.ExpectedCode != 0 {
213			ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
214			return
215		}
216		ctx.Session.MakeRequest(t, req, http.StatusCreated)
217	}
218}
219
220func doAPICreatePullRequest(ctx APITestContext, owner, repo, baseBranch, headBranch string) func(*testing.T) (api.PullRequest, error) {
221	return func(t *testing.T) (api.PullRequest, error) {
222		urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls?token=%s",
223			owner, repo, ctx.Token)
224		req := NewRequestWithJSON(t, http.MethodPost, urlStr, &api.CreatePullRequestOption{
225			Head:  headBranch,
226			Base:  baseBranch,
227			Title: fmt.Sprintf("create a pr from %s to %s", headBranch, baseBranch),
228		})
229
230		expected := 201
231		if ctx.ExpectedCode != 0 {
232			expected = ctx.ExpectedCode
233		}
234		resp := ctx.Session.MakeRequest(t, req, expected)
235
236		decoder := json.NewDecoder(resp.Body)
237		pr := api.PullRequest{}
238		err := decoder.Decode(&pr)
239		return pr, err
240	}
241}
242
243func doAPIGetPullRequest(ctx APITestContext, owner, repo string, index int64) func(*testing.T) (api.PullRequest, error) {
244	return func(t *testing.T) (api.PullRequest, error) {
245		urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d?token=%s",
246			owner, repo, index, ctx.Token)
247		req := NewRequest(t, http.MethodGet, urlStr)
248
249		expected := 200
250		if ctx.ExpectedCode != 0 {
251			expected = ctx.ExpectedCode
252		}
253		resp := ctx.Session.MakeRequest(t, req, expected)
254
255		decoder := json.NewDecoder(resp.Body)
256		pr := api.PullRequest{}
257		err := decoder.Decode(&pr)
258		return pr, err
259	}
260}
261
262func doAPIMergePullRequest(ctx APITestContext, owner, repo string, index int64) func(*testing.T) {
263	return func(t *testing.T) {
264		urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge?token=%s",
265			owner, repo, index, ctx.Token)
266
267		var req *http.Request
268		var resp *httptest.ResponseRecorder
269
270		for i := 0; i < 6; i++ {
271			req = NewRequestWithJSON(t, http.MethodPost, urlStr, &forms.MergePullRequestForm{
272				MergeMessageField: "doAPIMergePullRequest Merge",
273				Do:                string(repo_model.MergeStyleMerge),
274			})
275
276			resp = ctx.Session.MakeRequest(t, req, NoExpectedStatus)
277
278			if resp.Code != http.StatusMethodNotAllowed {
279				break
280			}
281			err := api.APIError{}
282			DecodeJSON(t, resp, &err)
283			assert.EqualValues(t, "Please try again later", err.Message)
284			queue.GetManager().FlushAll(context.Background(), 5*time.Second)
285			<-time.After(1 * time.Second)
286		}
287
288		expected := ctx.ExpectedCode
289		if expected == 0 {
290			expected = 200
291		}
292
293		if !assert.EqualValues(t, expected, resp.Code,
294			"Request: %s %s", req.Method, req.URL.String()) {
295			logUnexpectedResponse(t, resp)
296		}
297	}
298}
299
300func doAPIManuallyMergePullRequest(ctx APITestContext, owner, repo, commitID string, index int64) func(*testing.T) {
301	return func(t *testing.T) {
302		urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge?token=%s",
303			owner, repo, index, ctx.Token)
304		req := NewRequestWithJSON(t, http.MethodPost, urlStr, &forms.MergePullRequestForm{
305			Do:            string(repo_model.MergeStyleManuallyMerged),
306			MergeCommitID: commitID,
307		})
308
309		if ctx.ExpectedCode != 0 {
310			ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
311			return
312		}
313		ctx.Session.MakeRequest(t, req, 200)
314	}
315}
316
317func doAPIGetBranch(ctx APITestContext, branch string, callback ...func(*testing.T, api.Branch)) func(*testing.T) {
318	return func(t *testing.T) {
319		req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/branches/%s?token=%s", ctx.Username, ctx.Reponame, branch, ctx.Token)
320		if ctx.ExpectedCode != 0 {
321			ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
322			return
323		}
324		resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
325
326		var branch api.Branch
327		DecodeJSON(t, resp, &branch)
328		if len(callback) > 0 {
329			callback[0](t, branch)
330		}
331	}
332}
333
334func doAPICreateFile(ctx APITestContext, treepath string, options *api.CreateFileOptions, callback ...func(*testing.T, api.FileResponse)) func(*testing.T) {
335	return func(t *testing.T) {
336		url := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", ctx.Username, ctx.Reponame, treepath, ctx.Token)
337		req := NewRequestWithJSON(t, "POST", url, &options)
338		if ctx.ExpectedCode != 0 {
339			ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
340			return
341		}
342		resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
343
344		var contents api.FileResponse
345		DecodeJSON(t, resp, &contents)
346		if len(callback) > 0 {
347			callback[0](t, contents)
348		}
349	}
350}
351
352func doAPICreateOrganization(ctx APITestContext, options *api.CreateOrgOption, callback ...func(*testing.T, api.Organization)) func(t *testing.T) {
353	return func(t *testing.T) {
354		url := fmt.Sprintf("/api/v1/orgs?token=%s", ctx.Token)
355
356		req := NewRequestWithJSON(t, "POST", url, &options)
357		if ctx.ExpectedCode != 0 {
358			ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
359			return
360		}
361		resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
362
363		var contents api.Organization
364		DecodeJSON(t, resp, &contents)
365		if len(callback) > 0 {
366			callback[0](t, contents)
367		}
368	}
369}
370
371func doAPICreateOrganizationRepository(ctx APITestContext, orgName string, options *api.CreateRepoOption, callback ...func(*testing.T, api.Repository)) func(t *testing.T) {
372	return func(t *testing.T) {
373		url := fmt.Sprintf("/api/v1/orgs/%s/repos?token=%s", orgName, ctx.Token)
374
375		req := NewRequestWithJSON(t, "POST", url, &options)
376		if ctx.ExpectedCode != 0 {
377			ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
378			return
379		}
380		resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
381
382		var contents api.Repository
383		DecodeJSON(t, resp, &contents)
384		if len(callback) > 0 {
385			callback[0](t, contents)
386		}
387	}
388}
389
390func doAPICreateOrganizationTeam(ctx APITestContext, orgName string, options *api.CreateTeamOption, callback ...func(*testing.T, api.Team)) func(t *testing.T) {
391	return func(t *testing.T) {
392		url := fmt.Sprintf("/api/v1/orgs/%s/teams?token=%s", orgName, ctx.Token)
393
394		req := NewRequestWithJSON(t, "POST", url, &options)
395		if ctx.ExpectedCode != 0 {
396			ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
397			return
398		}
399		resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
400
401		var contents api.Team
402		DecodeJSON(t, resp, &contents)
403		if len(callback) > 0 {
404			callback[0](t, contents)
405		}
406	}
407}
408
409func doAPIAddUserToOrganizationTeam(ctx APITestContext, teamID int64, username string) func(t *testing.T) {
410	return func(t *testing.T) {
411		url := fmt.Sprintf("/api/v1/teams/%d/members/%s?token=%s", teamID, username, ctx.Token)
412
413		req := NewRequest(t, "PUT", url)
414		if ctx.ExpectedCode != 0 {
415			ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
416			return
417		}
418		ctx.Session.MakeRequest(t, req, http.StatusNoContent)
419	}
420}
421
422func doAPIAddRepoToOrganizationTeam(ctx APITestContext, teamID int64, orgName, repoName string) func(t *testing.T) {
423	return func(t *testing.T) {
424		url := fmt.Sprintf("/api/v1/teams/%d/repos/%s/%s?token=%s", teamID, orgName, repoName, ctx.Token)
425
426		req := NewRequest(t, "PUT", url)
427		if ctx.ExpectedCode != 0 {
428			ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
429			return
430		}
431		ctx.Session.MakeRequest(t, req, http.StatusNoContent)
432	}
433}
434