Skip to content

Commit 37e10d4

Browse files
6543techknowlogick
authored andcommitted
[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] works
1 parent ee7df7b commit 37e10d4

File tree

12 files changed

+1049
-32
lines changed

12 files changed

+1049
-32
lines changed
+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Copyright 2019 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package integrations
6+
7+
import (
8+
"fmt"
9+
"net/http"
10+
"testing"
11+
"time"
12+
13+
"code.gitea.io/gitea/models"
14+
api "code.gitea.io/gitea/modules/structs"
15+
16+
"github.com/stretchr/testify/assert"
17+
)
18+
19+
func TestAPIIssuesReactions(t *testing.T) {
20+
defer prepareTestEnv(t)()
21+
22+
issue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 1}).(*models.Issue)
23+
_ = issue.LoadRepo()
24+
owner := models.AssertExistsAndLoadBean(t, &models.User{ID: issue.Repo.OwnerID}).(*models.User)
25+
26+
session := loginUser(t, owner.Name)
27+
token := getTokenForLoggedInUser(t, session)
28+
29+
user1 := models.AssertExistsAndLoadBean(t, &models.User{ID: 1}).(*models.User)
30+
user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
31+
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/reactions?token=%s",
32+
owner.Name, issue.Repo.Name, issue.Index, token)
33+
34+
//Try to add not allowed reaction
35+
req := NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
36+
Reaction: "wrong",
37+
})
38+
resp := session.MakeRequest(t, req, http.StatusForbidden)
39+
40+
//Delete not allowed reaction
41+
req = NewRequestWithJSON(t, "DELETE", urlStr, &api.EditReactionOption{
42+
Reaction: "zzz",
43+
})
44+
resp = session.MakeRequest(t, req, http.StatusOK)
45+
46+
//Add allowed reaction
47+
req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
48+
Reaction: "rocket",
49+
})
50+
resp = session.MakeRequest(t, req, http.StatusCreated)
51+
var apiNewReaction api.ReactionResponse
52+
DecodeJSON(t, resp, &apiNewReaction)
53+
54+
//Add existing reaction
55+
resp = session.MakeRequest(t, req, http.StatusForbidden)
56+
57+
//Get end result of reaction list of issue #1
58+
req = NewRequestf(t, "GET", urlStr)
59+
resp = session.MakeRequest(t, req, http.StatusOK)
60+
var apiReactions []*api.ReactionResponse
61+
DecodeJSON(t, resp, &apiReactions)
62+
expectResponse := make(map[int]api.ReactionResponse)
63+
expectResponse[0] = api.ReactionResponse{
64+
User: user1.APIFormat(),
65+
Reaction: "zzz",
66+
Created: time.Unix(1573248002, 0),
67+
}
68+
expectResponse[1] = api.ReactionResponse{
69+
User: user2.APIFormat(),
70+
Reaction: "eyes",
71+
Created: time.Unix(1573248003, 0),
72+
}
73+
expectResponse[2] = apiNewReaction
74+
assert.Len(t, apiReactions, 3)
75+
for i, r := range apiReactions {
76+
assert.Equal(t, expectResponse[i].Reaction, r.Reaction)
77+
assert.Equal(t, expectResponse[i].Created.Unix(), r.Created.Unix())
78+
assert.Equal(t, expectResponse[i].User.ID, r.User.ID)
79+
}
80+
}
81+
82+
func TestAPICommentReactions(t *testing.T) {
83+
defer prepareTestEnv(t)()
84+
85+
comment := models.AssertExistsAndLoadBean(t, &models.Comment{ID: 2}).(*models.Comment)
86+
_ = comment.LoadIssue()
87+
issue := comment.Issue
88+
_ = issue.LoadRepo()
89+
owner := models.AssertExistsAndLoadBean(t, &models.User{ID: issue.Repo.OwnerID}).(*models.User)
90+
91+
session := loginUser(t, owner.Name)
92+
token := getTokenForLoggedInUser(t, session)
93+
94+
user1 := models.AssertExistsAndLoadBean(t, &models.User{ID: 1}).(*models.User)
95+
user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
96+
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/reactions?token=%s",
97+
owner.Name, issue.Repo.Name, comment.ID, token)
98+
99+
//Try to add not allowed reaction
100+
req := NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
101+
Reaction: "wrong",
102+
})
103+
resp := session.MakeRequest(t, req, http.StatusForbidden)
104+
105+
//Delete none existing reaction
106+
req = NewRequestWithJSON(t, "DELETE", urlStr, &api.EditReactionOption{
107+
Reaction: "eyes",
108+
})
109+
resp = session.MakeRequest(t, req, http.StatusOK)
110+
111+
//Add allowed reaction
112+
req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
113+
Reaction: "+1",
114+
})
115+
resp = session.MakeRequest(t, req, http.StatusCreated)
116+
var apiNewReaction api.ReactionResponse
117+
DecodeJSON(t, resp, &apiNewReaction)
118+
119+
//Add existing reaction
120+
resp = session.MakeRequest(t, req, http.StatusForbidden)
121+
122+
//Get end result of reaction list of issue #1
123+
req = NewRequestf(t, "GET", urlStr)
124+
resp = session.MakeRequest(t, req, http.StatusOK)
125+
var apiReactions []*api.ReactionResponse
126+
DecodeJSON(t, resp, &apiReactions)
127+
expectResponse := make(map[int]api.ReactionResponse)
128+
expectResponse[0] = api.ReactionResponse{
129+
User: user2.APIFormat(),
130+
Reaction: "laugh",
131+
Created: time.Unix(1573248004, 0),
132+
}
133+
expectResponse[1] = api.ReactionResponse{
134+
User: user1.APIFormat(),
135+
Reaction: "laugh",
136+
Created: time.Unix(1573248005, 0),
137+
}
138+
expectResponse[2] = apiNewReaction
139+
assert.Len(t, apiReactions, 3)
140+
for i, r := range apiReactions {
141+
assert.Equal(t, expectResponse[i].Reaction, r.Reaction)
142+
assert.Equal(t, expectResponse[i].Created.Unix(), r.Created.Unix())
143+
assert.Equal(t, expectResponse[i].User.ID, r.User.ID)
144+
}
145+
}

models/error.go

+15
Original file line numberDiff line numberDiff line change
@@ -1121,6 +1121,21 @@ func (err ErrNewIssueInsert) Error() string {
11211121
return err.OriginalError.Error()
11221122
}
11231123

1124+
// ErrForbiddenIssueReaction is used when a forbidden reaction was try to created
1125+
type ErrForbiddenIssueReaction struct {
1126+
Reaction string
1127+
}
1128+
1129+
// IsErrForbiddenIssueReaction checks if an error is a ErrForbiddenIssueReaction.
1130+
func IsErrForbiddenIssueReaction(err error) bool {
1131+
_, ok := err.(ErrForbiddenIssueReaction)
1132+
return ok
1133+
}
1134+
1135+
func (err ErrForbiddenIssueReaction) Error() string {
1136+
return fmt.Sprintf("'%s' is not an allowed reaction", err.Reaction)
1137+
}
1138+
11241139
// __________ .__ .__ __________ __
11251140
// \______ \__ __| | | |\______ \ ____ ________ __ ____ _______/ |_
11261141
// | ___/ | \ | | | | _// __ \/ ____/ | \_/ __ \ / ___/\ __\

models/fixtures/reaction.yml

+39-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,39 @@
1-
[] # empty
1+
-
2+
id: 1 #issue reaction
3+
type: zzz # not allowed reaction (added before allowed reaction list has changed)
4+
issue_id: 1
5+
comment_id: 0
6+
user_id: 2
7+
created_unix: 1573248001
8+
9+
-
10+
id: 2 #issue reaction
11+
type: zzz # not allowed reaction (added before allowed reaction list has changed)
12+
issue_id: 1
13+
comment_id: 0
14+
user_id: 1
15+
created_unix: 1573248002
16+
17+
-
18+
id: 3 #issue reaction
19+
type: eyes # allowed reaction
20+
issue_id: 1
21+
comment_id: 0
22+
user_id: 2
23+
created_unix: 1573248003
24+
25+
-
26+
id: 4 #comment reaction
27+
type: laugh # allowed reaction
28+
issue_id: 1
29+
comment_id: 2
30+
user_id: 2
31+
created_unix: 1573248004
32+
33+
-
34+
id: 5 #comment reaction
35+
type: laugh # allowed reaction
36+
issue_id: 1
37+
comment_id: 2
38+
user_id: 1
39+
created_unix: 1573248005

models/issue_reaction.go

+39
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,38 @@ type FindReactionsOptions struct {
3333
}
3434

3535
func (opts *FindReactionsOptions) toConds() builder.Cond {
36+
//If Issue ID is set add to Query
3637
var cond = builder.NewCond()
3738
if opts.IssueID > 0 {
3839
cond = cond.And(builder.Eq{"reaction.issue_id": opts.IssueID})
3940
}
41+
//If CommentID is > 0 add to Query
42+
//If it is 0 Query ignore CommentID to select
43+
//If it is -1 it explicit search of Issue Reactions where CommentID = 0
4044
if opts.CommentID > 0 {
4145
cond = cond.And(builder.Eq{"reaction.comment_id": opts.CommentID})
46+
} else if opts.CommentID == -1 {
47+
cond = cond.And(builder.Eq{"reaction.comment_id": 0})
4248
}
49+
4350
return cond
4451
}
4552

53+
// FindCommentReactions returns a ReactionList of all reactions from an comment
54+
func FindCommentReactions(comment *Comment) (ReactionList, error) {
55+
return findReactions(x, FindReactionsOptions{
56+
IssueID: comment.IssueID,
57+
CommentID: comment.ID})
58+
}
59+
60+
// FindIssueReactions returns a ReactionList of all reactions from an issue
61+
func FindIssueReactions(issue *Issue) (ReactionList, error) {
62+
return findReactions(x, FindReactionsOptions{
63+
IssueID: issue.ID,
64+
CommentID: -1,
65+
})
66+
}
67+
4668
func findReactions(e Engine, opts FindReactionsOptions) ([]*Reaction, error) {
4769
reactions := make([]*Reaction, 0, 10)
4870
sess := e.Where(opts.toConds())
@@ -77,6 +99,10 @@ type ReactionOptions struct {
7799

78100
// CreateReaction creates reaction for issue or comment.
79101
func CreateReaction(opts *ReactionOptions) (reaction *Reaction, err error) {
102+
if !setting.UI.ReactionsMap[opts.Type] {
103+
return nil, ErrForbiddenIssueReaction{opts.Type}
104+
}
105+
80106
sess := x.NewSession()
81107
defer sess.Close()
82108
if err = sess.Begin(); err != nil {
@@ -160,6 +186,19 @@ func DeleteCommentReaction(doer *User, issue *Issue, comment *Comment, content s
160186
})
161187
}
162188

189+
// LoadUser load user of reaction
190+
func (r *Reaction) LoadUser() (*User, error) {
191+
if r.User != nil {
192+
return r.User, nil
193+
}
194+
user, err := getUserByID(x, r.UserID)
195+
if err != nil {
196+
return nil, err
197+
}
198+
r.User = user
199+
return user, nil
200+
}
201+
163202
// ReactionList represents list of reactions
164203
type ReactionList []*Reaction
165204

models/issue_reaction_test.go

+12-12
Original file line numberDiff line numberDiff line change
@@ -81,22 +81,22 @@ func TestIssueReactionCount(t *testing.T) {
8181
user4 := AssertExistsAndLoadBean(t, &User{ID: 4}).(*User)
8282
ghost := NewGhostUser()
8383

84-
issue1 := AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue)
84+
issue := AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue)
8585

86-
addReaction(t, user1, issue1, nil, "heart")
87-
addReaction(t, user2, issue1, nil, "heart")
88-
addReaction(t, user3, issue1, nil, "heart")
89-
addReaction(t, user3, issue1, nil, "+1")
90-
addReaction(t, user4, issue1, nil, "+1")
91-
addReaction(t, user4, issue1, nil, "heart")
92-
addReaction(t, ghost, issue1, nil, "-1")
93-
94-
err := issue1.loadReactions(x)
86+
addReaction(t, user1, issue, nil, "heart")
87+
addReaction(t, user2, issue, nil, "heart")
88+
addReaction(t, user3, issue, nil, "heart")
89+
addReaction(t, user3, issue, nil, "+1")
90+
addReaction(t, user4, issue, nil, "+1")
91+
addReaction(t, user4, issue, nil, "heart")
92+
addReaction(t, ghost, issue, nil, "-1")
93+
94+
err := issue.loadReactions(x)
9595
assert.NoError(t, err)
9696

97-
assert.Len(t, issue1.Reactions, 7)
97+
assert.Len(t, issue.Reactions, 7)
9898

99-
reactions := issue1.Reactions.GroupByType()
99+
reactions := issue.Reactions.GroupByType()
100100
assert.Len(t, reactions["heart"], 4)
101101
assert.Equal(t, 2, reactions["heart"].GetMoreUserCount())
102102
assert.Equal(t, user1.DisplayName()+", "+user2.DisplayName(), reactions["heart"].GetFirstUsers())

modules/setting/setting.go

+6
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ var (
171171
DefaultTheme string
172172
Themes []string
173173
Reactions []string
174+
ReactionsMap map[string]bool
174175
SearchRepoDescription bool
175176
UseServiceWorker bool
176177

@@ -985,6 +986,11 @@ func NewContext() {
985986
U2F.AppID = sec.Key("APP_ID").MustString(strings.TrimRight(AppURL, "/"))
986987

987988
zip.Verbose = false
989+
990+
UI.ReactionsMap = make(map[string]bool)
991+
for _, reaction := range UI.Reactions {
992+
UI.ReactionsMap[reaction] = true
993+
}
988994
}
989995

990996
func loadInternalToken(sec *ini.Section) string {

modules/structs/issue_reaction.go

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright 2019 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package structs
6+
7+
import (
8+
"time"
9+
)
10+
11+
// EditReactionOption contain the reaction type
12+
type EditReactionOption struct {
13+
Reaction string `json:"content"`
14+
}
15+
16+
// ReactionResponse contain one reaction
17+
type ReactionResponse struct {
18+
User *User `json:"user"`
19+
Reaction string `json:"content"`
20+
// swagger:strfmt date-time
21+
Created time.Time `json:"created_at"`
22+
}

0 commit comments

Comments
 (0)