Files
gitea/tests/integration/api_repo_file_update_test.go
wxiaoguang 9dc04289aa refactor: make git package handle all git operations (#38543)
before: gitrepo vs git packages
after: git package fully handle all git operations

by the way, use `WithRepo(repo)` instead of `WithDir(repo.Path)` to hide
path details.

benefits:

1. remove all unnecessary wrappers, developers no need to struggle with
"which package should be used"
2. simplify code, RepositoryFacade can (will) be used everywhere, all
"path" details are (will be) hidden
2026-07-20 16:07:38 +00:00

290 lines
12 KiB
Go

// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"encoding/base64"
"fmt"
"net/http"
"net/url"
"path"
"testing"
auth_model "gitea.dev/models/auth"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/modules/git"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/services/context"
"github.com/stretchr/testify/assert"
)
func getUpdateFileOptions() *api.UpdateFileOptions {
content := "This is updated text"
contentEncoded := base64.StdEncoding.EncodeToString([]byte(content))
return &api.UpdateFileOptions{
SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885",
FileOptions: api.FileOptions{
BranchName: "master",
NewBranchName: "master",
Message: "My update of new/file.txt",
Author: api.Identity{
Name: "John Doe",
Email: "johndoe@example.com",
},
Committer: api.Identity{
Name: "Anne Doe",
Email: "annedoe@example.com",
},
},
ContentBase64: contentEncoded,
}
}
func getExpectedFileResponseForUpdate(info apiFileResponseInfo) *api.FileResponse {
sha := "08bd14b2e2852529157324de9c226b3364e76136"
encoding := "base64"
content := "VGhpcyBpcyB1cGRhdGVkIHRleHQ="
selfURL := setting.AppURL + "api/v1/repos/user2/repo1/contents/" + info.treePath + "?ref=master"
htmlURL := setting.AppURL + "user2/repo1/src/branch/master/" + info.treePath
gitURL := setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha
downloadURL := setting.AppURL + "user2/repo1/raw/branch/master/" + info.treePath
ret := &api.FileResponse{
Content: &api.ContentsResponse{
Name: path.Base(info.treePath),
Path: info.treePath,
SHA: sha,
LastCommitSHA: new(info.lastCommitSHA),
LastCommitterDate: new(info.lastCommitterWhen),
LastAuthorDate: new(info.lastAuthorWhen),
Type: "file",
Size: 20,
Encoding: &encoding,
Content: &content,
URL: &selfURL,
HTMLURL: &htmlURL,
GitURL: &gitURL,
DownloadURL: &downloadURL,
Links: &api.FileLinksResponse{
Self: &selfURL,
GitURL: &gitURL,
HTMLURL: &htmlURL,
},
},
Commit: &api.FileCommitResponse{
CommitMeta: api.CommitMeta{
URL: setting.AppURL + "api/v1/repos/user2/repo1/git/commits/" + info.commitID,
SHA: info.commitID,
},
HTMLURL: setting.AppURL + "user2/repo1/commit/" + info.commitID,
Author: &api.CommitUser{
Identity: api.Identity{
Name: "John Doe",
Email: "johndoe@example.com",
},
},
Committer: &api.CommitUser{
Identity: api.Identity{
Name: "Anne Doe",
Email: "annedoe@example.com",
},
},
Message: "My update of README.md\n",
},
Verification: &api.PayloadCommitVerification{
Verified: false,
Reason: "gpg.error.not_signed_commit",
Signature: "",
Payload: "",
},
}
normalizeFileContentResponseCommitTime(ret.Content)
return ret
}
func TestAPIUpdateFile(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo1 & repo16
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // owner of the repo3, is an org
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // owner of neither repos
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) // public repo
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) // public repo
repo16 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 16}) // private repo
fileID := 0
// Get user2's token
session := loginUser(t, user2.Name)
token2 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
// Get user4's token
session = loginUser(t, user4.Name)
token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
// Test updating a file in repo1 which user2 owns, try both with branch and empty branch
for _, branch := range [...]string{
"master", // Branch
"", // Empty branch
} {
fileID++
treePath := fmt.Sprintf("update/file%d.txt", fileID)
createFile(user2, repo1, treePath)
updateFileOptions := getUpdateFileOptions()
updateFileOptions.BranchName = branch
req := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath), &updateFileOptions).
AddTokenAuth(token2)
resp := MakeRequest(t, req, http.StatusOK)
gitRepo, _ := git.OpenRepository(repo1)
defer gitRepo.Close()
commitID, _ := gitRepo.GetBranchCommitID(t.Context(), updateFileOptions.NewBranchName)
lasCommit, _ := gitRepo.GetCommitByPath(t.Context(), treePath)
expectedFileResponse := getExpectedFileResponseForUpdate(apiFileResponseInfo{
commitID: commitID,
treePath: treePath,
lastCommitSHA: lasCommit.ID.String(),
lastCommitterWhen: lasCommit.Committer.When,
lastAuthorWhen: lasCommit.Author.When,
})
fileResponse := DecodeJSON(t, resp, &api.FileResponse{})
normalizeFileContentResponseCommitTime(fileResponse.Content)
assert.Equal(t, expectedFileResponse.Content, fileResponse.Content)
assert.Equal(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA)
assert.Equal(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL)
assert.Equal(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email)
assert.Equal(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name)
}
// Test updating a file in a new branch
updateFileOptions := getUpdateFileOptions()
updateFileOptions.BranchName = repo1.DefaultBranch
updateFileOptions.NewBranchName = "new_branch"
fileID++
treePath := fmt.Sprintf("update/file%d.txt", fileID)
createFile(user2, repo1, treePath)
req := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath), &updateFileOptions).
AddTokenAuth(token2)
resp := MakeRequest(t, req, http.StatusOK)
fileResponse := DecodeJSON(t, resp, &api.FileResponse{})
expectedSHA := "08bd14b2e2852529157324de9c226b3364e76136"
expectedHTMLURL := fmt.Sprintf(setting.AppURL+"user2/repo1/src/branch/new_branch/update/file%d.txt", fileID)
expectedDownloadURL := fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/new_branch/update/file%d.txt", fileID)
assert.Equal(t, expectedSHA, fileResponse.Content.SHA)
assert.Equal(t, expectedHTMLURL, *fileResponse.Content.HTMLURL)
assert.Equal(t, expectedDownloadURL, *fileResponse.Content.DownloadURL)
assert.Equal(t, updateFileOptions.Message+"\n", fileResponse.Commit.Message)
// Test updating a file without SHA (should create the file)
updateFileOptions = getUpdateFileOptions()
updateFileOptions.SHA = ""
req = NewRequestWithJSON(t, "PUT", "/api/v1/repos/user2/repo1/contents/update-create.txt", &updateFileOptions).AddTokenAuth(token2)
resp = MakeRequest(t, req, http.StatusCreated)
fileResponse = DecodeJSON(t, resp, &api.FileResponse{})
assert.Equal(t, "08bd14b2e2852529157324de9c226b3364e76136", fileResponse.Content.SHA)
assert.Equal(t, setting.AppURL+"user2/repo1/raw/branch/master/update-create.txt", *fileResponse.Content.DownloadURL)
// Test updating a file and renaming it
updateFileOptions = getUpdateFileOptions()
updateFileOptions.BranchName = repo1.DefaultBranch
fileID++
treePath = fmt.Sprintf("update/file%d.txt", fileID)
createFile(user2, repo1, treePath)
updateFileOptions.FromPath = treePath
treePath = "rename/" + treePath
req = NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath), &updateFileOptions).
AddTokenAuth(token2)
resp = MakeRequest(t, req, http.StatusOK)
fileResponse = DecodeJSON(t, resp, &api.FileResponse{})
expectedSHA = "08bd14b2e2852529157324de9c226b3364e76136"
expectedHTMLURL = fmt.Sprintf(setting.AppURL+"user2/repo1/src/branch/master/rename/update/file%d.txt", fileID)
expectedDownloadURL = fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/master/rename/update/file%d.txt", fileID)
assert.Equal(t, expectedSHA, fileResponse.Content.SHA)
assert.Equal(t, expectedHTMLURL, *fileResponse.Content.HTMLURL)
assert.Equal(t, expectedDownloadURL, *fileResponse.Content.DownloadURL)
// Test updating a file without a message
updateFileOptions = getUpdateFileOptions()
updateFileOptions.Message = ""
updateFileOptions.BranchName = repo1.DefaultBranch
fileID++
treePath = fmt.Sprintf("update/file%d.txt", fileID)
createFile(user2, repo1, treePath)
req = NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath), &updateFileOptions).
AddTokenAuth(token2)
resp = MakeRequest(t, req, http.StatusOK)
fileResponse = DecodeJSON(t, resp, &api.FileResponse{})
expectedMessage := "Update " + treePath + "\n"
assert.Equal(t, expectedMessage, fileResponse.Commit.Message)
// Test updating a file with the wrong SHA
fileID++
treePath = fmt.Sprintf("update/file%d.txt", fileID)
createFile(user2, repo1, treePath)
updateFileOptions = getUpdateFileOptions()
correctSHA := updateFileOptions.SHA
updateFileOptions.SHA = "badsha"
req = NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath), &updateFileOptions).
AddTokenAuth(token2)
resp = MakeRequest(t, req, http.StatusUnprocessableEntity)
expectedAPIError := &context.APIError{
Message: "sha does not match [given: " + updateFileOptions.SHA + ", expected: " + correctSHA + "]",
URL: setting.API.SwaggerURL,
}
apiError := DecodeJSON(t, resp, &context.APIError{})
assert.Equal(t, expectedAPIError, apiError)
// Test creating a file in repo1 by user4 who does not have write access
fileID++
treePath = fmt.Sprintf("update/file%d.txt", fileID)
createFile(user2, repo16, treePath)
updateFileOptions = getUpdateFileOptions()
req = NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo16.Name, treePath), &updateFileOptions).
AddTokenAuth(token4)
MakeRequest(t, req, http.StatusNotFound)
// Tests a repo with no token given so will fail
fileID++
treePath = fmt.Sprintf("update/file%d.txt", fileID)
createFile(user2, repo16, treePath)
updateFileOptions = getUpdateFileOptions()
req = NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo16.Name, treePath), &updateFileOptions)
MakeRequest(t, req, http.StatusNotFound)
// Test using access token for a private repo that the user of the token owns
fileID++
treePath = fmt.Sprintf("update/file%d.txt", fileID)
createFile(user2, repo16, treePath)
updateFileOptions = getUpdateFileOptions()
req = NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo16.Name, treePath), &updateFileOptions).
AddTokenAuth(token2)
MakeRequest(t, req, http.StatusOK)
// Test using org repo "org3/repo3" where user2 is a collaborator
fileID++
treePath = fmt.Sprintf("update/file%d.txt", fileID)
createFile(org3, repo3, treePath)
updateFileOptions = getUpdateFileOptions()
req = NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", org3.Name, repo3.Name, treePath), &updateFileOptions).
AddTokenAuth(token2)
MakeRequest(t, req, http.StatusOK)
// Test using org repo "org3/repo3" with no user token
fileID++
treePath = fmt.Sprintf("update/file%d.txt", fileID)
createFile(org3, repo3, treePath)
updateFileOptions = getUpdateFileOptions()
req = NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", org3.Name, repo3.Name, treePath), &updateFileOptions)
MakeRequest(t, req, http.StatusNotFound)
// Test using repo "user2/repo1" where user4 is a NOT collaborator
fileID++
treePath = fmt.Sprintf("update/file%d.txt", fileID)
createFile(user2, repo1, treePath)
updateFileOptions = getUpdateFileOptions()
req = NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath), &updateFileOptions).
AddTokenAuth(token4)
MakeRequest(t, req, http.StatusForbidden)
})
}