code.gitea.io/gitea@v1.22.3/tests/integration/api_pull_test.go (about)

     1  // Copyright 2017 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package integration
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"testing"
    11  
    12  	auth_model "code.gitea.io/gitea/models/auth"
    13  	"code.gitea.io/gitea/models/db"
    14  	issues_model "code.gitea.io/gitea/models/issues"
    15  	"code.gitea.io/gitea/models/perm"
    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/setting"
    20  	api "code.gitea.io/gitea/modules/structs"
    21  	"code.gitea.io/gitea/services/forms"
    22  	issue_service "code.gitea.io/gitea/services/issue"
    23  	"code.gitea.io/gitea/tests"
    24  
    25  	"github.com/stretchr/testify/assert"
    26  )
    27  
    28  func TestAPIViewPulls(t *testing.T) {
    29  	defer tests.PrepareTestEnv(t)()
    30  	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
    31  	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
    32  
    33  	ctx := NewAPITestContext(t, "user2", repo.Name, auth_model.AccessTokenScopeReadRepository)
    34  
    35  	req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls?state=all", owner.Name, repo.Name).
    36  		AddTokenAuth(ctx.Token)
    37  	resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
    38  
    39  	var pulls []*api.PullRequest
    40  	DecodeJSON(t, resp, &pulls)
    41  	expectedLen := unittest.GetCount(t, &issues_model.Issue{RepoID: repo.ID}, unittest.Cond("is_pull = ?", true))
    42  	assert.Len(t, pulls, expectedLen)
    43  
    44  	pull := pulls[0]
    45  	if assert.EqualValues(t, 5, pull.ID) {
    46  		resp = ctx.Session.MakeRequest(t, NewRequest(t, "GET", pull.DiffURL), http.StatusOK)
    47  		_, err := io.ReadAll(resp.Body)
    48  		assert.NoError(t, err)
    49  		// TODO: use diff to generate stats to test against
    50  
    51  		t.Run(fmt.Sprintf("APIGetPullFiles_%d", pull.ID),
    52  			doAPIGetPullFiles(ctx, pull, func(t *testing.T, files []*api.ChangedFile) {
    53  				if assert.Len(t, files, 1) {
    54  					assert.Equal(t, "File-WoW", files[0].Filename)
    55  					assert.Empty(t, files[0].PreviousFilename)
    56  					assert.EqualValues(t, 1, files[0].Additions)
    57  					assert.EqualValues(t, 1, files[0].Changes)
    58  					assert.EqualValues(t, 0, files[0].Deletions)
    59  					assert.Equal(t, "added", files[0].Status)
    60  				}
    61  			}))
    62  	}
    63  }
    64  
    65  func TestAPIViewPullsByBaseHead(t *testing.T) {
    66  	defer tests.PrepareTestEnv(t)()
    67  	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
    68  	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
    69  
    70  	ctx := NewAPITestContext(t, "user2", repo.Name, auth_model.AccessTokenScopeReadRepository)
    71  
    72  	req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls/master/branch2", owner.Name, repo.Name).
    73  		AddTokenAuth(ctx.Token)
    74  	resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
    75  
    76  	pull := &api.PullRequest{}
    77  	DecodeJSON(t, resp, pull)
    78  	assert.EqualValues(t, 3, pull.Index)
    79  	assert.EqualValues(t, 2, pull.ID)
    80  
    81  	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls/master/branch-not-exist", owner.Name, repo.Name).
    82  		AddTokenAuth(ctx.Token)
    83  	ctx.Session.MakeRequest(t, req, http.StatusNotFound)
    84  }
    85  
    86  // TestAPIMergePullWIP ensures that we can't merge a WIP pull request
    87  func TestAPIMergePullWIP(t *testing.T) {
    88  	defer tests.PrepareTestEnv(t)()
    89  	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
    90  	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
    91  	pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{Status: issues_model.PullRequestStatusMergeable}, unittest.Cond("has_merged = ?", false))
    92  	pr.LoadIssue(db.DefaultContext)
    93  	issue_service.ChangeTitle(db.DefaultContext, pr.Issue, owner, setting.Repository.PullRequest.WorkInProgressPrefixes[0]+" "+pr.Issue.Title)
    94  
    95  	// force reload
    96  	pr.LoadAttributes(db.DefaultContext)
    97  
    98  	assert.Contains(t, pr.Issue.Title, setting.Repository.PullRequest.WorkInProgressPrefixes[0])
    99  
   100  	session := loginUser(t, owner.Name)
   101  	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
   102  	req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge", owner.Name, repo.Name, pr.Index), &forms.MergePullRequestForm{
   103  		MergeMessageField: pr.Issue.Title,
   104  		Do:                string(repo_model.MergeStyleMerge),
   105  	}).AddTokenAuth(token)
   106  
   107  	MakeRequest(t, req, http.StatusMethodNotAllowed)
   108  }
   109  
   110  func TestAPICreatePullSuccess(t *testing.T) {
   111  	defer tests.PrepareTestEnv(t)()
   112  	repo10 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
   113  	// repo10 have code, pulls units.
   114  	repo11 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11})
   115  	// repo11 only have code unit but should still create pulls
   116  	owner10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo10.OwnerID})
   117  	owner11 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo11.OwnerID})
   118  
   119  	session := loginUser(t, owner11.Name)
   120  	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
   121  	req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &api.CreatePullRequestOption{
   122  		Head:  fmt.Sprintf("%s:master", owner11.Name),
   123  		Base:  "master",
   124  		Title: "create a failure pr",
   125  	}).AddTokenAuth(token)
   126  	MakeRequest(t, req, http.StatusCreated)
   127  	MakeRequest(t, req, http.StatusUnprocessableEntity) // second request should fail
   128  }
   129  
   130  func TestAPICreatePullBasePermission(t *testing.T) {
   131  	defer tests.PrepareTestEnv(t)()
   132  	repo10 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
   133  	// repo10 have code, pulls units.
   134  	repo11 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11})
   135  	// repo11 only have code unit but should still create pulls
   136  	owner10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo10.OwnerID})
   137  	user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
   138  
   139  	session := loginUser(t, user4.Name)
   140  	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
   141  	opts := &api.CreatePullRequestOption{
   142  		Head:  fmt.Sprintf("%s:master", repo11.OwnerName),
   143  		Base:  "master",
   144  		Title: "create a failure pr",
   145  	}
   146  	req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &opts).AddTokenAuth(token)
   147  	MakeRequest(t, req, http.StatusForbidden)
   148  
   149  	// add user4 to be a collaborator to base repo
   150  	ctx := NewAPITestContext(t, repo10.OwnerName, repo10.Name, auth_model.AccessTokenScopeWriteRepository)
   151  	t.Run("AddUser4AsCollaborator", doAPIAddCollaborator(ctx, user4.Name, perm.AccessModeRead))
   152  
   153  	// create again
   154  	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &opts).AddTokenAuth(token)
   155  	MakeRequest(t, req, http.StatusCreated)
   156  }
   157  
   158  func TestAPICreatePullHeadPermission(t *testing.T) {
   159  	defer tests.PrepareTestEnv(t)()
   160  	repo10 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
   161  	// repo10 have code, pulls units.
   162  	repo11 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11})
   163  	// repo11 only have code unit but should still create pulls
   164  	owner10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo10.OwnerID})
   165  	user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
   166  
   167  	session := loginUser(t, user4.Name)
   168  	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
   169  	opts := &api.CreatePullRequestOption{
   170  		Head:  fmt.Sprintf("%s:master", repo11.OwnerName),
   171  		Base:  "master",
   172  		Title: "create a failure pr",
   173  	}
   174  	req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &opts).AddTokenAuth(token)
   175  	MakeRequest(t, req, http.StatusForbidden)
   176  
   177  	// add user4 to be a collaborator to head repo with read permission
   178  	ctx := NewAPITestContext(t, repo11.OwnerName, repo11.Name, auth_model.AccessTokenScopeWriteRepository)
   179  	t.Run("AddUser4AsCollaboratorWithRead", doAPIAddCollaborator(ctx, user4.Name, perm.AccessModeRead))
   180  	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &opts).AddTokenAuth(token)
   181  	MakeRequest(t, req, http.StatusForbidden)
   182  
   183  	// add user4 to be a collaborator to head repo with write permission
   184  	t.Run("AddUser4AsCollaboratorWithWrite", doAPIAddCollaborator(ctx, user4.Name, perm.AccessModeWrite))
   185  	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &opts).AddTokenAuth(token)
   186  	MakeRequest(t, req, http.StatusCreated)
   187  }
   188  
   189  func TestAPICreatePullSameRepoSuccess(t *testing.T) {
   190  	defer tests.PrepareTestEnv(t)()
   191  	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
   192  	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
   193  
   194  	session := loginUser(t, owner.Name)
   195  	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
   196  
   197  	req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner.Name, repo.Name), &api.CreatePullRequestOption{
   198  		Head:  fmt.Sprintf("%s:pr-to-update", owner.Name),
   199  		Base:  "master",
   200  		Title: "successfully create a PR between branches of the same repository",
   201  	}).AddTokenAuth(token)
   202  	MakeRequest(t, req, http.StatusCreated)
   203  	MakeRequest(t, req, http.StatusUnprocessableEntity) // second request should fail
   204  }
   205  
   206  func TestAPICreatePullWithFieldsSuccess(t *testing.T) {
   207  	defer tests.PrepareTestEnv(t)()
   208  	// repo10 have code, pulls units.
   209  	repo10 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
   210  	owner10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo10.OwnerID})
   211  	// repo11 only have code unit but should still create pulls
   212  	repo11 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11})
   213  	owner11 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo11.OwnerID})
   214  
   215  	session := loginUser(t, owner11.Name)
   216  	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
   217  
   218  	opts := &api.CreatePullRequestOption{
   219  		Head:      fmt.Sprintf("%s:master", owner11.Name),
   220  		Base:      "master",
   221  		Title:     "create a failure pr",
   222  		Body:      "foobaaar",
   223  		Milestone: 5,
   224  		Assignees: []string{owner10.Name},
   225  		Labels:    []int64{5},
   226  	}
   227  
   228  	req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), opts).
   229  		AddTokenAuth(token)
   230  
   231  	res := MakeRequest(t, req, http.StatusCreated)
   232  	pull := new(api.PullRequest)
   233  	DecodeJSON(t, res, pull)
   234  
   235  	assert.NotNil(t, pull.Milestone)
   236  	assert.EqualValues(t, opts.Milestone, pull.Milestone.ID)
   237  	if assert.Len(t, pull.Assignees, 1) {
   238  		assert.EqualValues(t, opts.Assignees[0], owner10.Name)
   239  	}
   240  	assert.NotNil(t, pull.Labels)
   241  	assert.EqualValues(t, opts.Labels[0], pull.Labels[0].ID)
   242  }
   243  
   244  func TestAPICreatePullWithFieldsFailure(t *testing.T) {
   245  	defer tests.PrepareTestEnv(t)()
   246  	// repo10 have code, pulls units.
   247  	repo10 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
   248  	owner10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo10.OwnerID})
   249  	// repo11 only have code unit but should still create pulls
   250  	repo11 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11})
   251  	owner11 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo11.OwnerID})
   252  
   253  	session := loginUser(t, owner11.Name)
   254  	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
   255  
   256  	opts := &api.CreatePullRequestOption{
   257  		Head: fmt.Sprintf("%s:master", owner11.Name),
   258  		Base: "master",
   259  	}
   260  
   261  	req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), opts).
   262  		AddTokenAuth(token)
   263  	MakeRequest(t, req, http.StatusUnprocessableEntity)
   264  	opts.Title = "is required"
   265  
   266  	opts.Milestone = 666
   267  	MakeRequest(t, req, http.StatusUnprocessableEntity)
   268  	opts.Milestone = 5
   269  
   270  	opts.Assignees = []string{"qweruqweroiuyqweoiruywqer"}
   271  	MakeRequest(t, req, http.StatusUnprocessableEntity)
   272  	opts.Assignees = []string{owner10.LoginName}
   273  
   274  	opts.Labels = []int64{55555}
   275  	MakeRequest(t, req, http.StatusUnprocessableEntity)
   276  	opts.Labels = []int64{5}
   277  }
   278  
   279  func TestAPIEditPull(t *testing.T) {
   280  	defer tests.PrepareTestEnv(t)()
   281  	repo10 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
   282  	owner10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo10.OwnerID})
   283  
   284  	session := loginUser(t, owner10.Name)
   285  	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
   286  	title := "create a success pr"
   287  	req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &api.CreatePullRequestOption{
   288  		Head:  "develop",
   289  		Base:  "master",
   290  		Title: title,
   291  	}).AddTokenAuth(token)
   292  	apiPull := new(api.PullRequest)
   293  	resp := MakeRequest(t, req, http.StatusCreated)
   294  	DecodeJSON(t, resp, apiPull)
   295  	assert.EqualValues(t, "master", apiPull.Base.Name)
   296  
   297  	newTitle := "edit a this pr"
   298  	newBody := "edited body"
   299  	req = NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner10.Name, repo10.Name, apiPull.Index), &api.EditPullRequestOption{
   300  		Base:  "feature/1",
   301  		Title: newTitle,
   302  		Body:  &newBody,
   303  	}).AddTokenAuth(token)
   304  	resp = MakeRequest(t, req, http.StatusCreated)
   305  	DecodeJSON(t, resp, apiPull)
   306  	assert.EqualValues(t, "feature/1", apiPull.Base.Name)
   307  	// check comment history
   308  	pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: apiPull.ID})
   309  	err := pull.LoadIssue(db.DefaultContext)
   310  	assert.NoError(t, err)
   311  	unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: pull.Issue.ID, OldTitle: title, NewTitle: newTitle})
   312  	unittest.AssertExistsAndLoadBean(t, &issues_model.ContentHistory{IssueID: pull.Issue.ID, ContentText: newBody, IsFirstCreated: false})
   313  
   314  	req = NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner10.Name, repo10.Name, pull.Index), &api.EditPullRequestOption{
   315  		Base: "not-exist",
   316  	}).AddTokenAuth(token)
   317  	MakeRequest(t, req, http.StatusNotFound)
   318  }
   319  
   320  func doAPIGetPullFiles(ctx APITestContext, pr *api.PullRequest, callback func(*testing.T, []*api.ChangedFile)) func(*testing.T) {
   321  	return func(t *testing.T) {
   322  		req := NewRequest(t, http.MethodGet, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/files", ctx.Username, ctx.Reponame, pr.Index)).
   323  			AddTokenAuth(ctx.Token)
   324  		if ctx.ExpectedCode == 0 {
   325  			ctx.ExpectedCode = http.StatusOK
   326  		}
   327  		resp := ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
   328  
   329  		files := make([]*api.ChangedFile, 0, 1)
   330  		DecodeJSON(t, resp, &files)
   331  
   332  		if callback != nil {
   333  			callback(t, files)
   334  		}
   335  	}
   336  }
   337  
   338  func TestAPICommitPullRequest(t *testing.T) {
   339  	defer tests.PrepareTestEnv(t)()
   340  	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
   341  	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
   342  
   343  	ctx := NewAPITestContext(t, "user2", repo.Name, auth_model.AccessTokenScopeReadRepository)
   344  
   345  	mergedCommitSHA := "1a8823cd1a9549fde083f992f6b9b87a7ab74fb3"
   346  	req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/commits/%s/pull", owner.Name, repo.Name, mergedCommitSHA).AddTokenAuth(ctx.Token)
   347  	ctx.Session.MakeRequest(t, req, http.StatusOK)
   348  
   349  	invalidCommitSHA := "abcd1234abcd1234abcd1234abcd1234abcd1234"
   350  	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/commits/%s/pull", owner.Name, repo.Name, invalidCommitSHA).AddTokenAuth(ctx.Token)
   351  	ctx.Session.MakeRequest(t, req, http.StatusNotFound)
   352  }