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

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