1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
|
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package polls_test
import (
"context"
"math/rand"
"net/http"
"testing"
"github.com/stretchr/testify/suite"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/processing/common"
"github.com/superseriousbusiness/gotosocial/internal/processing/polls"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
"github.com/superseriousbusiness/gotosocial/internal/visibility"
"github.com/superseriousbusiness/gotosocial/testrig"
)
type PollTestSuite struct {
suite.Suite
state state.State
filter *visibility.Filter
polls polls.Processor
testAccounts map[string]*gtsmodel.Account
testPolls map[string]*gtsmodel.Poll
}
func (suite *PollTestSuite) SetupTest() {
testrig.InitTestConfig()
testrig.InitTestLog()
suite.state.Caches.Init()
testrig.StartNoopWorkers(&suite.state)
testrig.NewTestDB(&suite.state)
converter := typeutils.NewConverter(&suite.state)
controller := testrig.NewTestTransportController(&suite.state, nil)
mediaMgr := media.NewManager(&suite.state)
federator := testrig.NewTestFederator(&suite.state, controller, mediaMgr)
suite.filter = visibility.NewFilter(&suite.state)
common := common.New(&suite.state, converter, federator, suite.filter)
suite.polls = polls.New(&common, &suite.state, converter)
}
func (suite *PollTestSuite) TearDownTest() {
testrig.StopWorkers(&suite.state)
testrig.StandardDBTeardown(suite.state.DB)
}
func (suite *PollTestSuite) TestPollGet() {
// Create a new context for this test.
ctx, cncl := context.WithCancel(context.Background())
defer cncl()
// Perform test for all requester + poll combos.
for _, account := range suite.testAccounts {
for _, poll := range suite.testPolls {
suite.testPollGet(ctx, account, poll)
}
}
}
func (suite *PollTestSuite) testPollGet(ctx context.Context, requester *gtsmodel.Account, poll *gtsmodel.Poll) {
// Ensure poll model is fully populated before anything.
if err := suite.state.DB.PopulatePoll(ctx, poll); err != nil {
suite.T().Fatalf("error populating poll: %v", err)
}
var check func(*apimodel.Poll, gtserror.WithCode) bool
switch {
case !pollIsVisible(suite.filter, ctx, requester, poll):
// Poll should not be visible to requester, this should
// return an error code 404 (to prevent info leak).
check = func(poll *apimodel.Poll, err gtserror.WithCode) bool {
return poll == nil && err.Code() == http.StatusNotFound
}
default:
// All other cases should succeed! i.e. no error and poll returned.
check = func(poll *apimodel.Poll, err gtserror.WithCode) bool {
return poll != nil && err == nil
}
}
// Perform the poll vote and check the expected response.
if !check(suite.polls.PollGet(ctx, requester, poll.ID)) {
suite.T().Errorf("unexpected response for poll get by %s", requester.DisplayName)
}
}
func (suite *PollTestSuite) TestPollVote() {
// Create a new context for this test.
ctx, cncl := context.WithCancel(context.Background())
defer cncl()
// randomChoices generates random vote choices in poll.
randomChoices := func(poll *gtsmodel.Poll) []int {
var max int
if *poll.Multiple {
max = len(poll.Options)
} else {
max = 1
}
count := 1 + rand.Intn(max)
choices := make([]int, count)
for i := range choices {
choices[i] = rand.Intn(len(poll.Options))
}
return choices
}
// Perform test for all requester + poll combos.
for _, account := range suite.testAccounts {
for _, poll := range suite.testPolls {
// Generate some valid choices and test.
choices := randomChoices(poll)
suite.testPollVote(ctx,
account,
poll,
choices,
)
// Test with empty choices.
suite.testPollVote(ctx,
account,
poll,
nil,
)
// Test with out of range choice.
suite.testPollVote(ctx,
account,
poll,
[]int{len(poll.Options)},
)
}
}
}
func (suite *PollTestSuite) testPollVote(ctx context.Context, requester *gtsmodel.Account, poll *gtsmodel.Poll, choices []int) {
// Ensure poll model is fully populated before anything.
if err := suite.state.DB.PopulatePoll(ctx, poll); err != nil {
suite.T().Fatalf("error populating poll: %v", err)
}
var check func(*apimodel.Poll, gtserror.WithCode) bool
switch {
case !poll.ClosedAt.IsZero():
// Poll is already closed, i.e. no new votes allowed!
// This should return an error 422 (unprocessable entity).
check = func(poll *apimodel.Poll, err gtserror.WithCode) bool {
return poll == nil && err.Code() == http.StatusUnprocessableEntity
}
case !voteChoicesAreValid(poll, choices):
// These are invalid vote choices, this should return
// an error code 400 to indicate invalid request data.
check = func(poll *apimodel.Poll, err gtserror.WithCode) bool {
return poll == nil && err.Code() == http.StatusBadRequest
}
case poll.Status.AccountID == requester.ID:
// Immediately we know that poll owner cannot vote in
// their own poll. this should return an error 422.
check = func(poll *apimodel.Poll, err gtserror.WithCode) bool {
return poll == nil && err.Code() == http.StatusUnprocessableEntity
}
case !pollIsVisible(suite.filter, ctx, requester, poll):
// Poll should not be visible to requester, this should
// return an error code 404 (to prevent info leak).
check = func(poll *apimodel.Poll, err gtserror.WithCode) bool {
return poll == nil && err.Code() == http.StatusNotFound
}
default:
// All other cases should succeed! i.e. no error and poll returned.
check = func(poll *apimodel.Poll, err gtserror.WithCode) bool {
return poll != nil && err == nil
}
}
// Perform the poll vote and check the expected response.
if !check(suite.polls.PollVote(ctx, requester, poll.ID, choices)) {
suite.T().Errorf("unexpected response for poll vote by %s with %v", requester.DisplayName, choices)
}
}
// voteChoicesAreValid is a utility function to check whether choices are valid for poll.
func voteChoicesAreValid(poll *gtsmodel.Poll, choices []int) bool {
if len(choices) == 0 || !*poll.Multiple && len(choices) > 1 {
// Invalid number of vote choices.
return false
}
for _, choice := range choices {
if choice < 0 || choice >= len(poll.Options) {
// Choice index out of range.
return false
}
}
return true
}
// pollIsVisible is a short-hand function to return only a single boolean value for a visibility check on poll source status to account.
func pollIsVisible(filter *visibility.Filter, ctx context.Context, to *gtsmodel.Account, poll *gtsmodel.Poll) bool {
visible, _ := filter.StatusVisible(ctx, to, poll.Status)
return visible
}
func TestPollTestSuite(t *testing.T) {
suite.Run(t, new(PollTestSuite))
}
|