| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461 |
- // Copyright 2017 The Gitea Authors. All rights reserved.
- // SPDX-License-Identifier: MIT
-
- package integration
-
- import (
- "bytes"
- "fmt"
- "io"
- "maps"
- "mime/multipart"
- "net/http"
- "net/http/httptest"
- "net/url"
- "path"
- "strings"
- "testing"
-
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/test"
- "code.gitea.io/gitea/modules/translation"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/tests"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- )
-
- func TestEditor(t *testing.T) {
- onGiteaRun(t, func(t *testing.T, u *url.URL) {
- sessionUser2 := loginUser(t, "user2")
- t.Run("EditFileNotAllowed", testEditFileNotAllowed)
- t.Run("DiffPreview", testEditorDiffPreview)
- t.Run("CreateFile", testEditorCreateFile)
- t.Run("EditFile", func(t *testing.T) {
- testEditFile(t, sessionUser2, "user2", "repo1", "master", "README.md", "Hello, World (direct)\n")
- testEditFileToNewBranch(t, sessionUser2, "user2", "repo1", "master", "feature/test", "README.md", "Hello, World (commit-to-new-branch)\n")
- })
- t.Run("PatchFile", testEditorPatchFile)
- t.Run("DeleteFile", func(t *testing.T) {
- viewLink := "/user2/repo1/src/branch/branch2/README.md"
- sessionUser2.MakeRequest(t, NewRequest(t, "GET", viewLink), http.StatusOK)
- testEditorActionPostRequest(t, sessionUser2, "/user2/repo1/_delete/branch2/README.md", map[string]string{"commit_choice": "direct"})
- sessionUser2.MakeRequest(t, NewRequest(t, "GET", viewLink), http.StatusNotFound)
- })
- t.Run("ForkToEditFile", func(t *testing.T) {
- testForkToEditFile(t, loginUser(t, "user4"), "user4", "user2", "repo1", "master", "README.md")
- })
- t.Run("WebGitCommitEmail", testEditorWebGitCommitEmail)
- t.Run("ProtectedBranch", testEditorProtectedBranch)
- })
- }
-
- func testEditorCreateFile(t *testing.T) {
- session := loginUser(t, "user2")
- testCreateFile(t, session, "user2", "repo1", "master", "", "test.txt", "Content")
- testEditorActionPostRequestError(t, session, "/user2/repo1/_new/master/", map[string]string{
- "tree_path": "test.txt",
- "commit_choice": "direct",
- "new_branch_name": "master",
- }, `A file named "test.txt" already exists in this repository.`)
- testEditorActionPostRequestError(t, session, "/user2/repo1/_new/master/", map[string]string{
- "tree_path": "test.txt",
- "commit_choice": "commit-to-new-branch",
- "new_branch_name": "master",
- }, `Branch "master" already exists in this repository.`)
- }
-
- func testCreateFile(t *testing.T, session *TestSession, user, repo, baseBranchName, newBranchName, filePath, content string) {
- commitChoice := "direct"
- if newBranchName != "" && newBranchName != baseBranchName {
- commitChoice = "commit-to-new-branch"
- }
- testEditorActionEdit(t, session, user, repo, "_new", baseBranchName, "", map[string]string{
- "tree_path": filePath,
- "content": content,
- "commit_choice": commitChoice,
- "new_branch_name": newBranchName,
- })
- }
-
- func testEditorProtectedBranch(t *testing.T) {
- session := loginUser(t, "user2")
- // Change the "master" branch to "protected"
- req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{
- "_csrf": GetUserCSRFToken(t, session),
- "rule_name": "master",
- "enable_push": "true",
- })
- session.MakeRequest(t, req, http.StatusSeeOther)
- flashMsg := session.GetCookieFlashMessage()
- assert.Equal(t, `Branch protection for rule "master" has been updated.`, flashMsg.SuccessMsg)
-
- // Try to commit a file to the "master" branch and it should fail
- resp := testEditorActionPostRequest(t, session, "/user2/repo1/_new/master/", map[string]string{"tree_path": "test-protected-branch.txt", "commit_choice": "direct"})
- assert.Equal(t, http.StatusBadRequest, resp.Code)
- assert.Equal(t, `Cannot commit to protected branch "master".`, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage)
- }
-
- func testEditorActionPostRequest(t *testing.T, session *TestSession, requestPath string, params map[string]string) *httptest.ResponseRecorder {
- req := NewRequest(t, "GET", requestPath)
- resp := session.MakeRequest(t, req, http.StatusOK)
- htmlDoc := NewHTMLParser(t, resp.Body)
- form := map[string]string{
- "_csrf": htmlDoc.GetCSRF(),
- "last_commit": htmlDoc.GetInputValueByName("last_commit"),
- }
- maps.Copy(form, params)
- req = NewRequestWithValues(t, "POST", requestPath, form)
- return session.MakeRequest(t, req, NoExpectedStatus)
- }
-
- func testEditorActionPostRequestError(t *testing.T, session *TestSession, requestPath string, params map[string]string, errorMessage string) {
- resp := testEditorActionPostRequest(t, session, requestPath, params)
- assert.Equal(t, http.StatusBadRequest, resp.Code)
- assert.Equal(t, errorMessage, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage)
- }
-
- func testEditorActionEdit(t *testing.T, session *TestSession, user, repo, editorAction, branch, filePath string, params map[string]string) *httptest.ResponseRecorder {
- params["tree_path"] = util.IfZero(params["tree_path"], filePath)
- newBranchName := util.Iif(params["commit_choice"] == "direct", branch, params["new_branch_name"])
- resp := testEditorActionPostRequest(t, session, fmt.Sprintf("/%s/%s/%s/%s/%s", user, repo, editorAction, branch, filePath), params)
- assert.Equal(t, http.StatusOK, resp.Code)
- assert.NotEmpty(t, test.RedirectURL(resp))
- req := NewRequest(t, "GET", path.Join(user, repo, "raw/branch", newBranchName, params["tree_path"]))
- resp = session.MakeRequest(t, req, http.StatusOK)
- assert.Equal(t, params["content"], resp.Body.String())
- return resp
- }
-
- func testEditFile(t *testing.T, session *TestSession, user, repo, branch, filePath, newContent string) {
- testEditorActionEdit(t, session, user, repo, "_edit", branch, filePath, map[string]string{
- "content": newContent,
- "commit_choice": "direct",
- })
- }
-
- func testEditFileToNewBranch(t *testing.T, session *TestSession, user, repo, branch, targetBranch, filePath, newContent string) {
- testEditorActionEdit(t, session, user, repo, "_edit", branch, filePath, map[string]string{
- "content": newContent,
- "commit_choice": "commit-to-new-branch",
- "new_branch_name": targetBranch,
- })
- }
-
- func testEditorDiffPreview(t *testing.T) {
- session := loginUser(t, "user2")
- req := NewRequestWithValues(t, "POST", "/user2/repo1/_preview/master/README.md", map[string]string{
- "_csrf": GetUserCSRFToken(t, session),
- "content": "Hello, World (Edited)\n",
- })
- resp := session.MakeRequest(t, req, http.StatusOK)
- assert.Contains(t, resp.Body.String(), `<span class="added-code">Hello, World (Edited)</span>`)
- }
-
- func testEditorPatchFile(t *testing.T) {
- session := loginUser(t, "user2")
- pathContentCommon := `diff --git a/patch-file-1.txt b/patch-file-1.txt
- new file mode 100644
- index 0000000000..aaaaaaaaaa
- --- /dev/null
- +++ b/patch-file-1.txt
- @@ -0,0 +1 @@
- +`
- testEditorActionPostRequest(t, session, "/user2/repo1/_diffpatch/master/", map[string]string{
- "content": pathContentCommon + "patched content\n",
- "commit_choice": "commit-to-new-branch",
- "new_branch_name": "patched-branch",
- })
- resp := MakeRequest(t, NewRequest(t, "GET", "/user2/repo1/raw/branch/patched-branch/patch-file-1.txt"), http.StatusOK)
- assert.Equal(t, "patched content\n", resp.Body.String())
-
- // patch again, it should fail
- resp = testEditorActionPostRequest(t, session, "/user2/repo1/_diffpatch/patched-branch/", map[string]string{
- "content": pathContentCommon + "another patched content\n",
- "commit_choice": "commit-to-new-branch",
- "new_branch_name": "patched-branch-1",
- })
- assert.Equal(t, "Unable to apply patch", test.ParseJSONError(resp.Body.Bytes()).ErrorMessage)
- }
-
- func testEditorWebGitCommitEmail(t *testing.T) {
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
- require.True(t, user.KeepEmailPrivate)
-
- repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
- gitRepo, _ := git.OpenRepository(t.Context(), repo1.RepoPath())
- defer gitRepo.Close()
- getLastCommit := func(t *testing.T) *git.Commit {
- c, err := gitRepo.GetBranchCommit("master")
- require.NoError(t, err)
- return c
- }
-
- session := loginUser(t, user.Name)
-
- makeReq := func(t *testing.T, link string, params map[string]string, expectedUserName, expectedEmail string) *httptest.ResponseRecorder {
- lastCommit := getLastCommit(t)
- params["_csrf"] = GetUserCSRFToken(t, session)
- params["last_commit"] = lastCommit.ID.String()
- params["commit_choice"] = "direct"
- req := NewRequestWithValues(t, "POST", link, params)
- resp := session.MakeRequest(t, req, NoExpectedStatus)
- newCommit := getLastCommit(t)
- if expectedUserName == "" {
- require.Equal(t, lastCommit.ID.String(), newCommit.ID.String())
- respErr := test.ParseJSONError(resp.Body.Bytes())
- assert.Equal(t, translation.NewLocale("en-US").TrString("repo.editor.invalid_commit_email"), respErr.ErrorMessage)
- } else {
- require.NotEqual(t, lastCommit.ID.String(), newCommit.ID.String())
- assert.Equal(t, expectedUserName, newCommit.Author.Name)
- assert.Equal(t, expectedEmail, newCommit.Author.Email)
- assert.Equal(t, expectedUserName, newCommit.Committer.Name)
- assert.Equal(t, expectedEmail, newCommit.Committer.Email)
- }
- return resp
- }
-
- uploadFile := func(t *testing.T, name, content string) string {
- body := &bytes.Buffer{}
- uploadForm := multipart.NewWriter(body)
- file, _ := uploadForm.CreateFormFile("file", name)
- _, _ = io.Copy(file, strings.NewReader(content))
- _ = uploadForm.WriteField("_csrf", GetUserCSRFToken(t, session))
- _ = uploadForm.Close()
-
- req := NewRequestWithBody(t, "POST", "/user2/repo1/upload-file", body)
- req.Header.Add("Content-Type", uploadForm.FormDataContentType())
- resp := session.MakeRequest(t, req, http.StatusOK)
-
- respMap := map[string]string{}
- DecodeJSON(t, resp, &respMap)
- return respMap["uuid"]
- }
-
- t.Run("EmailInactive", func(t *testing.T) {
- defer tests.PrintCurrentTest(t)()
- email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 35, UID: user.ID})
- require.False(t, email.IsActivated)
- makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{
- "tree_path": "README.md",
- "content": "test content",
- "commit_email": email.Email,
- }, "", "")
- })
-
- t.Run("EmailInvalid", func(t *testing.T) {
- defer tests.PrintCurrentTest(t)()
- email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 1, IsActivated: true})
- require.NotEqual(t, email.UID, user.ID)
- makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{
- "tree_path": "README.md",
- "content": "test content",
- "commit_email": email.Email,
- }, "", "")
- })
-
- testWebGit := func(t *testing.T, linkForKeepPrivate string, paramsForKeepPrivate map[string]string, linkForChosenEmail string, paramsForChosenEmail map[string]string) (resp1, resp2 *httptest.ResponseRecorder) {
- t.Run("DefaultEmailKeepPrivate", func(t *testing.T) {
- defer tests.PrintCurrentTest(t)()
- paramsForKeepPrivate["commit_email"] = ""
- resp1 = makeReq(t, linkForKeepPrivate, paramsForKeepPrivate, "User Two", "user2@noreply.example.org")
- })
- t.Run("ChooseEmail", func(t *testing.T) {
- defer tests.PrintCurrentTest(t)()
- paramsForChosenEmail["commit_email"] = "user2@example.com"
- resp2 = makeReq(t, linkForChosenEmail, paramsForChosenEmail, "User Two", "user2@example.com")
- })
- return resp1, resp2
- }
-
- t.Run("Edit", func(t *testing.T) {
- testWebGit(t,
- "/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for keep private"},
- "/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for chosen email"},
- )
- })
-
- t.Run("UploadDelete", func(t *testing.T) {
- file1UUID := uploadFile(t, "file1", "File 1")
- file2UUID := uploadFile(t, "file2", "File 2")
- testWebGit(t,
- "/user2/repo1/_upload/master", map[string]string{"files": file1UUID},
- "/user2/repo1/_upload/master", map[string]string{"files": file2UUID},
- )
- testWebGit(t,
- "/user2/repo1/_delete/master/file1", map[string]string{},
- "/user2/repo1/_delete/master/file2", map[string]string{},
- )
- })
-
- t.Run("ApplyPatchCherryPick", func(t *testing.T) {
- testWebGit(t,
- "/user2/repo1/_diffpatch/master", map[string]string{
- "tree_path": "__dummy__",
- "content": `diff --git a/patch-file-1.txt b/patch-file-1.txt
- new file mode 100644
- index 0000000000..aaaaaaaaaa
- --- /dev/null
- +++ b/patch-file-1.txt
- @@ -0,0 +1 @@
- +File 1
- `,
- },
- "/user2/repo1/_diffpatch/master", map[string]string{
- "tree_path": "__dummy__",
- "content": `diff --git a/patch-file-2.txt b/patch-file-2.txt
- new file mode 100644
- index 0000000000..bbbbbbbbbb
- --- /dev/null
- +++ b/patch-file-2.txt
- @@ -0,0 +1 @@
- +File 2
- `,
- },
- )
-
- commit1, err := gitRepo.GetCommitByPath("patch-file-1.txt")
- require.NoError(t, err)
- commit2, err := gitRepo.GetCommitByPath("patch-file-2.txt")
- require.NoError(t, err)
- resp1, _ := testWebGit(t,
- "/user2/repo1/_cherrypick/"+commit1.ID.String()+"/master", map[string]string{"revert": "true"},
- "/user2/repo1/_cherrypick/"+commit2.ID.String()+"/master", map[string]string{"revert": "true"},
- )
-
- // By the way, test the "cherrypick" page: a successful revert redirects to the main branch
- assert.Equal(t, "/user2/repo1/src/branch/master", test.RedirectURL(resp1))
- })
- }
-
- func testForkToEditFile(t *testing.T, session *TestSession, user, owner, repo, branch, filePath string) {
- forkToEdit := func(t *testing.T, session *TestSession, owner, repo, operation, branch, filePath string) {
- // visit the base repo, see the "Add File" button
- req := NewRequest(t, "GET", path.Join(owner, repo))
- resp := session.MakeRequest(t, req, http.StatusOK)
- htmlDoc := NewHTMLParser(t, resp.Body)
- AssertHTMLElement(t, htmlDoc, ".repo-add-file", 1)
-
- // attempt to edit a file, see the guideline page
- req = NewRequest(t, "GET", path.Join(owner, repo, operation, branch, filePath))
- resp = session.MakeRequest(t, req, http.StatusOK)
- assert.Contains(t, resp.Body.String(), "Fork Repository to Propose Changes")
-
- // fork the repository
- req = NewRequestWithValues(t, "POST", path.Join(owner, repo, "_fork", branch), map[string]string{"_csrf": GetUserCSRFToken(t, session)})
- resp = session.MakeRequest(t, req, http.StatusOK)
- assert.JSONEq(t, `{"redirect":""}`, resp.Body.String())
- }
-
- t.Run("ForkButArchived", func(t *testing.T) {
- // Fork repository because we can't edit it
- forkToEdit(t, session, owner, repo, "_edit", branch, filePath)
-
- // Archive the repository
- req := NewRequestWithValues(t, "POST", path.Join(user, repo, "settings"),
- map[string]string{
- "_csrf": GetUserCSRFToken(t, session),
- "repo_name": repo,
- "action": "archive",
- },
- )
- session.MakeRequest(t, req, http.StatusSeeOther)
-
- // Check editing archived repository is disabled
- req = NewRequest(t, "GET", path.Join(owner, repo, "_edit", branch, filePath)).SetHeader("Accept", "text/html")
- resp := session.MakeRequest(t, req, http.StatusNotFound)
- assert.Contains(t, resp.Body.String(), "You have forked this repository but your fork is not editable.")
-
- // Unfork the repository
- req = NewRequestWithValues(t, "POST", path.Join(user, repo, "settings"),
- map[string]string{
- "_csrf": GetUserCSRFToken(t, session),
- "repo_name": repo,
- "action": "convert_fork",
- },
- )
- session.MakeRequest(t, req, http.StatusSeeOther)
- })
-
- // Fork repository again, and check the existence of the forked repo with unique name
- forkToEdit(t, session, owner, repo, "_edit", branch, filePath)
- session.MakeRequest(t, NewRequestf(t, "GET", "/%s/%s-1", user, repo), http.StatusOK)
-
- t.Run("CheckBaseRepoForm", func(t *testing.T) {
- // the base repo's edit form should have the correct action and upload links (pointing to the forked repo)
- req := NewRequest(t, "GET", path.Join(owner, repo, "_upload", branch, filePath)+"?foo=bar")
- resp := session.MakeRequest(t, req, http.StatusOK)
- htmlDoc := NewHTMLParser(t, resp.Body)
-
- uploadForm := htmlDoc.doc.Find(".form-fetch-action")
- formAction := uploadForm.AttrOr("action", "")
- assert.Equal(t, fmt.Sprintf("/%s/%s-1/_upload/%s/%s?from_base_branch=%s&foo=bar", user, repo, branch, filePath, branch), formAction)
- uploadLink := uploadForm.Find(".dropzone").AttrOr("data-link-url", "")
- assert.Equal(t, fmt.Sprintf("/%s/%s-1/upload-file", user, repo), uploadLink)
- newBranchName := uploadForm.Find("input[name=new_branch_name]").AttrOr("value", "")
- assert.Equal(t, user+"-patch-1", newBranchName)
- commitChoice := uploadForm.Find("input[name=commit_choice][checked]").AttrOr("value", "")
- assert.Equal(t, "commit-to-new-branch", commitChoice)
- lastCommit := uploadForm.Find("input[name=last_commit]").AttrOr("value", "")
- assert.NotEmpty(t, lastCommit)
- })
-
- t.Run("ViewBaseEditFormAndCommitToFork", func(t *testing.T) {
- req := NewRequest(t, "GET", path.Join(owner, repo, "_edit", branch, filePath))
- resp := session.MakeRequest(t, req, http.StatusOK)
- htmlDoc := NewHTMLParser(t, resp.Body)
- editRequestForm := map[string]string{
- "_csrf": GetUserCSRFToken(t, session),
- "last_commit": htmlDoc.GetInputValueByName("last_commit"),
- "tree_path": filePath,
- "content": "new content in fork",
- "commit_choice": "commit-to-new-branch",
- }
- // change a file in the forked repo with existing branch name (should fail)
- editRequestForm["new_branch_name"] = "master"
- req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s-1/_edit/%s/%s?from_base_branch=%s", user, repo, branch, filePath, branch), editRequestForm)
- resp = session.MakeRequest(t, req, http.StatusBadRequest)
- respJSON := test.ParseJSONError(resp.Body.Bytes())
- assert.Equal(t, `Branch "master" already exists in your fork. Please choose a new branch name.`, respJSON.ErrorMessage)
-
- // change a file in the forked repo (should succeed)
- newBranchName := htmlDoc.GetInputValueByName("new_branch_name")
- editRequestForm["new_branch_name"] = newBranchName
- req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s-1/_edit/%s/%s?from_base_branch=%s", user, repo, branch, filePath, branch), editRequestForm)
- resp = session.MakeRequest(t, req, http.StatusOK)
- assert.Equal(t, fmt.Sprintf("/%s/%s/compare/%s...%s/%s-1:%s", owner, repo, branch, user, repo, newBranchName), test.RedirectURL(resp))
-
- // check the file in the fork's branch is changed
- req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s-1/src/branch/%s/%s", user, repo, newBranchName, filePath))
- resp = session.MakeRequest(t, req, http.StatusOK)
- assert.Contains(t, resp.Body.String(), "new content in fork")
- })
- }
-
- func testEditFileNotAllowed(t *testing.T) {
- sessionUser1 := loginUser(t, "user1") // admin, all access
- sessionUser4 := loginUser(t, "user4")
- // "_cherrypick" has a different route pattern, so skip its test
- operations := []string{"_new", "_edit", "_delete", "_upload", "_diffpatch"}
- for _, operation := range operations {
- t.Run(operation, func(t *testing.T) {
- // Branch does not exist
- targetLink := path.Join("user2", "repo1", operation, "missing", "README.md")
- sessionUser1.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusNotFound)
-
- // Private repository
- targetLink = path.Join("user2", "repo2", operation, "master", "Home.md")
- sessionUser1.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusOK)
- sessionUser4.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusNotFound)
-
- // Empty repository
- targetLink = path.Join("org41", "repo61", operation, "master", "README.md")
- sessionUser1.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusNotFound)
- })
- }
- }
|