* test: add current attachement responses * refactor: check if attachement is linked and accessible by user * chore: clean TODO * fix: typo attachement -> attachment * revert un-needed go.sum change * refactor: move models logic to models * fix TestCreateIssueAttachment which was wrongly successful * fix unit tests with unittype added * fix unit tests with changes * use a valid uuid format for pgsql int. test * test: add unit test TestLinkedRepository * refactor: allow uploader to access unlinked attachement * add missing blank line * refactor: move to a separate function repo.GetAttachment * typo * test: remove err test return * refactor: use repo perm for access checking generally + 404 for all rejecttags/v1.21.12.1
| @@ -1,88 +0,0 @@ | |||||
| // Copyright 2019 The Gitea Authors. All rights reserved. | |||||
| // Use of this source code is governed by a MIT-style | |||||
| // license that can be found in the LICENSE file. | |||||
| package integrations | |||||
| import ( | |||||
| "bytes" | |||||
| "image" | |||||
| "image/png" | |||||
| "io" | |||||
| "mime/multipart" | |||||
| "net/http" | |||||
| "testing" | |||||
| "code.gitea.io/gitea/modules/test" | |||||
| "github.com/stretchr/testify/assert" | |||||
| ) | |||||
| func generateImg() bytes.Buffer { | |||||
| // Generate image | |||||
| myImage := image.NewRGBA(image.Rect(0, 0, 32, 32)) | |||||
| var buff bytes.Buffer | |||||
| png.Encode(&buff, myImage) | |||||
| return buff | |||||
| } | |||||
| func createAttachment(t *testing.T, session *TestSession, repoURL, filename string, buff bytes.Buffer, expectedStatus int) string { | |||||
| body := &bytes.Buffer{} | |||||
| //Setup multi-part | |||||
| writer := multipart.NewWriter(body) | |||||
| part, err := writer.CreateFormFile("file", filename) | |||||
| assert.NoError(t, err) | |||||
| _, err = io.Copy(part, &buff) | |||||
| assert.NoError(t, err) | |||||
| err = writer.Close() | |||||
| assert.NoError(t, err) | |||||
| csrf := GetCSRF(t, session, repoURL) | |||||
| req := NewRequestWithBody(t, "POST", "/attachments", body) | |||||
| req.Header.Add("X-Csrf-Token", csrf) | |||||
| req.Header.Add("Content-Type", writer.FormDataContentType()) | |||||
| resp := session.MakeRequest(t, req, expectedStatus) | |||||
| if expectedStatus != http.StatusOK { | |||||
| return "" | |||||
| } | |||||
| var obj map[string]string | |||||
| DecodeJSON(t, resp, &obj) | |||||
| return obj["uuid"] | |||||
| } | |||||
| func TestCreateAnonymousAttachment(t *testing.T) { | |||||
| prepareTestEnv(t) | |||||
| session := emptyTestSession(t) | |||||
| createAttachment(t, session, "user2/repo1", "image.png", generateImg(), http.StatusFound) | |||||
| } | |||||
| func TestCreateIssueAttachement(t *testing.T) { | |||||
| prepareTestEnv(t) | |||||
| const repoURL = "user2/repo1" | |||||
| session := loginUser(t, "user2") | |||||
| uuid := createAttachment(t, session, repoURL, "image.png", generateImg(), http.StatusOK) | |||||
| req := NewRequest(t, "GET", repoURL+"/issues/new") | |||||
| resp := session.MakeRequest(t, req, http.StatusOK) | |||||
| htmlDoc := NewHTMLParser(t, resp.Body) | |||||
| link, exists := htmlDoc.doc.Find("form").Attr("action") | |||||
| assert.True(t, exists, "The template has changed") | |||||
| postData := map[string]string{ | |||||
| "_csrf": htmlDoc.GetCSRF(), | |||||
| "title": "New Issue With Attachement", | |||||
| "content": "some content", | |||||
| "files[0]": uuid, | |||||
| } | |||||
| req = NewRequestWithValues(t, "POST", link, postData) | |||||
| resp = session.MakeRequest(t, req, http.StatusFound) | |||||
| test.RedirectURL(resp) // check that redirect URL exists | |||||
| //Validate that attachement is available | |||||
| req = NewRequest(t, "GET", "/attachments/"+uuid) | |||||
| session.MakeRequest(t, req, http.StatusOK) | |||||
| } | |||||
| @@ -0,0 +1,137 @@ | |||||
| // Copyright 2019 The Gitea Authors. All rights reserved. | |||||
| // Use of this source code is governed by a MIT-style | |||||
| // license that can be found in the LICENSE file. | |||||
| package integrations | |||||
| import ( | |||||
| "bytes" | |||||
| "image" | |||||
| "image/png" | |||||
| "io" | |||||
| "io/ioutil" | |||||
| "mime/multipart" | |||||
| "net/http" | |||||
| "os" | |||||
| "path" | |||||
| "testing" | |||||
| "code.gitea.io/gitea/models" | |||||
| "code.gitea.io/gitea/modules/test" | |||||
| "github.com/stretchr/testify/assert" | |||||
| ) | |||||
| func generateImg() bytes.Buffer { | |||||
| // Generate image | |||||
| myImage := image.NewRGBA(image.Rect(0, 0, 32, 32)) | |||||
| var buff bytes.Buffer | |||||
| png.Encode(&buff, myImage) | |||||
| return buff | |||||
| } | |||||
| func createAttachment(t *testing.T, session *TestSession, repoURL, filename string, buff bytes.Buffer, expectedStatus int) string { | |||||
| body := &bytes.Buffer{} | |||||
| //Setup multi-part | |||||
| writer := multipart.NewWriter(body) | |||||
| part, err := writer.CreateFormFile("file", filename) | |||||
| assert.NoError(t, err) | |||||
| _, err = io.Copy(part, &buff) | |||||
| assert.NoError(t, err) | |||||
| err = writer.Close() | |||||
| assert.NoError(t, err) | |||||
| csrf := GetCSRF(t, session, repoURL) | |||||
| req := NewRequestWithBody(t, "POST", "/attachments", body) | |||||
| req.Header.Add("X-Csrf-Token", csrf) | |||||
| req.Header.Add("Content-Type", writer.FormDataContentType()) | |||||
| resp := session.MakeRequest(t, req, expectedStatus) | |||||
| if expectedStatus != http.StatusOK { | |||||
| return "" | |||||
| } | |||||
| var obj map[string]string | |||||
| DecodeJSON(t, resp, &obj) | |||||
| return obj["uuid"] | |||||
| } | |||||
| func TestCreateAnonymousAttachment(t *testing.T) { | |||||
| prepareTestEnv(t) | |||||
| session := emptyTestSession(t) | |||||
| createAttachment(t, session, "user2/repo1", "image.png", generateImg(), http.StatusFound) | |||||
| } | |||||
| func TestCreateIssueAttachment(t *testing.T) { | |||||
| prepareTestEnv(t) | |||||
| const repoURL = "user2/repo1" | |||||
| session := loginUser(t, "user2") | |||||
| uuid := createAttachment(t, session, repoURL, "image.png", generateImg(), http.StatusOK) | |||||
| req := NewRequest(t, "GET", repoURL+"/issues/new") | |||||
| resp := session.MakeRequest(t, req, http.StatusOK) | |||||
| htmlDoc := NewHTMLParser(t, resp.Body) | |||||
| link, exists := htmlDoc.doc.Find("form").Attr("action") | |||||
| assert.True(t, exists, "The template has changed") | |||||
| postData := map[string]string{ | |||||
| "_csrf": htmlDoc.GetCSRF(), | |||||
| "title": "New Issue With Attachment", | |||||
| "content": "some content", | |||||
| "files": uuid, | |||||
| } | |||||
| req = NewRequestWithValues(t, "POST", link, postData) | |||||
| resp = session.MakeRequest(t, req, http.StatusFound) | |||||
| test.RedirectURL(resp) // check that redirect URL exists | |||||
| //Validate that attachment is available | |||||
| req = NewRequest(t, "GET", "/attachments/"+uuid) | |||||
| session.MakeRequest(t, req, http.StatusOK) | |||||
| } | |||||
| func TestGetAttachment(t *testing.T) { | |||||
| prepareTestEnv(t) | |||||
| adminSession := loginUser(t, "user1") | |||||
| user2Session := loginUser(t, "user2") | |||||
| user8Session := loginUser(t, "user8") | |||||
| emptySession := emptyTestSession(t) | |||||
| testCases := []struct { | |||||
| name string | |||||
| uuid string | |||||
| createFile bool | |||||
| session *TestSession | |||||
| want int | |||||
| }{ | |||||
| {"LinkedIssueUUID", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", true, user2Session, http.StatusOK}, | |||||
| {"LinkedCommentUUID", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a17", true, user2Session, http.StatusOK}, | |||||
| {"linked_release_uuid", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a19", true, user2Session, http.StatusOK}, | |||||
| {"NotExistingUUID", "b0eebc99-9c0b-4ef8-bb6d-6bb9bd380a18", false, user2Session, http.StatusNotFound}, | |||||
| {"FileMissing", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a18", false, user2Session, http.StatusInternalServerError}, | |||||
| {"NotLinked", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a20", true, user2Session, http.StatusNotFound}, | |||||
| {"NotLinkedAccessibleByUploader", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a20", true, user8Session, http.StatusOK}, | |||||
| {"PublicByNonLogged", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", true, emptySession, http.StatusOK}, | |||||
| {"PrivateByNonLogged", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12", true, emptySession, http.StatusNotFound}, | |||||
| {"PrivateAccessibleByAdmin", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12", true, adminSession, http.StatusOK}, | |||||
| {"PrivateAccessibleByUser", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12", true, user2Session, http.StatusOK}, | |||||
| {"RepoNotAccessibleByUser", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12", true, user8Session, http.StatusNotFound}, | |||||
| {"OrgNotAccessibleByUser", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a21", true, user8Session, http.StatusNotFound}, | |||||
| } | |||||
| for _, tc := range testCases { | |||||
| t.Run(tc.name, func(t *testing.T) { | |||||
| //Write empty file to be available for response | |||||
| if tc.createFile { | |||||
| localPath := models.AttachmentLocalPath(tc.uuid) | |||||
| err := os.MkdirAll(path.Dir(localPath), os.ModePerm) | |||||
| assert.NoError(t, err) | |||||
| err = ioutil.WriteFile(localPath, []byte("hello world"), 0644) | |||||
| assert.NoError(t, err) | |||||
| } | |||||
| //Actual test | |||||
| req := NewRequest(t, "GET", "/attachments/"+tc.uuid) | |||||
| tc.session.MakeRequest(t, req, tc.want) | |||||
| }) | |||||
| } | |||||
| } | |||||
| @@ -71,6 +71,26 @@ func (a *Attachment) DownloadURL() string { | |||||
| return fmt.Sprintf("%sattachments/%s", setting.AppURL, a.UUID) | return fmt.Sprintf("%sattachments/%s", setting.AppURL, a.UUID) | ||||
| } | } | ||||
| // LinkedRepository returns the linked repo if any | |||||
| func (a *Attachment) LinkedRepository() (*Repository, UnitType, error) { | |||||
| if a.IssueID != 0 { | |||||
| iss, err := GetIssueByID(a.IssueID) | |||||
| if err != nil { | |||||
| return nil, UnitTypeIssues, err | |||||
| } | |||||
| repo, err := GetRepositoryByID(iss.RepoID) | |||||
| return repo, UnitTypeIssues, err | |||||
| } else if a.ReleaseID != 0 { | |||||
| rel, err := GetReleaseByID(a.ReleaseID) | |||||
| if err != nil { | |||||
| return nil, UnitTypeReleases, err | |||||
| } | |||||
| repo, err := GetRepositoryByID(rel.RepoID) | |||||
| return repo, UnitTypeReleases, err | |||||
| } | |||||
| return nil, -1, nil | |||||
| } | |||||
| // NewAttachment creates a new attachment object. | // NewAttachment creates a new attachment object. | ||||
| func NewAttachment(attach *Attachment, buf []byte, file io.Reader) (_ *Attachment, err error) { | func NewAttachment(attach *Attachment, buf []byte, file io.Reader) (_ *Attachment, err error) { | ||||
| attach.UUID = gouuid.NewV4().String() | attach.UUID = gouuid.NewV4().String() | ||||
| @@ -61,7 +61,7 @@ func TestGetByCommentOrIssueID(t *testing.T) { | |||||
| // count of attachments from issue ID | // count of attachments from issue ID | ||||
| attachments, err := GetAttachmentsByIssueID(1) | attachments, err := GetAttachmentsByIssueID(1) | ||||
| assert.NoError(t, err) | assert.NoError(t, err) | ||||
| assert.Equal(t, 2, len(attachments)) | |||||
| assert.Equal(t, 1, len(attachments)) | |||||
| attachments, err = GetAttachmentsByCommentID(1) | attachments, err = GetAttachmentsByCommentID(1) | ||||
| assert.NoError(t, err) | assert.NoError(t, err) | ||||
| @@ -73,7 +73,7 @@ func TestDeleteAttachments(t *testing.T) { | |||||
| count, err := DeleteAttachmentsByIssue(4, false) | count, err := DeleteAttachmentsByIssue(4, false) | ||||
| assert.NoError(t, err) | assert.NoError(t, err) | ||||
| assert.Equal(t, 1, count) | |||||
| assert.Equal(t, 2, count) | |||||
| count, err = DeleteAttachmentsByComment(2, false) | count, err = DeleteAttachmentsByComment(2, false) | ||||
| assert.NoError(t, err) | assert.NoError(t, err) | ||||
| @@ -128,3 +128,31 @@ func TestGetAttachmentsByUUIDs(t *testing.T) { | |||||
| assert.Equal(t, int64(1), attachList[0].IssueID) | assert.Equal(t, int64(1), attachList[0].IssueID) | ||||
| assert.Equal(t, int64(5), attachList[1].IssueID) | assert.Equal(t, int64(5), attachList[1].IssueID) | ||||
| } | } | ||||
| func TestLinkedRepository(t *testing.T) { | |||||
| assert.NoError(t, PrepareTestDatabase()) | |||||
| testCases := []struct { | |||||
| name string | |||||
| attachID int64 | |||||
| expectedRepo *Repository | |||||
| expectedUnitType UnitType | |||||
| }{ | |||||
| {"LinkedIssue", 1, &Repository{ID: 1}, UnitTypeIssues}, | |||||
| {"LinkedComment", 3, &Repository{ID: 1}, UnitTypeIssues}, | |||||
| {"LinkedRelease", 9, &Repository{ID: 1}, UnitTypeReleases}, | |||||
| {"Notlinked", 10, nil, -1}, | |||||
| } | |||||
| for _, tc := range testCases { | |||||
| t.Run(tc.name, func(t *testing.T) { | |||||
| attach, err := GetAttachmentByID(tc.attachID) | |||||
| assert.NoError(t, err) | |||||
| repo, unitType, err := attach.LinkedRepository() | |||||
| assert.NoError(t, err) | |||||
| if tc.expectedRepo != nil { | |||||
| assert.Equal(t, tc.expectedRepo.ID, repo.ID) | |||||
| } | |||||
| assert.Equal(t, tc.expectedUnitType, unitType) | |||||
| }) | |||||
| } | |||||
| } | |||||
| @@ -10,7 +10,7 @@ | |||||
| - | - | ||||
| id: 2 | id: 2 | ||||
| uuid: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12 | uuid: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12 | ||||
| issue_id: 1 | |||||
| issue_id: 4 | |||||
| comment_id: 0 | comment_id: 0 | ||||
| name: attach2 | name: attach2 | ||||
| download_count: 1 | download_count: 1 | ||||
| @@ -81,6 +81,15 @@ | |||||
| - | - | ||||
| id: 10 | id: 10 | ||||
| uuid: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a20 | uuid: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a20 | ||||
| uploader_id: 8 | |||||
| name: attach1 | |||||
| download_count: 0 | |||||
| created_unix: 946684800 | |||||
| - | |||||
| id: 11 | |||||
| uuid: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a21 | |||||
| release_id: 2 | |||||
| name: attach1 | name: attach1 | ||||
| download_count: 0 | download_count: 0 | ||||
| created_unix: 946684800 | |||||
| created_unix: 946684800 | |||||
| @@ -11,4 +11,19 @@ | |||||
| is_draft: false | is_draft: false | ||||
| is_prerelease: false | is_prerelease: false | ||||
| is_tag: false | is_tag: false | ||||
| created_unix: 946684800 | |||||
| created_unix: 946684800 | |||||
| - | |||||
| id: 2 | |||||
| repo_id: 40 | |||||
| publisher_id: 2 | |||||
| tag_name: "v1.1" | |||||
| lower_tag_name: "v1.1" | |||||
| target: "master" | |||||
| title: "testing-release" | |||||
| sha1: "65f1bf27bc3bf70f64657658635e66094edbcb4d" | |||||
| num_commits: 10 | |||||
| is_draft: false | |||||
| is_prerelease: false | |||||
| is_tag: false | |||||
| created_unix: 946684800 | |||||
| @@ -472,4 +472,10 @@ | |||||
| repo_id: 48 | repo_id: 48 | ||||
| type: 7 | type: 7 | ||||
| config: "{\"ExternalTrackerURL\":\"https://tracker.com\",\"ExternalTrackerFormat\":\"https://tracker.com/{user}/{repo}/issues/{index}\",\"ExternalTrackerStyle\":\"alphanumeric\"}" | config: "{\"ExternalTrackerURL\":\"https://tracker.com\",\"ExternalTrackerFormat\":\"https://tracker.com/{user}/{repo}/issues/{index}\",\"ExternalTrackerStyle\":\"alphanumeric\"}" | ||||
| created_unix: 946684810 | |||||
| - | |||||
| id: 69 | |||||
| repo_id: 2 | |||||
| type: 2 | |||||
| config: "{}" | |||||
| created_unix: 946684810 | created_unix: 946684810 | ||||
| @@ -6,6 +6,8 @@ package repo | |||||
| import ( | import ( | ||||
| "fmt" | "fmt" | ||||
| "net/http" | |||||
| "os" | |||||
| "strings" | "strings" | ||||
| "code.gitea.io/gitea/models" | "code.gitea.io/gitea/models" | ||||
| @@ -85,3 +87,57 @@ func DeleteAttachment(ctx *context.Context) { | |||||
| "uuid": attach.UUID, | "uuid": attach.UUID, | ||||
| }) | }) | ||||
| } | } | ||||
| // GetAttachment serve attachements | |||||
| func GetAttachment(ctx *context.Context) { | |||||
| attach, err := models.GetAttachmentByUUID(ctx.Params(":uuid")) | |||||
| if err != nil { | |||||
| if models.IsErrAttachmentNotExist(err) { | |||||
| ctx.Error(404) | |||||
| } else { | |||||
| ctx.ServerError("GetAttachmentByUUID", err) | |||||
| } | |||||
| return | |||||
| } | |||||
| repository, unitType, err := attach.LinkedRepository() | |||||
| if err != nil { | |||||
| ctx.ServerError("LinkedRepository", err) | |||||
| return | |||||
| } | |||||
| if repository == nil { //If not linked | |||||
| if !(ctx.IsSigned && attach.UploaderID == ctx.User.ID) { //We block if not the uploader | |||||
| ctx.Error(http.StatusNotFound) | |||||
| return | |||||
| } | |||||
| } else { //If we have the repository we check access | |||||
| perm, err := models.GetUserRepoPermission(repository, ctx.User) | |||||
| if err != nil { | |||||
| ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err.Error()) | |||||
| return | |||||
| } | |||||
| if !perm.CanRead(unitType) { | |||||
| ctx.Error(http.StatusNotFound) | |||||
| return | |||||
| } | |||||
| } | |||||
| //If we have matched and access to release or issue | |||||
| fr, err := os.Open(attach.LocalPath()) | |||||
| if err != nil { | |||||
| ctx.ServerError("Open", err) | |||||
| return | |||||
| } | |||||
| defer fr.Close() | |||||
| if err := attach.IncreaseDownloadCount(); err != nil { | |||||
| ctx.ServerError("Update", err) | |||||
| return | |||||
| } | |||||
| if err = ServeData(ctx, attach.Name, fr); err != nil { | |||||
| ctx.ServerError("ServeData", err) | |||||
| return | |||||
| } | |||||
| } | |||||
| @@ -8,7 +8,6 @@ import ( | |||||
| "bytes" | "bytes" | ||||
| "encoding/gob" | "encoding/gob" | ||||
| "net/http" | "net/http" | ||||
| "os" | |||||
| "path" | "path" | ||||
| "text/template" | "text/template" | ||||
| "time" | "time" | ||||
| @@ -474,34 +473,7 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||
| m.Get("/following", user.Following) | m.Get("/following", user.Following) | ||||
| }) | }) | ||||
| m.Get("/attachments/:uuid", func(ctx *context.Context) { | |||||
| attach, err := models.GetAttachmentByUUID(ctx.Params(":uuid")) | |||||
| if err != nil { | |||||
| if models.IsErrAttachmentNotExist(err) { | |||||
| ctx.Error(404) | |||||
| } else { | |||||
| ctx.ServerError("GetAttachmentByUUID", err) | |||||
| } | |||||
| return | |||||
| } | |||||
| fr, err := os.Open(attach.LocalPath()) | |||||
| if err != nil { | |||||
| ctx.ServerError("Open", err) | |||||
| return | |||||
| } | |||||
| defer fr.Close() | |||||
| if err := attach.IncreaseDownloadCount(); err != nil { | |||||
| ctx.ServerError("Update", err) | |||||
| return | |||||
| } | |||||
| if err = repo.ServeData(ctx, attach.Name, fr); err != nil { | |||||
| ctx.ServerError("ServeData", err) | |||||
| return | |||||
| } | |||||
| }) | |||||
| m.Get("/attachments/:uuid", repo.GetAttachment) | |||||
| }, ignSignIn) | }, ignSignIn) | ||||
| m.Group("/attachments", func() { | m.Group("/attachments", func() { | ||||
| @@ -26,10 +26,10 @@ func TestIssues(t *testing.T) { | |||||
| Issues(ctx) | Issues(ctx) | ||||
| assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) | assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) | ||||
| assert.EqualValues(t, map[int64]int64{1: 1}, ctx.Data["Counts"]) | |||||
| assert.EqualValues(t, map[int64]int64{1: 1, 2: 1}, ctx.Data["Counts"]) | |||||
| assert.EqualValues(t, true, ctx.Data["IsShowClosed"]) | assert.EqualValues(t, true, ctx.Data["IsShowClosed"]) | ||||
| assert.Len(t, ctx.Data["Issues"], 1) | assert.Len(t, ctx.Data["Issues"], 1) | ||||
| assert.Len(t, ctx.Data["Repos"], 1) | |||||
| assert.Len(t, ctx.Data["Repos"], 2) | |||||
| } | } | ||||
| func TestMilestones(t *testing.T) { | func TestMilestones(t *testing.T) { | ||||