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	"encoding/base64"
9	"fmt"
10	"net/http"
11	"net/url"
12	"path/filepath"
13	"testing"
14	"time"
15
16	repo_model "code.gitea.io/gitea/models/repo"
17	"code.gitea.io/gitea/models/unittest"
18	user_model "code.gitea.io/gitea/models/user"
19	"code.gitea.io/gitea/modules/context"
20	"code.gitea.io/gitea/modules/git"
21	"code.gitea.io/gitea/modules/setting"
22	api "code.gitea.io/gitea/modules/structs"
23
24	"github.com/stretchr/testify/assert"
25)
26
27func getCreateFileOptions() api.CreateFileOptions {
28	content := "This is new text"
29	contentEncoded := base64.StdEncoding.EncodeToString([]byte(content))
30	return api.CreateFileOptions{
31		FileOptions: api.FileOptions{
32			BranchName:    "master",
33			NewBranchName: "master",
34			Message:       "Making this new file new/file.txt",
35			Author: api.Identity{
36				Name:  "Anne Doe",
37				Email: "annedoe@example.com",
38			},
39			Committer: api.Identity{
40				Name:  "John Doe",
41				Email: "johndoe@example.com",
42			},
43			Dates: api.CommitDateOptions{
44				Author:    time.Unix(946684810, 0),
45				Committer: time.Unix(978307190, 0),
46			},
47		},
48		Content: contentEncoded,
49	}
50}
51
52func getExpectedFileResponseForCreate(commitID, treePath string) *api.FileResponse {
53	sha := "a635aa942442ddfdba07468cf9661c08fbdf0ebf"
54	encoding := "base64"
55	content := "VGhpcyBpcyBuZXcgdGV4dA=="
56	selfURL := setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath + "?ref=master"
57	htmlURL := setting.AppURL + "user2/repo1/src/branch/master/" + treePath
58	gitURL := setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha
59	downloadURL := setting.AppURL + "user2/repo1/raw/branch/master/" + treePath
60	return &api.FileResponse{
61		Content: &api.ContentsResponse{
62			Name:        filepath.Base(treePath),
63			Path:        treePath,
64			SHA:         sha,
65			Size:        16,
66			Type:        "file",
67			Encoding:    &encoding,
68			Content:     &content,
69			URL:         &selfURL,
70			HTMLURL:     &htmlURL,
71			GitURL:      &gitURL,
72			DownloadURL: &downloadURL,
73			Links: &api.FileLinksResponse{
74				Self:    &selfURL,
75				GitURL:  &gitURL,
76				HTMLURL: &htmlURL,
77			},
78		},
79		Commit: &api.FileCommitResponse{
80			CommitMeta: api.CommitMeta{
81				URL: setting.AppURL + "api/v1/repos/user2/repo1/git/commits/" + commitID,
82				SHA: commitID,
83			},
84			HTMLURL: setting.AppURL + "user2/repo1/commit/" + commitID,
85			Author: &api.CommitUser{
86				Identity: api.Identity{
87					Name:  "Anne Doe",
88					Email: "annedoe@example.com",
89				},
90				Date: "2000-01-01T00:00:10Z",
91			},
92			Committer: &api.CommitUser{
93				Identity: api.Identity{
94					Name:  "John Doe",
95					Email: "johndoe@example.com",
96				},
97				Date: "2000-12-31T23:59:50Z",
98			},
99			Message: "Updates README.md\n",
100		},
101		Verification: &api.PayloadCommitVerification{
102			Verified:  false,
103			Reason:    "gpg.error.not_signed_commit",
104			Signature: "",
105			Payload:   "",
106		},
107	}
108}
109
110func BenchmarkAPICreateFileSmall(b *testing.B) {
111	onGiteaRunTB(b, func(t testing.TB, u *url.URL) {
112		b := t.(*testing.B)
113		user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)             // owner of the repo1 & repo16
114		repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) // public repo
115
116		for n := 0; n < b.N; n++ {
117			treePath := fmt.Sprintf("update/file%d.txt", n)
118			createFileInBranch(user2, repo1, treePath, repo1.DefaultBranch, treePath)
119		}
120	})
121}
122
123func BenchmarkAPICreateFileMedium(b *testing.B) {
124	data := make([]byte, 10*1024*1024)
125
126	onGiteaRunTB(b, func(t testing.TB, u *url.URL) {
127		b := t.(*testing.B)
128		user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)             // owner of the repo1 & repo16
129		repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) // public repo
130
131		b.ResetTimer()
132		for n := 0; n < b.N; n++ {
133			treePath := fmt.Sprintf("update/file%d.txt", n)
134			copy(data, treePath)
135			createFileInBranch(user2, repo1, treePath, repo1.DefaultBranch, treePath)
136		}
137	})
138}
139
140func TestAPICreateFile(t *testing.T) {
141	onGiteaRun(t, func(t *testing.T, u *url.URL) {
142		user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)               // owner of the repo1 & repo16
143		user3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}).(*user_model.User)               // owner of the repo3, is an org
144		user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}).(*user_model.User)               // owner of neither repos
145		repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)   // public repo
146		repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}).(*repo_model.Repository)   // public repo
147		repo16 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 16}).(*repo_model.Repository) // private repo
148		fileID := 0
149
150		// Get user2's token
151		session := loginUser(t, user2.Name)
152		token2 := getTokenForLoggedInUser(t, session)
153		// Get user4's token
154		session = loginUser(t, user4.Name)
155		token4 := getTokenForLoggedInUser(t, session)
156		session = emptyTestSession(t)
157
158		// Test creating a file in repo1 which user2 owns, try both with branch and empty branch
159		for _, branch := range [...]string{
160			"master", // Branch
161			"",       // Empty branch
162		} {
163			createFileOptions := getCreateFileOptions()
164			createFileOptions.BranchName = branch
165			fileID++
166			treePath := fmt.Sprintf("new/file%d.txt", fileID)
167			url := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2)
168			req := NewRequestWithJSON(t, "POST", url, &createFileOptions)
169			resp := session.MakeRequest(t, req, http.StatusCreated)
170			gitRepo, _ := git.OpenRepository(repo1.RepoPath())
171			commitID, _ := gitRepo.GetBranchCommitID(createFileOptions.NewBranchName)
172			expectedFileResponse := getExpectedFileResponseForCreate(commitID, treePath)
173			var fileResponse api.FileResponse
174			DecodeJSON(t, resp, &fileResponse)
175			assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content)
176			assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA)
177			assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL)
178			assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email)
179			assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name)
180			assert.EqualValues(t, expectedFileResponse.Commit.Author.Date, fileResponse.Commit.Author.Date)
181			assert.EqualValues(t, expectedFileResponse.Commit.Committer.Email, fileResponse.Commit.Committer.Email)
182			assert.EqualValues(t, expectedFileResponse.Commit.Committer.Name, fileResponse.Commit.Committer.Name)
183			assert.EqualValues(t, expectedFileResponse.Commit.Committer.Date, fileResponse.Commit.Committer.Date)
184			gitRepo.Close()
185		}
186
187		// Test creating a file in a new branch
188		createFileOptions := getCreateFileOptions()
189		createFileOptions.BranchName = repo1.DefaultBranch
190		createFileOptions.NewBranchName = "new_branch"
191		fileID++
192		treePath := fmt.Sprintf("new/file%d.txt", fileID)
193		url := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2)
194		req := NewRequestWithJSON(t, "POST", url, &createFileOptions)
195		resp := session.MakeRequest(t, req, http.StatusCreated)
196		var fileResponse api.FileResponse
197		DecodeJSON(t, resp, &fileResponse)
198		expectedSHA := "a635aa942442ddfdba07468cf9661c08fbdf0ebf"
199		expectedHTMLURL := fmt.Sprintf(setting.AppURL+"user2/repo1/src/branch/new_branch/new/file%d.txt", fileID)
200		expectedDownloadURL := fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/new_branch/new/file%d.txt", fileID)
201		assert.EqualValues(t, expectedSHA, fileResponse.Content.SHA)
202		assert.EqualValues(t, expectedHTMLURL, *fileResponse.Content.HTMLURL)
203		assert.EqualValues(t, expectedDownloadURL, *fileResponse.Content.DownloadURL)
204		assert.EqualValues(t, createFileOptions.Message+"\n", fileResponse.Commit.Message)
205
206		// Test creating a file without a message
207		createFileOptions = getCreateFileOptions()
208		createFileOptions.Message = ""
209		fileID++
210		treePath = fmt.Sprintf("new/file%d.txt", fileID)
211		url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2)
212		req = NewRequestWithJSON(t, "POST", url, &createFileOptions)
213		resp = session.MakeRequest(t, req, http.StatusCreated)
214		DecodeJSON(t, resp, &fileResponse)
215		expectedMessage := "Add '" + treePath + "'\n"
216		assert.EqualValues(t, expectedMessage, fileResponse.Commit.Message)
217
218		// Test trying to create a file that already exists, should fail
219		createFileOptions = getCreateFileOptions()
220		treePath = "README.md"
221		url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2)
222		req = NewRequestWithJSON(t, "POST", url, &createFileOptions)
223		resp = session.MakeRequest(t, req, http.StatusUnprocessableEntity)
224		expectedAPIError := context.APIError{
225			Message: "repository file already exists [path: " + treePath + "]",
226			URL:     setting.API.SwaggerURL,
227		}
228		var apiError context.APIError
229		DecodeJSON(t, resp, &apiError)
230		assert.Equal(t, expectedAPIError, apiError)
231
232		// Test creating a file in repo1 by user4 who does not have write access
233		createFileOptions = getCreateFileOptions()
234		fileID++
235		treePath = fmt.Sprintf("new/file%d.txt", fileID)
236		url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo16.Name, treePath, token4)
237		req = NewRequestWithJSON(t, "POST", url, &createFileOptions)
238		session.MakeRequest(t, req, http.StatusNotFound)
239
240		// Tests a repo with no token given so will fail
241		createFileOptions = getCreateFileOptions()
242		fileID++
243		treePath = fmt.Sprintf("new/file%d.txt", fileID)
244		url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo16.Name, treePath)
245		req = NewRequestWithJSON(t, "POST", url, &createFileOptions)
246		session.MakeRequest(t, req, http.StatusNotFound)
247
248		// Test using access token for a private repo that the user of the token owns
249		createFileOptions = getCreateFileOptions()
250		fileID++
251		treePath = fmt.Sprintf("new/file%d.txt", fileID)
252		url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo16.Name, treePath, token2)
253		req = NewRequestWithJSON(t, "POST", url, &createFileOptions)
254		session.MakeRequest(t, req, http.StatusCreated)
255
256		// Test using org repo "user3/repo3" where user2 is a collaborator
257		createFileOptions = getCreateFileOptions()
258		fileID++
259		treePath = fmt.Sprintf("new/file%d.txt", fileID)
260		url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user3.Name, repo3.Name, treePath, token2)
261		req = NewRequestWithJSON(t, "POST", url, &createFileOptions)
262		session.MakeRequest(t, req, http.StatusCreated)
263
264		// Test using org repo "user3/repo3" with no user token
265		createFileOptions = getCreateFileOptions()
266		fileID++
267		treePath = fmt.Sprintf("new/file%d.txt", fileID)
268		url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user3.Name, repo3.Name, treePath)
269		req = NewRequestWithJSON(t, "POST", url, &createFileOptions)
270		session.MakeRequest(t, req, http.StatusNotFound)
271
272		// Test using repo "user2/repo1" where user4 is a NOT collaborator
273		createFileOptions = getCreateFileOptions()
274		fileID++
275		treePath = fmt.Sprintf("new/file%d.txt", fileID)
276		url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token4)
277		req = NewRequestWithJSON(t, "POST", url, &createFileOptions)
278		session.MakeRequest(t, req, http.StatusForbidden)
279	})
280}
281