Skip to content

[API] Add issue and comment attachments #14601

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
f9595c3
[API] Add issue and comment attachments
andrebruch Feb 7, 2021
04abe85
Specify capacity for assets slice
andrebruch Feb 8, 2021
bef0c92
Make comment attachments accessible
andrebruch Mar 2, 2021
10d7f90
Update attachments in correct order
andrebruch Mar 2, 2021
806143c
Fix: Notifications for API attachments
andrebruch Mar 3, 2021
dc49872
Fix: new line in v1_json.tmpl
andrebruch Mar 13, 2021
7393e67
Fix import order
andrebruch Mar 13, 2021
488a072
Fix missed return
andrebruch Mar 13, 2021
7ac28eb
Merge branch 'master' into feature-add-issue-and-comment-attachments-api
zeripath Mar 13, 2021
6e3cf41
Merge remote-tracking branch 'origin/main' into feature-add-issue-and…
zeripath Jun 27, 2021
7dff40d
Merge branch 'main' into feature-add-issue-and-comment-attachments-api
6543 Jun 29, 2021
75785e1
partial correction of permissions checks
zeripath Jun 29, 2021
202ece1
Fix title of error
andrebruch Oct 2, 2021
587605e
Merge functions ToIssueAttachment and ToCommentAttachment
andrebruch Oct 2, 2021
036dd17
deduplicate convert.ToAttachment()
noerw Sep 30, 2021
9c433e5
refer to issue by index, not by ID..
noerw Sep 30, 2021
065d88f
Merge branch 'master' into feature-add-issue-and-comment-attachments-api
6543 Oct 3, 2021
d883f69
use attachment_service.UploadAttachment()
noerw Sep 30, 2021
e98bf3e
fix permissions of *IssueAttachment()
noerw Oct 2, 2021
f2a05e7
WIP: fix permissions of *IssueCommentAttachment()
noerw Oct 2, 2021
ad9a947
rm dublicate func
6543 Oct 3, 2021
f99ee82
update swagger
6543 Oct 3, 2021
5134ff6
fixup! fix permissions of *IssueAttachment()
noerw Oct 8, 2021
7557df1
fix permissions of *IssueCommentAttachment() (cont)
noerw Oct 8, 2021
170134a
move all asset APIs into issue / comment namespace
noerw Oct 8, 2021
a776dc2
fix swagger docs
noerw Oct 8, 2021
bfafd9d
fix updating issue
noerw Oct 8, 2021
ed6d55e
Merge branch 'main' into feature-add-issue-and-comment-attachments-api
6543 Oct 8, 2021
6b521f2
Correct the year of the code contribution
andrebruch Oct 15, 2021
e5633d8
Merge branch 'main' into feature-add-issue-and-comment-attachments-api
6543 Oct 19, 2021
32e8508
fix errors
noerw Oct 9, 2021
d32924d
Fix call of GetIssueByIndex and correction of grammar errors
andrebruch Oct 21, 2021
500d163
Merge branch 'main' into feature-add-issue-and-comment-attachments-api
andrebruch Oct 21, 2021
1459011
Remove redundant `return` statement
andrebruch Oct 21, 2021
6d6b858
make generate-swagger
andrebruch Oct 21, 2021
6517302
Add newline to v1_json.tmpl
andrebruch Oct 21, 2021
cbd1a77
[API] Add integration tests for issue attachments
andrebruch Oct 23, 2021
992b8ec
Merge branch 'main' into feature-add-issue-and-comment-attachments-api
6543 Oct 25, 2021
9227070
[API] Add integration tests for comment attachments
andrebruch Oct 29, 2021
d05625b
Merge branch 'main' into feature-add-issue-and-comment-attachments-api
6543 Oct 29, 2021
e39c3de
Merge branch 'master' into feature-add-issue-and-comment-attachments-api
6543 Apr 22, 2022
fbab446
adapt latest refactors
6543 Apr 22, 2022
c8cafc0
rm merge conflict resolve
6543 Apr 22, 2022
5bd533c
format & fix
6543 Apr 22, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions integrations/api_comment_attachment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Copyright 2021 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"
"fmt"
"io"
"mime/multipart"
"net/http"
"testing"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/convert"
api "code.gitea.io/gitea/modules/structs"

"github.com/stretchr/testify/assert"
)

func TestAPIGetCommentAttachment(t *testing.T) {
defer prepareTestEnv(t)()

comment := db.AssertExistsAndLoadBean(t, &models.Comment{ID: 2}).(*models.Comment)
assert.NoError(t, comment.LoadIssue())
assert.NoError(t, comment.LoadAttachments())
attachment := db.AssertExistsAndLoadBean(t, &models.Attachment{ID: comment.Attachments[0].ID}).(*models.Attachment)
repo := db.AssertExistsAndLoadBean(t, &models.Repository{ID: comment.Issue.RepoID}).(*models.Repository)
repoOwner := db.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User)

session := loginUser(t, repoOwner.Name)
token := getTokenForLoggedInUser(t, session)
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets/%d", repoOwner.Name, repo.Name, comment.ID, attachment.ID)
resp := session.MakeRequest(t, req, http.StatusOK)
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets/%d?token=%s", repoOwner.Name, repo.Name, comment.ID, attachment.ID, token)
resp = session.MakeRequest(t, req, http.StatusOK)

var apiAttachment api.Attachment
DecodeJSON(t, resp, &apiAttachment)

expect := convert.ToAttachment(attachment)
assert.Equal(t, expect.ID, apiAttachment.ID)
assert.Equal(t, expect.Name, apiAttachment.Name)
assert.Equal(t, expect.UUID, apiAttachment.UUID)
assert.Equal(t, expect.Created.Unix(), apiAttachment.Created.Unix())
}

func TestAPIListCommentAttachments(t *testing.T) {
defer prepareTestEnv(t)()

comment := db.AssertExistsAndLoadBean(t, &models.Comment{ID: 2}).(*models.Comment)
issue := db.AssertExistsAndLoadBean(t, &models.Issue{ID: comment.IssueID}).(*models.Issue)
repo := db.AssertExistsAndLoadBean(t, &models.Repository{ID: issue.RepoID}).(*models.Repository)
repoOwner := db.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User)

session := loginUser(t, repoOwner.Name)
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets",
repoOwner.Name, repo.Name, comment.ID)
resp := session.MakeRequest(t, req, http.StatusOK)

var apiAttachments []*api.Attachment
DecodeJSON(t, resp, &apiAttachments)
expectedCount := db.GetCount(t, &models.Attachment{CommentID: comment.ID})
assert.EqualValues(t, expectedCount, len(apiAttachments))
db.AssertExistsAndLoadBean(t, &models.Attachment{ID: apiAttachments[0].ID, CommentID: comment.ID})
}

func TestAPICreateCommentAttachment(t *testing.T) {
defer prepareTestEnv(t)()

comment := db.AssertExistsAndLoadBean(t, &models.Comment{ID: 2}).(*models.Comment)
issue := db.AssertExistsAndLoadBean(t, &models.Issue{ID: comment.IssueID}).(*models.Issue)
repo := db.AssertExistsAndLoadBean(t, &models.Repository{ID: issue.RepoID}).(*models.Repository)
repoOwner := db.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User)

session := loginUser(t, repoOwner.Name)
token := getTokenForLoggedInUser(t, session)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/assets?token=%s",
repoOwner.Name, repo.Name, comment.ID, token)

filename := "image.png"
buff := generateImg()
body := &bytes.Buffer{}

// Setup multi-part
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("attachment", filename)
assert.NoError(t, err)
_, err = io.Copy(part, &buff)
assert.NoError(t, err)
err = writer.Close()
assert.NoError(t, err)

req := NewRequestWithBody(t, "POST", urlStr, body)
req.Header.Add("Content-Type", writer.FormDataContentType())
resp := session.MakeRequest(t, req, http.StatusCreated)

apiAttachment := new(api.Attachment)
DecodeJSON(t, resp, &apiAttachment)

db.AssertExistsAndLoadBean(t, &models.Attachment{ID: apiAttachment.ID, CommentID: comment.ID})
}

func TestAPIEditCommentAttachment(t *testing.T) {
defer prepareTestEnv(t)()
const newAttachmentName = "newAttachmentName"

attachment := db.AssertExistsAndLoadBean(t, &models.Attachment{ID: 6}).(*models.Attachment)
comment := db.AssertExistsAndLoadBean(t, &models.Comment{ID: attachment.CommentID}).(*models.Comment)
issue := db.AssertExistsAndLoadBean(t, &models.Issue{ID: comment.IssueID}).(*models.Issue)
repo := db.AssertExistsAndLoadBean(t, &models.Repository{ID: issue.RepoID}).(*models.Repository)
repoOwner := db.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User)

session := loginUser(t, repoOwner.Name)
token := getTokenForLoggedInUser(t, session)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/assets/%d?token=%s",
repoOwner.Name, repo.Name, comment.ID, attachment.ID, token)
req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
"name": newAttachmentName,
})
resp := session.MakeRequest(t, req, http.StatusCreated)
apiAttachment := new(api.Attachment)
DecodeJSON(t, resp, &apiAttachment)

db.AssertExistsAndLoadBean(t, &models.Attachment{ID: apiAttachment.ID, CommentID: comment.ID, Name: apiAttachment.Name})
}

func TestAPIDeleteCommentAttachment(t *testing.T) {
defer prepareTestEnv(t)()

attachment := db.AssertExistsAndLoadBean(t, &models.Attachment{ID: 6}).(*models.Attachment)
comment := db.AssertExistsAndLoadBean(t, &models.Comment{ID: attachment.CommentID}).(*models.Comment)
issue := db.AssertExistsAndLoadBean(t, &models.Issue{ID: comment.IssueID}).(*models.Issue)
repo := db.AssertExistsAndLoadBean(t, &models.Repository{ID: issue.RepoID}).(*models.Repository)
repoOwner := db.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User)

session := loginUser(t, repoOwner.Name)
token := getTokenForLoggedInUser(t, session)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/assets/%d?token=%s",
repoOwner.Name, repo.Name, comment.ID, attachment.ID, token)

req := NewRequestf(t, "DELETE", urlStr)
session.MakeRequest(t, req, http.StatusNoContent)

db.AssertNotExistsBean(t, &models.Attachment{ID: attachment.ID, CommentID: comment.ID})
}
139 changes: 139 additions & 0 deletions integrations/api_issue_attachment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Copyright 2021 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"
"fmt"
"io"
"mime/multipart"
"net/http"
"testing"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
api "code.gitea.io/gitea/modules/structs"

"github.com/stretchr/testify/assert"
)

func TestAPIGetIssueAttachment(t *testing.T) {
defer prepareTestEnv(t)()

attachment := db.AssertExistsAndLoadBean(t, &models.Attachment{ID: 1}).(*models.Attachment)
repo := db.AssertExistsAndLoadBean(t, &models.Repository{ID: attachment.RepoID}).(*models.Repository)
issue := db.AssertExistsAndLoadBean(t, &models.Issue{RepoID: attachment.IssueID}).(*models.Issue)
repoOwner := db.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User)

session := loginUser(t, repoOwner.Name)
token := getTokenForLoggedInUser(t, session)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets/%d?token=%s",
repoOwner.Name, repo.Name, issue.Index, attachment.ID, token)

req := NewRequest(t, "GET", urlStr)
resp := session.MakeRequest(t, req, http.StatusOK)
apiAttachment := new(api.Attachment)
DecodeJSON(t, resp, &apiAttachment)

db.AssertExistsAndLoadBean(t, &models.Attachment{ID: apiAttachment.ID, IssueID: issue.ID})
}

func TestAPIListIssueAttachments(t *testing.T) {
defer prepareTestEnv(t)()

attachment := db.AssertExistsAndLoadBean(t, &models.Attachment{ID: 1}).(*models.Attachment)
repo := db.AssertExistsAndLoadBean(t, &models.Repository{ID: attachment.RepoID}).(*models.Repository)
issue := db.AssertExistsAndLoadBean(t, &models.Issue{RepoID: attachment.IssueID}).(*models.Issue)
repoOwner := db.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User)

session := loginUser(t, repoOwner.Name)
token := getTokenForLoggedInUser(t, session)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets?token=%s",
repoOwner.Name, repo.Name, issue.Index, token)

req := NewRequest(t, "GET", urlStr)
resp := session.MakeRequest(t, req, http.StatusOK)
apiAttachment := new([]api.Attachment)
DecodeJSON(t, resp, &apiAttachment)

db.AssertExistsAndLoadBean(t, &models.Attachment{ID: (*apiAttachment)[0].ID, IssueID: issue.ID})
}

func TestAPICreateIssueAttachment(t *testing.T) {
defer prepareTestEnv(t)()

repo := db.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
issue := db.AssertExistsAndLoadBean(t, &models.Issue{RepoID: repo.ID}).(*models.Issue)
repoOwner := db.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User)

session := loginUser(t, repoOwner.Name)
token := getTokenForLoggedInUser(t, session)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets?token=%s",
repoOwner.Name, repo.Name, issue.Index, token)

filename := "image.png"
buff := generateImg()
body := &bytes.Buffer{}

// Setup multi-part
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("attachment", filename)
assert.NoError(t, err)
_, err = io.Copy(part, &buff)
assert.NoError(t, err)
err = writer.Close()
assert.NoError(t, err)

req := NewRequestWithBody(t, "POST", urlStr, body)
req.Header.Add("Content-Type", writer.FormDataContentType())
resp := session.MakeRequest(t, req, http.StatusCreated)

apiAttachment := new(api.Attachment)
DecodeJSON(t, resp, &apiAttachment)

db.AssertExistsAndLoadBean(t, &models.Attachment{ID: apiAttachment.ID, IssueID: issue.ID})
}

func TestAPIEditIssueAttachment(t *testing.T) {
defer prepareTestEnv(t)()
const newAttachmentName = "newAttachmentName"

attachment := db.AssertExistsAndLoadBean(t, &models.Attachment{ID: 1}).(*models.Attachment)
repo := db.AssertExistsAndLoadBean(t, &models.Repository{ID: attachment.RepoID}).(*models.Repository)
issue := db.AssertExistsAndLoadBean(t, &models.Issue{RepoID: attachment.IssueID}).(*models.Issue)
repoOwner := db.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User)

session := loginUser(t, repoOwner.Name)
token := getTokenForLoggedInUser(t, session)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets/%d?token=%s",
repoOwner.Name, repo.Name, issue.Index, attachment.ID, token)
req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
"name": newAttachmentName,
})
resp := session.MakeRequest(t, req, http.StatusCreated)
apiAttachment := new(api.Attachment)
DecodeJSON(t, resp, &apiAttachment)

db.AssertExistsAndLoadBean(t, &models.Attachment{ID: apiAttachment.ID, IssueID: issue.ID, Name: apiAttachment.Name})
}

func TestAPIDeleteIssueAttachment(t *testing.T) {
defer prepareTestEnv(t)()

attachment := db.AssertExistsAndLoadBean(t, &models.Attachment{ID: 1}).(*models.Attachment)
repo := db.AssertExistsAndLoadBean(t, &models.Repository{ID: attachment.RepoID}).(*models.Repository)
issue := db.AssertExistsAndLoadBean(t, &models.Issue{RepoID: attachment.IssueID}).(*models.Issue)
repoOwner := db.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User)

session := loginUser(t, repoOwner.Name)
token := getTokenForLoggedInUser(t, session)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets/%d?token=%s",
repoOwner.Name, repo.Name, issue.Index, attachment.ID, token)

req := NewRequest(t, "DELETE", urlStr)
session.MakeRequest(t, req, http.StatusNoContent)

db.AssertNotExistsBean(t, &models.Attachment{ID: attachment.ID, IssueID: issue.ID})
}
23 changes: 13 additions & 10 deletions models/issue_comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -845,18 +845,21 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment
fallthrough
case CommentTypeReview:
// Check attachments
attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments)
if err != nil {
return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %v", opts.Attachments, err)
}
if len(opts.Attachments) > 0 {
attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments)
if err != nil {
return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %v", opts.Attachments, err)
}

for i := range attachments {
attachments[i].IssueID = opts.Issue.ID
attachments[i].CommentID = comment.ID
// No assign value could be 0, so ignore AllCols().
if _, err = e.ID(attachments[i].ID).Update(attachments[i]); err != nil {
return fmt.Errorf("update attachment [%d]: %v", attachments[i].ID, err)
for i := range attachments {
attachments[i].IssueID = opts.Issue.ID
attachments[i].CommentID = comment.ID
// No assign value could be 0, so ignore AllCols().
if _, err = e.ID(attachments[i].ID).Update(attachments[i]); err != nil {
return fmt.Errorf("update attachment [%d]: %v", attachments[i].ID, err)
}
}
comment.Attachments = attachments
}
case CommentTypeReopen, CommentTypeClose:
if err = updateIssueClosedNum(ctx, opts.Issue); err != nil {
Expand Down
5 changes: 0 additions & 5 deletions models/repo/attachment.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,11 +225,6 @@ func DeleteAttachmentsByComment(commentID int64, remove bool) (int, error) {
return DeleteAttachments(db.DefaultContext, attachments, remove)
}

// UpdateAttachment updates the given attachment in database
func UpdateAttachment(atta *Attachment) error {
return UpdateAttachmentCtx(db.DefaultContext, atta)
}

// UpdateAttachmentByUUID Updates attachment via uuid
func UpdateAttachmentByUUID(ctx context.Context, attach *Attachment, cols ...string) error {
if attach.UUID == "" {
Expand Down
2 changes: 1 addition & 1 deletion models/repo/attachment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func TestUpdateAttachment(t *testing.T) {
assert.Equal(t, "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", attach.UUID)

attach.Name = "new_name"
assert.NoError(t, UpdateAttachment(attach))
assert.NoError(t, UpdateAttachmentCtx(db.DefaultContext, attach))

unittest.AssertExistsAndLoadBean(t, &Attachment{Name: "new_name"})
}
Expand Down
23 changes: 23 additions & 0 deletions modules/convert/attachment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright 2021 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 convert

import (
repo_model "code.gitea.io/gitea/models/repo"
api "code.gitea.io/gitea/modules/structs"
)

// ToAttachment converts models.Attachment to api.Attachment
func ToAttachment(a *repo_model.Attachment) *api.Attachment {
return &api.Attachment{
ID: a.ID,
Name: a.Name,
Created: a.CreatedUnix.AsTime(),
DownloadCount: a.DownloadCount,
Size: a.Size,
UUID: a.UUID,
DownloadURL: a.DownloadURL(),
}
}
Loading