Files
Gitea/tests/integration/api_pull_test.go
T
okxint ab1979bcfb fix(api): normalize all API due dates to end of day (#38677)
Due dates set through the API were stored inconsistently. The create
endpoints kept the raw timestamp, issue and pull request edits used end
of day in the client's offset, and only the deadline and milestone edit
endpoints normalized to end of day in the server's UI timezone like the
web UI does. All API due dates now go through
`ParseAPIDeadlineToEndOfDay`.

Editing a pull request with `unset_due_date: false` and no `due_date`
dereferenced a nil pointer and returned 500. It now shares the issue
edit logic and returns 400.

Related to https://github.com/go-gitea/gitea/issues/37620, which is
about the web sidebar and isn't fixed here.

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-10-03 11:59:43 +00:00

670 lines
28 KiB
Go

// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"bytes"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"testing"
"time"
auth_model "gitea.dev/models/auth"
issues_model "gitea.dev/models/issues"
"gitea.dev/models/perm"
repo_model "gitea.dev/models/repo"
unit_model "gitea.dev/models/unit"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/services/convert"
"gitea.dev/services/forms"
"gitea.dev/services/gitdiff"
issue_service "gitea.dev/services/issue"
pull_service "gitea.dev/services/pull"
repo_service "gitea.dev/services/repository"
files_service "gitea.dev/services/repository/files"
"gitea.dev/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAPIViewPulls(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
ctx := NewAPITestContext(t, "user2", repo.Name, auth_model.AccessTokenScopeReadRepository)
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls?state=all", owner.Name, repo.Name).
AddTokenAuth(ctx.Token)
resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
pulls := DecodeJSON(t, resp, []*api.PullRequest{})
expectedLen := unittest.GetCount(t, &issues_model.Issue{RepoID: repo.ID}, unittest.Cond("is_pull = ?", true))
assert.Len(t, pulls, expectedLen)
assert.Len(t, pulls, 3)
pull := pulls[0]
assert.EqualValues(t, 1, pull.Poster.ID)
assert.Len(t, pull.RequestedReviewers, 2)
assert.Empty(t, pull.RequestedReviewersTeams)
assert.EqualValues(t, 5, pull.RequestedReviewers[0].ID)
assert.EqualValues(t, 6, pull.RequestedReviewers[1].ID)
if assert.EqualValues(t, 5, pull.ID) {
resp = ctx.Session.MakeRequest(t, NewRequest(t, "GET", pull.DiffURL), http.StatusOK)
bs, err := io.ReadAll(resp.Body)
assert.NoError(t, err)
patch, err := gitdiff.ParsePatch(t.Context(), 1000, 5000, 10, bytes.NewReader(bs), "")
assert.NoError(t, err)
if assert.Len(t, patch.Files, 1) {
assert.Equal(t, "File-WoW", patch.Files[0].Name)
// FIXME: The old name should be empty if it's a file add type
assert.Equal(t, "File-WoW", patch.Files[0].OldName)
assert.Equal(t, 1, patch.Files[0].Addition)
assert.Equal(t, 0, patch.Files[0].Deletion)
assert.Equal(t, gitdiff.DiffFileAdd, patch.Files[0].Type)
}
t.Run(fmt.Sprintf("APIGetPullFiles_%d", pull.ID),
doAPIGetPullFiles(ctx, pull, func(t *testing.T, files []*api.ChangedFile) {
if assert.Len(t, files, 1) {
assert.Equal(t, "File-WoW", files[0].Filename)
assert.Empty(t, files[0].PreviousFilename)
assert.Equal(t, 1, files[0].Additions)
assert.Equal(t, 1, files[0].Changes)
assert.Equal(t, 0, files[0].Deletions)
assert.Equal(t, "added", files[0].Status)
}
}))
}
pull = pulls[1]
assert.EqualValues(t, 1, pull.Poster.ID)
assert.Len(t, pull.RequestedReviewers, 4)
assert.Empty(t, pull.RequestedReviewersTeams)
assert.EqualValues(t, 3, pull.RequestedReviewers[0].ID)
assert.EqualValues(t, 4, pull.RequestedReviewers[1].ID)
assert.EqualValues(t, 2, pull.RequestedReviewers[2].ID)
assert.EqualValues(t, 5, pull.RequestedReviewers[3].ID)
if assert.EqualValues(t, 2, pull.ID) {
resp = ctx.Session.MakeRequest(t, NewRequest(t, "GET", pull.DiffURL), http.StatusOK)
bs, err := io.ReadAll(resp.Body)
assert.NoError(t, err)
patch, err := gitdiff.ParsePatch(t.Context(), 1000, 5000, 10, bytes.NewReader(bs), "")
assert.NoError(t, err)
if assert.Len(t, patch.Files, 1) {
assert.Equal(t, "README.md", patch.Files[0].Name)
assert.Equal(t, "README.md", patch.Files[0].OldName)
assert.Equal(t, 4, patch.Files[0].Addition)
assert.Equal(t, 1, patch.Files[0].Deletion)
assert.Equal(t, gitdiff.DiffFileChange, patch.Files[0].Type)
}
t.Run(fmt.Sprintf("APIGetPullFiles_%d", pull.ID),
doAPIGetPullFiles(ctx, pull, func(t *testing.T, files []*api.ChangedFile) {
if assert.Len(t, files, 1) {
assert.Equal(t, "README.md", files[0].Filename)
// FIXME: The PreviousFilename name should be the same as Filename if it's a file change
assert.Empty(t, files[0].PreviousFilename)
assert.Equal(t, 4, files[0].Additions)
assert.Equal(t, 1, files[0].Deletions)
assert.Equal(t, "changed", files[0].Status)
}
}))
}
pull = pulls[0]
assert.EqualValues(t, 1, pull.Poster.ID)
assert.Len(t, pull.RequestedReviewers, 2)
assert.Empty(t, pull.RequestedReviewersTeams)
assert.EqualValues(t, 5, pull.RequestedReviewers[0].ID)
if assert.EqualValues(t, 5, pull.ID) {
resp = ctx.Session.MakeRequest(t, NewRequest(t, "GET", pull.DiffURL), http.StatusOK)
bs, err := io.ReadAll(resp.Body)
assert.NoError(t, err)
patch, err := gitdiff.ParsePatch(t.Context(), 1000, 5000, 10, bytes.NewReader(bs), "")
assert.NoError(t, err)
assert.Len(t, patch.Files, 1)
t.Run(fmt.Sprintf("APIGetPullFiles_%d", pull.ID),
doAPIGetPullFiles(ctx, pull, func(t *testing.T, files []*api.ChangedFile) {
assert.Len(t, files, 1)
}))
}
}
func TestAPIViewPullsByBaseHead(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
ctx := NewAPITestContext(t, "user2", repo.Name, auth_model.AccessTokenScopeReadRepository)
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls/master/branch2", owner.Name, repo.Name).
AddTokenAuth(ctx.Token)
resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
pull := DecodeJSON(t, resp, &api.PullRequest{})
assert.EqualValues(t, 3, pull.Index)
assert.EqualValues(t, 2, pull.ID)
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls/master/branch-not-exist", owner.Name, repo.Name).
AddTokenAuth(ctx.Token)
ctx.Session.MakeRequest(t, req, http.StatusNotFound)
}
// TestAPIMergePullWIP ensures that we can't merge a WIP pull request
func TestAPIMergePullWIP(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{Status: issues_model.PullRequestStatusMergeable}, unittest.Cond("has_merged = ?", false))
pr.LoadIssue(t.Context())
issue_service.ChangeTitle(t.Context(), pr.Issue, owner, setting.Repository.PullRequest.WorkInProgressPrefixes[0]+" "+pr.Issue.Title)
// force reload
pr.LoadAttributes(t.Context())
assert.Contains(t, pr.Issue.Title, setting.Repository.PullRequest.WorkInProgressPrefixes[0])
session := loginUser(t, owner.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge", owner.Name, repo.Name, pr.Index), &forms.MergePullRequestForm{
MergeMessageField: pr.Issue.Title,
Do: string(repo_model.MergeStyleMerge),
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusMethodNotAllowed)
}
func TestAPIMergePull(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
apiCtx := NewAPITestContext(t, repo.OwnerName, repo.Name, auth_model.AccessTokenScopeWriteRepository)
checkBranchExists := func(t *testing.T, branchName string, status int) {
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/branches/%s", owner.Name, repo.Name, branchName)).AddTokenAuth(apiCtx.Token)
MakeRequest(t, req, status)
}
createTestBranchPR := func(t *testing.T, branchName string) *api.PullRequest {
testCreateFileInBranch(t, owner, repo, createFileInBranchOptions{NewBranch: branchName}, map[string]string{"a-new-file-" + branchName + ".txt": "dummy content"})
prDTO, err := doAPICreatePullRequest(apiCtx, repo.OwnerName, repo.Name, repo.DefaultBranch, branchName)(t)
require.NoError(t, err)
return &prDTO
}
performMerge := func(t *testing.T, prIndex int64, params map[string]any, optExpectedStatus ...int) {
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge", owner.Name, repo.Name, prIndex), params).AddTokenAuth(apiCtx.Token)
expectedStatus := util.OptionalArg(optExpectedStatus, http.StatusOK)
MakeRequest(t, req, expectedStatus)
}
t.Run("Normal", func(t *testing.T) {
newBranch := "test-pull-1"
prDTO := createTestBranchPR(t, newBranch)
performMerge(t, prDTO.Index, map[string]any{"do": "merge"})
checkBranchExists(t, newBranch, http.StatusOK)
// try to merge again, make sure we cannot perform a merge on the same PR
performMerge(t, prDTO.Index, map[string]any{"do": "merge"}, http.StatusMethodNotAllowed)
})
t.Run("DeleteBranchAfterMergePassedByFormField", func(t *testing.T) {
newBranch := "test-pull-2"
prDTO := createTestBranchPR(t, newBranch)
performMerge(t, prDTO.Index, map[string]any{"do": "merge", "delete_branch_after_merge": true})
checkBranchExists(t, newBranch, http.StatusNotFound)
})
updateRepoUnitDefaultDeleteBranchAfterMerge := func(t *testing.T, repo *repo_model.Repository, value bool) {
prUnit, err := repo.GetUnit(t.Context(), unit_model.TypePullRequests)
require.NoError(t, err)
prUnit.PullRequestsConfig().DefaultDeleteBranchAfterMerge = value
require.NoError(t, repo_service.UpdateRepositoryUnits(t.Context(), repo, []repo_model.RepoUnit{{
RepoID: repo.ID,
Type: unit_model.TypePullRequests,
Config: prUnit.PullRequestsConfig(),
}}, nil))
}
t.Run("DeleteBranchAfterMergePassedByRepoSettings", func(t *testing.T) {
newBranch := "test-pull-3"
prDTO := createTestBranchPR(t, newBranch)
updateRepoUnitDefaultDeleteBranchAfterMerge(t, repo, true)
performMerge(t, prDTO.Index, map[string]any{"do": "merge"})
checkBranchExists(t, newBranch, http.StatusNotFound)
})
t.Run("DeleteBranchAfterMergeFormFieldIsSetButNotRepoSettings", func(t *testing.T) {
newBranch := "test-pull-4"
prDTO := createTestBranchPR(t, newBranch)
updateRepoUnitDefaultDeleteBranchAfterMerge(t, repo, false)
performMerge(t, prDTO.Index, map[string]any{"do": "merge", "delete_branch_after_merge": true})
checkBranchExists(t, newBranch, http.StatusNotFound)
})
})
}
func TestAPICreatePullSuccess(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo10 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
// repo10 have code, pulls units.
repo11 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11})
// repo11 only have code unit but should still create pulls
owner10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo10.OwnerID})
owner11 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo11.OwnerID})
session := loginUser(t, owner11.Name)
prTitle := "test pull request title " + time.Now().String()
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &api.CreatePullRequestOption{
Head: owner11.Name + ":master",
Base: "master",
Title: prTitle,
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
// Also test that AllowMaintainerEdit is true by default, the "false" case is covered by TestAPICreatePullBasePermission
prIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: prTitle})
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{IssueID: prIssue.ID})
assert.True(t, pr.AllowMaintainerEdit)
MakeRequest(t, req, http.StatusUnprocessableEntity) // second request should fail
}
func TestAPICreatePullBasePermission(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo10 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
// repo10 have code, pulls units.
repo11 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11})
// repo11 only have code unit but should still create pulls
owner10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo10.OwnerID})
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
session := loginUser(t, user4.Name)
prTitle := "test pull request title " + time.Now().String()
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
opts := &api.CreatePullRequestOption{
Head: repo11.OwnerName + ":master",
Base: "master",
Title: prTitle,
AllowMaintainerEdit: new(false),
}
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &opts).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
// add user4 to be a collaborator to base repo
ctx := NewAPITestContext(t, repo10.OwnerName, repo10.Name, auth_model.AccessTokenScopeWriteRepository)
t.Run("AddUser4AsCollaborator", doAPIAddCollaborator(ctx, user4.Name, perm.AccessModeRead))
// create again
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &opts).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
// Also test that AllowMaintainerEdit is set to false, the default "true" case is covered by TestAPICreatePullSuccess
prIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: prTitle})
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{IssueID: prIssue.ID})
assert.False(t, pr.AllowMaintainerEdit)
}
func TestAPICreatePullHeadPermission(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo10 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
// repo10 have code, pulls units.
repo11 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11})
// repo11 only have code unit but should still create pulls
owner10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo10.OwnerID})
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
session := loginUser(t, user4.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
opts := &api.CreatePullRequestOption{
Head: repo11.OwnerName + ":master",
Base: "master",
Title: "create a failure pr",
}
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &opts).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
// add user4 to be a collaborator to head repo with read permission
ctx := NewAPITestContext(t, repo11.OwnerName, repo11.Name, auth_model.AccessTokenScopeWriteRepository)
t.Run("AddUser4AsCollaboratorWithRead", doAPIAddCollaborator(ctx, user4.Name, perm.AccessModeRead))
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &opts).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
// add user4 to be a collaborator to head repo with write permission
t.Run("AddUser4AsCollaboratorWithWrite", doAPIAddCollaborator(ctx, user4.Name, perm.AccessModeWrite))
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &opts).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
}
func TestAPICreatePullSameRepoSuccess(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, owner.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner.Name, repo.Name), &api.CreatePullRequestOption{
Head: owner.Name + ":pr-to-update",
Base: "master",
Title: "successfully create a PR between branches of the same repository",
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
MakeRequest(t, req, http.StatusUnprocessableEntity) // second request should fail
}
func TestAPICreatePullWithFieldsSuccess(t *testing.T) {
defer tests.PrepareTestEnv(t)()
// repo10 have code, pulls units.
repo10 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
owner10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo10.OwnerID})
// repo11 only have code unit but should still create pulls
repo11 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11})
owner11 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo11.OwnerID})
session := loginUser(t, owner11.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
opts := &api.CreatePullRequestOption{
Head: owner11.Name + ":master",
Base: "master",
Title: "create a failure pr",
Body: "foobaaar",
Milestone: 5,
Assignees: []string{owner10.Name},
Labels: []int64{5},
}
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), opts).
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusCreated)
pull := DecodeJSON(t, resp, &api.PullRequest{})
assert.NotNil(t, pull.Milestone)
assert.Equal(t, opts.Milestone, pull.Milestone.ID)
if assert.Len(t, pull.Assignees, 1) {
assert.Equal(t, opts.Assignees[0], owner10.Name)
}
assert.NotNil(t, pull.Labels)
assert.Equal(t, opts.Labels[0], pull.Labels[0].ID)
}
func TestAPICreatePullWithFieldsFailure(t *testing.T) {
defer tests.PrepareTestEnv(t)()
// repo10 have code, pulls units.
repo10 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
owner10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo10.OwnerID})
// repo11 only have code unit but should still create pulls
repo11 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11})
owner11 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo11.OwnerID})
session := loginUser(t, owner11.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
opts := &api.CreatePullRequestOption{
Head: owner11.Name + ":master",
Base: "master",
}
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), opts).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusUnprocessableEntity)
opts.Title = "is required"
opts.Milestone = 666
MakeRequest(t, req, http.StatusUnprocessableEntity)
opts.Milestone = 5
opts.Assignees = []string{"qweruqweroiuyqweoiruywqer"}
MakeRequest(t, req, http.StatusUnprocessableEntity)
opts.Assignees = []string{owner10.LoginName}
opts.Labels = []int64{55555}
MakeRequest(t, req, http.StatusUnprocessableEntity)
opts.Labels = []int64{5}
}
func TestAPIEditPull(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo10 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
owner10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo10.OwnerID})
session := loginUser(t, owner10.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
title := "create a success pr"
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &api.CreatePullRequestOption{
Head: "develop",
Base: "master",
Title: title,
Deadline: new(time.Date(2026, 5, 11, 12, 0, 0, 0, time.FixedZone("", -10*3600))),
}).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusCreated)
apiPull := DecodeJSON(t, resp, &api.PullRequest{})
assert.Equal(t, "master", apiPull.Base.Name)
assert.Equal(t, time.Date(2026, 5, 11, 23, 59, 59, 0, setting.DefaultUILocation).Unix(), apiPull.Deadline.Unix())
newTitle := "edit a this pr"
newBody := "edited body"
req = NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner10.Name, repo10.Name, apiPull.Index), &api.EditPullRequestOption{
Base: "feature/1",
Title: newTitle,
Body: &newBody,
}).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusCreated)
apiPull = DecodeJSON(t, resp, &api.PullRequest{})
assert.Equal(t, "feature/1", apiPull.Base.Name)
// check comment history
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: apiPull.ID})
err := pull.LoadIssue(t.Context())
assert.NoError(t, err)
unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: pull.Issue.ID, OldTitle: title, NewTitle: newTitle})
unittest.AssertExistsAndLoadBean(t, &issues_model.ContentHistory{IssueID: pull.Issue.ID, ContentText: newBody, IsFirstCreated: false})
req = NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner10.Name, repo10.Name, pull.Index), &api.EditPullRequestOption{
Base: "not-exist",
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
req = NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner10.Name, repo10.Name, pull.Index), &api.EditPullRequestOption{
RemoveDeadline: new(false),
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusBadRequest)
t.Run("PullContentVersion", func(t *testing.T) {
testAPIPullContentVersion(t, pull.ID)
})
}
func testAPIPullContentVersion(t *testing.T, pullID int64) {
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pullID})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pull.BaseRepoID})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, owner.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner.Name, repo.Name, pull.Index)
t.Run("ResponseIncludesContentVersion", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", urlStr).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
apiPull := DecodeJSON(t, resp, &api.PullRequest{})
assert.GreaterOrEqual(t, apiPull.ContentVersion, 0)
})
t.Run("EditWithCorrectVersion", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", urlStr).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
before := DecodeJSON(t, resp, &api.PullRequest{})
req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditPullRequestOption{
Body: new("updated body with correct version"),
ContentVersion: new(before.ContentVersion),
}).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusCreated)
after := DecodeJSON(t, resp, &api.PullRequest{})
assert.Equal(t, "updated body with correct version", after.Body)
assert.Greater(t, after.ContentVersion, before.ContentVersion)
})
t.Run("EditWithWrongVersion", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditPullRequestOption{
Body: new("should fail"),
ContentVersion: new(99999),
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusConflict)
})
t.Run("EditWithoutVersion", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditPullRequestOption{
Body: new("edit without version succeeds"),
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
})
}
func doAPIGetPullFiles(ctx APITestContext, pr *api.PullRequest, callback func(*testing.T, []*api.ChangedFile)) func(*testing.T) {
return func(t *testing.T) {
req := NewRequest(t, http.MethodGet, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/files", ctx.Username, ctx.Reponame, pr.Index)).
AddTokenAuth(ctx.Token)
if ctx.ExpectedCode == 0 {
ctx.ExpectedCode = http.StatusOK
}
resp := ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
files := DecodeJSON(t, resp, []*api.ChangedFile{})
if callback != nil {
callback(t, files)
}
}
}
func TestAPICommitPullRequest(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
ctx := NewAPITestContext(t, "user2", repo.Name, auth_model.AccessTokenScopeReadRepository)
mergedCommitSHA := "1a8823cd1a9549fde083f992f6b9b87a7ab74fb3"
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/commits/%s/pull", owner.Name, repo.Name, mergedCommitSHA).AddTokenAuth(ctx.Token)
ctx.Session.MakeRequest(t, req, http.StatusOK)
invalidCommitSHA := "abcd1234abcd1234abcd1234abcd1234abcd1234"
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/commits/%s/pull", owner.Name, repo.Name, invalidCommitSHA).AddTokenAuth(ctx.Token)
ctx.Session.MakeRequest(t, req, http.StatusNotFound)
}
func TestAPIViewPullFilesWithHeadRepoDeleted(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
ctx := NewAPITestContext(t, "user1", baseRepo.Name, auth_model.AccessTokenScopeAll)
doAPIForkRepository(ctx, "user2")(t)
forkedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ForkID: baseRepo.ID, OwnerName: "user1"})
// add a new file to the forked repo
addFileToForkedResp, err := files_service.ChangeRepoFiles(t.Context(), forkedRepo, user1, &files_service.ChangeRepoFilesOptions{
Files: []*files_service.ChangeRepoFile{
{
Operation: "create",
TreePath: "file_1.txt",
ContentReader: strings.NewReader("file1"),
},
},
Message: "add file1",
OldBranch: "master",
NewBranch: "fork-branch-1",
Author: &files_service.IdentityOptions{
GitUserName: user1.Name,
GitUserEmail: user1.Email,
},
Committer: &files_service.IdentityOptions{
GitUserName: user1.Name,
GitUserEmail: user1.Email,
},
Dates: &files_service.CommitDateOptions{
Author: time.Now(),
Committer: time.Now(),
},
})
assert.NoError(t, err)
assert.NotEmpty(t, addFileToForkedResp)
// create Pull
pullIssue := &issues_model.Issue{
RepoID: baseRepo.ID,
Title: "Test pull-request-target-event",
PosterID: user1.ID,
Poster: user1,
IsPull: true,
}
pullRequest := &issues_model.PullRequest{
HeadRepoID: forkedRepo.ID,
BaseRepoID: baseRepo.ID,
HeadBranch: "fork-branch-1",
BaseBranch: "master",
HeadRepo: forkedRepo,
BaseRepo: baseRepo,
Type: issues_model.PullRequestGitea,
}
prOpts := &pull_service.NewPullRequestOptions{Repo: baseRepo, Issue: pullIssue, PullRequest: pullRequest}
err = pull_service.NewPullRequest(t.Context(), prOpts)
assert.NoError(t, err)
pr := convert.ToAPIPullRequest(t.Context(), pullRequest, user1)
ctx = NewAPITestContext(t, "user2", baseRepo.Name, auth_model.AccessTokenScopeAll)
doAPIGetPullFiles(ctx, pr, func(t *testing.T, files []*api.ChangedFile) {
if assert.Len(t, files, 1) {
assert.Equal(t, "file_1.txt", files[0].Filename)
assert.Empty(t, files[0].PreviousFilename)
assert.Equal(t, 1, files[0].Additions)
assert.Equal(t, 1, files[0].Changes)
assert.Equal(t, 0, files[0].Deletions)
assert.Equal(t, "added", files[0].Status)
}
})(t)
// delete the head repository of the pull request
forkCtx := NewAPITestContext(t, "user1", forkedRepo.Name, auth_model.AccessTokenScopeAll)
doAPIDeleteRepository(forkCtx)(t)
doAPIGetPullFiles(ctx, pr, func(t *testing.T, files []*api.ChangedFile) {
if assert.Len(t, files, 1) {
assert.Equal(t, "file_1.txt", files[0].Filename)
assert.Empty(t, files[0].PreviousFilename)
assert.Equal(t, 1, files[0].Additions)
assert.Equal(t, 1, files[0].Changes)
assert.Equal(t, 0, files[0].Deletions)
assert.Equal(t, "added", files[0].Status)
}
})(t)
})
}