[API] Add Reactions (#9220)
* reject reactions wich ar not allowed * dont duble check CreateReaction now throw ErrForbiddenIssueReaction * add /repos/{owner}/{repo}/issues/comments/{id}/reactions endpoint * add Find Functions * fix some swagger stuff + add issue reaction endpoints + GET ReactionList now use FindReactions... * explicite Issue Only Reaction for FindReactionsOptions with "-1" commentID * load issue; load user ... * return error again * swagger def canged after LINT * check if user has ben loaded * add Tests * better way of comparing results * add suggestion * use different issue for test (dont interfear with integration test) * test dont compare Location on timeCompare * TEST: add forbidden dubble add * add comments in code to explain * add settings.UI.ReactionsMap so if !setting.UI.ReactionsMap[opts.Type] workstokarchuk/v1.17
parent
ee7df7ba8c
commit
37e10d4543
@ -0,0 +1,145 @@ |
||||
// 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 ( |
||||
"fmt" |
||||
"net/http" |
||||
"testing" |
||||
"time" |
||||
|
||||
"code.gitea.io/gitea/models" |
||||
api "code.gitea.io/gitea/modules/structs" |
||||
|
||||
"github.com/stretchr/testify/assert" |
||||
) |
||||
|
||||
func TestAPIIssuesReactions(t *testing.T) { |
||||
defer prepareTestEnv(t)() |
||||
|
||||
issue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 1}).(*models.Issue) |
||||
_ = issue.LoadRepo() |
||||
owner := models.AssertExistsAndLoadBean(t, &models.User{ID: issue.Repo.OwnerID}).(*models.User) |
||||
|
||||
session := loginUser(t, owner.Name) |
||||
token := getTokenForLoggedInUser(t, session) |
||||
|
||||
user1 := models.AssertExistsAndLoadBean(t, &models.User{ID: 1}).(*models.User) |
||||
user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) |
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/reactions?token=%s", |
||||
owner.Name, issue.Repo.Name, issue.Index, token) |
||||
|
||||
//Try to add not allowed reaction
|
||||
req := NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{ |
||||
Reaction: "wrong", |
||||
}) |
||||
resp := session.MakeRequest(t, req, http.StatusForbidden) |
||||
|
||||
//Delete not allowed reaction
|
||||
req = NewRequestWithJSON(t, "DELETE", urlStr, &api.EditReactionOption{ |
||||
Reaction: "zzz", |
||||
}) |
||||
resp = session.MakeRequest(t, req, http.StatusOK) |
||||
|
||||
//Add allowed reaction
|
||||
req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{ |
||||
Reaction: "rocket", |
||||
}) |
||||
resp = session.MakeRequest(t, req, http.StatusCreated) |
||||
var apiNewReaction api.ReactionResponse |
||||
DecodeJSON(t, resp, &apiNewReaction) |
||||
|
||||
//Add existing reaction
|
||||
resp = session.MakeRequest(t, req, http.StatusForbidden) |
||||
|
||||
//Get end result of reaction list of issue #1
|
||||
req = NewRequestf(t, "GET", urlStr) |
||||
resp = session.MakeRequest(t, req, http.StatusOK) |
||||
var apiReactions []*api.ReactionResponse |
||||
DecodeJSON(t, resp, &apiReactions) |
||||
expectResponse := make(map[int]api.ReactionResponse) |
||||
expectResponse[0] = api.ReactionResponse{ |
||||
User: user1.APIFormat(), |
||||
Reaction: "zzz", |
||||
Created: time.Unix(1573248002, 0), |
||||
} |
||||
expectResponse[1] = api.ReactionResponse{ |
||||
User: user2.APIFormat(), |
||||
Reaction: "eyes", |
||||
Created: time.Unix(1573248003, 0), |
||||
} |
||||
expectResponse[2] = apiNewReaction |
||||
assert.Len(t, apiReactions, 3) |
||||
for i, r := range apiReactions { |
||||
assert.Equal(t, expectResponse[i].Reaction, r.Reaction) |
||||
assert.Equal(t, expectResponse[i].Created.Unix(), r.Created.Unix()) |
||||
assert.Equal(t, expectResponse[i].User.ID, r.User.ID) |
||||
} |
||||
} |
||||
|
||||
func TestAPICommentReactions(t *testing.T) { |
||||
defer prepareTestEnv(t)() |
||||
|
||||
comment := models.AssertExistsAndLoadBean(t, &models.Comment{ID: 2}).(*models.Comment) |
||||
_ = comment.LoadIssue() |
||||
issue := comment.Issue |
||||
_ = issue.LoadRepo() |
||||
owner := models.AssertExistsAndLoadBean(t, &models.User{ID: issue.Repo.OwnerID}).(*models.User) |
||||
|
||||
session := loginUser(t, owner.Name) |
||||
token := getTokenForLoggedInUser(t, session) |
||||
|
||||
user1 := models.AssertExistsAndLoadBean(t, &models.User{ID: 1}).(*models.User) |
||||
user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) |
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/reactions?token=%s", |
||||
owner.Name, issue.Repo.Name, comment.ID, token) |
||||
|
||||
//Try to add not allowed reaction
|
||||
req := NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{ |
||||
Reaction: "wrong", |
||||
}) |
||||
resp := session.MakeRequest(t, req, http.StatusForbidden) |
||||
|
||||
//Delete none existing reaction
|
||||
req = NewRequestWithJSON(t, "DELETE", urlStr, &api.EditReactionOption{ |
||||
Reaction: "eyes", |
||||
}) |
||||
resp = session.MakeRequest(t, req, http.StatusOK) |
||||
|
||||
//Add allowed reaction
|
||||
req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{ |
||||
Reaction: "+1", |
||||
}) |
||||
resp = session.MakeRequest(t, req, http.StatusCreated) |
||||
var apiNewReaction api.ReactionResponse |
||||
DecodeJSON(t, resp, &apiNewReaction) |
||||
|
||||
//Add existing reaction
|
||||
resp = session.MakeRequest(t, req, http.StatusForbidden) |
||||
|
||||
//Get end result of reaction list of issue #1
|
||||
req = NewRequestf(t, "GET", urlStr) |
||||
resp = session.MakeRequest(t, req, http.StatusOK) |
||||
var apiReactions []*api.ReactionResponse |
||||
DecodeJSON(t, resp, &apiReactions) |
||||
expectResponse := make(map[int]api.ReactionResponse) |
||||
expectResponse[0] = api.ReactionResponse{ |
||||
User: user2.APIFormat(), |
||||
Reaction: "laugh", |
||||
Created: time.Unix(1573248004, 0), |
||||
} |
||||
expectResponse[1] = api.ReactionResponse{ |
||||
User: user1.APIFormat(), |
||||
Reaction: "laugh", |
||||
Created: time.Unix(1573248005, 0), |
||||
} |
||||
expectResponse[2] = apiNewReaction |
||||
assert.Len(t, apiReactions, 3) |
||||
for i, r := range apiReactions { |
||||
assert.Equal(t, expectResponse[i].Reaction, r.Reaction) |
||||
assert.Equal(t, expectResponse[i].Created.Unix(), r.Created.Unix()) |
||||
assert.Equal(t, expectResponse[i].User.ID, r.User.ID) |
||||
} |
||||
} |
@ -1 +1,39 @@ |
||||
[] # empty |
||||
- |
||||
id: 1 #issue reaction |
||||
type: zzz # not allowed reaction (added before allowed reaction list has changed) |
||||
issue_id: 1 |
||||
comment_id: 0 |
||||
user_id: 2 |
||||
created_unix: 1573248001 |
||||
|
||||
- |
||||
id: 2 #issue reaction |
||||
type: zzz # not allowed reaction (added before allowed reaction list has changed) |
||||
issue_id: 1 |
||||
comment_id: 0 |
||||
user_id: 1 |
||||
created_unix: 1573248002 |
||||
|
||||
- |
||||
id: 3 #issue reaction |
||||
type: eyes # allowed reaction |
||||
issue_id: 1 |
||||
comment_id: 0 |
||||
user_id: 2 |
||||
created_unix: 1573248003 |
||||
|
||||
- |
||||
id: 4 #comment reaction |
||||
type: laugh # allowed reaction |
||||
issue_id: 1 |
||||
comment_id: 2 |
||||
user_id: 2 |
||||
created_unix: 1573248004 |
||||
|
||||
- |
||||
id: 5 #comment reaction |
||||
type: laugh # allowed reaction |
||||
issue_id: 1 |
||||
comment_id: 2 |
||||
user_id: 1 |
||||
created_unix: 1573248005 |
||||
|
@ -0,0 +1,22 @@ |
||||
// 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 structs |
||||
|
||||
import ( |
||||
"time" |
||||
) |
||||
|
||||
// EditReactionOption contain the reaction type
|
||||
type EditReactionOption struct { |
||||
Reaction string `json:"content"` |
||||
} |
||||
|
||||
// ReactionResponse contain one reaction
|
||||
type ReactionResponse struct { |
||||
User *User `json:"user"` |
||||
Reaction string `json:"content"` |
||||
// swagger:strfmt date-time
|
||||
Created time.Time `json:"created_at"` |
||||
} |
@ -0,0 +1,394 @@ |
||||
// 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 repo |
||||
|
||||
import ( |
||||
"errors" |
||||
|
||||
"code.gitea.io/gitea/models" |
||||
"code.gitea.io/gitea/modules/context" |
||||
api "code.gitea.io/gitea/modules/structs" |
||||
) |
||||
|
||||
// GetIssueCommentReactions list reactions of a issue comment
|
||||
func GetIssueCommentReactions(ctx *context.APIContext) { |
||||
// swagger:operation GET /repos/{owner}/{repo}/issues/comments/{id}/reactions issue issueGetCommentReactions
|
||||
// ---
|
||||
// summary: Get a list reactions of a issue comment
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the comment to edit
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/ReactionResponseList"
|
||||
comment, err := models.GetCommentByID(ctx.ParamsInt64(":id")) |
||||
if err != nil { |
||||
if models.IsErrCommentNotExist(err) { |
||||
ctx.NotFound(err) |
||||
} else { |
||||
ctx.Error(500, "GetCommentByID", err) |
||||
} |
||||
return |
||||
} |
||||
|
||||
if !ctx.Repo.CanRead(models.UnitTypeIssues) && !ctx.User.IsAdmin { |
||||
ctx.Error(403, "GetIssueCommentReactions", errors.New("no permission to get reactions")) |
||||
return |
||||
} |
||||
|
||||
reactions, err := models.FindCommentReactions(comment) |
||||
if err != nil { |
||||
ctx.Error(500, "FindIssueReactions", err) |
||||
return |
||||
} |
||||
_, err = reactions.LoadUsers() |
||||
if err != nil { |
||||
ctx.Error(500, "ReactionList.LoadUsers()", err) |
||||
return |
||||
} |
||||
|
||||
var result []api.ReactionResponse |
||||
for _, r := range reactions { |
||||
result = append(result, api.ReactionResponse{ |
||||
User: r.User.APIFormat(), |
||||
Reaction: r.Type, |
||||
Created: r.CreatedUnix.AsTime(), |
||||
}) |
||||
} |
||||
|
||||
ctx.JSON(200, result) |
||||
} |
||||
|
||||
// PostIssueCommentReaction add a reaction to a comment of a issue
|
||||
func PostIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOption) { |
||||
// swagger:operation POST /repos/{owner}/{repo}/issues/comments/{id}/reactions issue issuePostCommentReaction
|
||||
// ---
|
||||
// summary: Add a reaction to a comment of a issue comment
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the comment to edit
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: content
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditReactionOption"
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/ReactionResponse"
|
||||
changeIssueCommentReaction(ctx, form, true) |
||||
} |
||||
|
||||
// DeleteIssueCommentReaction list reactions of a issue comment
|
||||
func DeleteIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOption) { |
||||
// swagger:operation DELETE /repos/{owner}/{repo}/issues/comments/{id}/reactions issue issueDeleteCommentReaction
|
||||
// ---
|
||||
// summary: Remove a reaction from a comment of a issue comment
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the comment to edit
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: content
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditReactionOption"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/empty"
|
||||
changeIssueCommentReaction(ctx, form, false) |
||||
} |
||||
|
||||
func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) { |
||||
comment, err := models.GetCommentByID(ctx.ParamsInt64(":id")) |
||||
if err != nil { |
||||
if models.IsErrCommentNotExist(err) { |
||||
ctx.NotFound(err) |
||||
} else { |
||||
ctx.Error(500, "GetCommentByID", err) |
||||
} |
||||
return |
||||
} |
||||
|
||||
err = comment.LoadIssue() |
||||
if err != nil { |
||||
ctx.Error(500, "comment.LoadIssue() failed", err) |
||||
} |
||||
|
||||
if comment.Issue.IsLocked && !ctx.Repo.CanWrite(models.UnitTypeIssues) && !ctx.User.IsAdmin { |
||||
ctx.Error(403, "ChangeIssueCommentReaction", errors.New("no permission to change reaction")) |
||||
return |
||||
} |
||||
|
||||
if isCreateType { |
||||
// PostIssueCommentReaction part
|
||||
reaction, err := models.CreateCommentReaction(ctx.User, comment.Issue, comment, form.Reaction) |
||||
if err != nil { |
||||
if models.IsErrForbiddenIssueReaction(err) { |
||||
ctx.Error(403, err.Error(), err) |
||||
} else { |
||||
ctx.Error(500, "CreateCommentReaction", err) |
||||
} |
||||
return |
||||
} |
||||
_, err = reaction.LoadUser() |
||||
if err != nil { |
||||
ctx.Error(500, "Reaction.LoadUser()", err) |
||||
return |
||||
} |
||||
|
||||
ctx.JSON(201, api.ReactionResponse{ |
||||
User: reaction.User.APIFormat(), |
||||
Reaction: reaction.Type, |
||||
Created: reaction.CreatedUnix.AsTime(), |
||||
}) |
||||
} else { |
||||
// DeleteIssueCommentReaction part
|
||||
err = models.DeleteCommentReaction(ctx.User, comment.Issue, comment, form.Reaction) |
||||
if err != nil { |
||||
ctx.Error(500, "DeleteCommentReaction", err) |
||||
return |
||||
} |
||||
ctx.Status(200) |
||||
} |
||||
} |
||||
|
||||
// GetIssueReactions list reactions of a issue comment
|
||||
func GetIssueReactions(ctx *context.APIContext) { |
||||
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/reactions issue issueGetIssueReactions
|
||||
// ---
|
||||
// summary: Get a list reactions of a issue
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/ReactionResponseList"
|
||||
issue, err := models.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) |
||||
if err != nil { |
||||
if models.IsErrIssueNotExist(err) { |
||||
ctx.NotFound() |
||||
} else { |
||||
ctx.Error(500, "GetIssueByIndex", err) |
||||
} |
||||
return |
||||
} |
||||
|
||||
if !ctx.Repo.CanRead(models.UnitTypeIssues) && !ctx.User.IsAdmin { |
||||
ctx.Error(403, "GetIssueReactions", errors.New("no permission to get reactions")) |
||||
return |
||||
} |
||||
|
||||
reactions, err := models.FindIssueReactions(issue) |
||||
if err != nil { |
||||
ctx.Error(500, "FindIssueReactions", err) |
||||
return |
||||
} |
||||
_, err = reactions.LoadUsers() |
||||
if err != nil { |
||||
ctx.Error(500, "ReactionList.LoadUsers()", err) |
||||
return |
||||
} |
||||
|
||||
var result []api.ReactionResponse |
||||
for _, r := range reactions { |
||||
result = append(result, api.ReactionResponse{ |
||||
User: r.User.APIFormat(), |
||||
Reaction: r.Type, |
||||
Created: r.CreatedUnix.AsTime(), |
||||
}) |
||||
} |
||||
|
||||
ctx.JSON(200, result) |
||||
} |
||||
|
||||
// PostIssueReaction add a reaction to a comment of a issue
|
||||
func PostIssueReaction(ctx *context.APIContext, form api.EditReactionOption) { |
||||
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/reactions issue issuePostIssueReaction
|
||||
// ---
|
||||
// summary: Add a reaction to a comment of a issue
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: content
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditReactionOption"
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/ReactionResponse"
|
||||
changeIssueReaction(ctx, form, true) |
||||
} |
||||
|
||||
// DeleteIssueReaction list reactions of a issue comment
|
||||
func DeleteIssueReaction(ctx *context.APIContext, form api.EditReactionOption) { |
||||
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/reactions issue issueDeleteIssueReaction
|
||||
// ---
|
||||
// summary: Remove a reaction from a comment of a issue
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: content
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditReactionOption"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/empty"
|
||||
changeIssueReaction(ctx, form, false) |
||||
} |
||||
|
||||
func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) { |
||||
issue, err := models.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) |
||||
if err != nil { |
||||
if models.IsErrIssueNotExist(err) { |
||||
ctx.NotFound() |
||||
} else { |
||||
ctx.Error(500, "GetIssueByIndex", err) |
||||
} |
||||
return |
||||
} |
||||
|
||||
if issue.IsLocked && !ctx.Repo.CanWrite(models.UnitTypeIssues) && !ctx.User.IsAdmin { |
||||
ctx.Error(403, "ChangeIssueCommentReaction", errors.New("no permission to change reaction")) |
||||
return |
||||
} |
||||
|
||||
if isCreateType { |
||||
// PostIssueReaction part
|
||||
reaction, err := models.CreateIssueReaction(ctx.User, issue, form.Reaction) |
||||
if err != nil { |
||||
if models.IsErrForbiddenIssueReaction(err) { |
||||
ctx.Error(403, err.Error(), err) |
||||
} else { |
||||
ctx.Error(500, "CreateCommentReaction", err) |
||||
} |
||||
return |
||||
} |
||||
_, err = reaction.LoadUser() |
||||
if err != nil { |
||||
ctx.Error(500, "Reaction.LoadUser()", err) |
||||
return |
||||
} |
||||
|
||||
ctx.JSON(201, api.ReactionResponse{ |
||||
User: reaction.User.APIFormat(), |
||||
Reaction: reaction.Type, |
||||
Created: reaction.CreatedUnix.AsTime(), |
||||
}) |
||||
} else { |
||||
// DeleteIssueReaction part
|
||||
err = models.DeleteIssueReaction(ctx.User, issue, form.Reaction) |
||||
if err != nil { |
||||
ctx.Error(500, "DeleteIssueReaction", err) |
||||
return |
||||
} |
||||
ctx.Status(200) |
||||
} |
||||
} |
Loading…
Reference in new issue