1<?php
2
3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
4// See the LICENCE file in the repository root for full licence text.
5
6namespace Tests\Controllers;
7
8use App\Models\Beatmap;
9use App\Models\BeatmapDiscussion;
10use App\Models\BeatmapDiscussionPost;
11use App\Models\BeatmapDiscussionVote;
12use App\Models\Beatmapset;
13use App\Models\User;
14use Faker;
15use Tests\TestCase;
16
17class BeatmapDiscussionsControllerTest extends TestCase
18{
19 protected static $faker;
20
21 protected BeatmapDiscussion $discussion;
22
23 public static function setUpBeforeClass(): void
24 {
25 self::$faker = Faker\Factory::create();
26 }
27
28 /**
29 * @dataProvider putVoteDataProvider
30 */
31 public function testPutVote(string $beatmapState, int $status, int $change)
32 {
33 $this->discussion->beatmapset->update(['approved' => Beatmapset::STATES[$beatmapState]]);
34
35 $user = User::factory()->create();
36
37 $currentVotes = BeatmapDiscussionVote::count();
38 $currentScore = $this->currentScore();
39
40 $this->putVote($user, '1')
41 ->assertStatus($status);
42
43 $this->assertSame($currentVotes + $change, BeatmapDiscussionVote::count());
44 $this->assertSame($currentScore + $change, $this->currentScore());
45 }
46
47 /**
48 * @dataProvider putVoteAgainDataProvider
49 */
50 public function testPutVoteAgain(string $score, int $change)
51 {
52 $user = User::factory()->create();
53
54 $this->discussion->vote([
55 'score' => 1,
56 'user_id' => $user->getKey(),
57 ]);
58
59 $currentVotes = BeatmapDiscussionVote::count();
60 $currentScore = $this->currentScore();
61
62 $this->putVote($user, $score)
63 ->assertStatus(200);
64
65 $this->assertSame($currentVotes + $change, BeatmapDiscussionVote::count());
66 $this->assertSame($currentScore + $change, $this->currentScore());
67 }
68
69 // can not vote as discussion starter
70 public function testPutVoteSelf()
71 {
72 $user = $this->discussion->user;
73 $currentVotes = BeatmapDiscussionVote::count();
74 $currentScore = $this->currentScore();
75
76 $this->putVote($user, '1')
77 ->assertStatus(403);
78
79 $this->assertSame($currentVotes, BeatmapDiscussionVote::count());
80 $this->assertSame($currentScore, $this->currentScore());
81 }
82
83 /**
84 * @dataProvider putVoteChangeToDownDataProvider
85 */
86 public function testPutVoteChangeToDown(?string $group, int $status, int $scoreChange)
87 {
88 $user = User::factory()->withGroup($group)->create();
89
90 $this->discussion->vote([
91 'score' => 1,
92 'user_id' => $user->getKey(),
93 ]);
94
95 $currentVotes = BeatmapDiscussionVote::count();
96 $currentScore = $this->currentScore();
97
98 $this
99 ->putVote($user, '-1')
100 ->assertStatus($status);
101
102 $this->assertSame($currentVotes, BeatmapDiscussionVote::count());
103 $this->assertSame($currentScore + $scoreChange, $this->currentScore());
104 }
105
106 /**
107 * @dataProvider putVoteDownDataProvider
108 */
109 public function testPutVoteDown(?string $group, int $status, int $voteChange, int $scoreChange)
110 {
111 $user = User::factory()->withGroup($group)->create();
112
113 $currentVotes = BeatmapDiscussionVote::count();
114 $currentScore = $this->currentScore();
115
116 $this
117 ->putVote($user, '-1')
118 ->assertStatus($status);
119
120 $this->assertSame($currentVotes + $voteChange, BeatmapDiscussionVote::count());
121 $this->assertSame($currentScore + $scoreChange, $this->currentScore());
122 }
123
124 // posting reviews - fail scenarios ----
125
126 // guest user
127 public function testPostReviewGuest()
128 {
129 $this
130 ->post(route('beatmapsets.discussion.review', $this->discussion->beatmapset_id))
131 ->assertUnauthorized();
132 }
133
134 // invalid document
135 public function testPostReviewDocumentMissing()
136 {
137 $this
138 ->actingAsVerified($this->discussion->user)
139 ->post(route('beatmapsets.discussion.review', $this->discussion->beatmapset_id))
140 ->assertStatus(422);
141 }
142
143 // posting reviews - success scenario ----
144
145 // valid document containing issue embeds
146 public function testPostReviewDocumentValidWithIssues()
147 {
148 $user = $this->discussion->user;
149 $discussionCount = BeatmapDiscussion::count();
150 $discussionPostCount = BeatmapDiscussionPost::count();
151 $timestampedIssueText = '00:01:234 '.self::$faker->sentence();
152 $issueText = self::$faker->sentence();
153
154 $document = json_encode(
155 [
156 [
157 'type' => 'embed',
158 'discussion_type' => 'problem',
159 'text' => $timestampedIssueText,
160 'timestamp' => true,
161 'beatmap_id' => $this->discussion->beatmap_id,
162 ],
163 [
164 'type' => 'embed',
165 'discussion_type' => 'problem',
166 'text' => $issueText,
167 ],
168 ]
169 );
170
171 $this
172 ->actingAsVerified($user)
173 ->post(route('beatmapsets.discussion.review', $this->discussion->beatmapset_id), [
174 'document' => $document,
175 ])
176 ->assertSuccessful()
177 ->assertJsonFragment(
178 [
179 'user_id' => $user->getKey(),
180 'message' => $timestampedIssueText,
181 ]
182 )
183 // ensure timestamp was parsed correctly
184 ->assertJsonFragment(
185 [
186 'timestamp' => 1234,
187 ]
188 )
189 ->assertJsonFragment(
190 [
191 'user_id' => $user->getKey(),
192 'message' => $issueText,
193 ]
194 );
195
196 // ensure 3 discussions/posts are created - one for the review and one for each embedded problem
197 $this->assertSame($discussionCount + 3, BeatmapDiscussion::count());
198 $this->assertSame($discussionPostCount + 3, BeatmapDiscussionPost::count());
199 }
200
201 public static function putVoteDataProvider()
202 {
203 return [
204 ['graveyard', 403, 0],
205 ['wip', 200, 1],
206 ['pending', 200, 1],
207 ['ranked', 403, 0],
208 ['approved', 403, 0],
209 // TODO: qualified; factory the beatmapset with the correct state instead of using update.
210 ['loved', 403, 0],
211 ];
212 }
213
214 public static function putVoteAgainDataProvider()
215 {
216 return [
217 'voting again has no effect' => ['1', 0],
218 'voting 0 will remove the vote' => ['0', -1],
219 ];
220 }
221
222 public static function putVoteChangeToDownDataProvider()
223 {
224 return [
225 'bng can change to down vote' => ['bng', 200, -2],
226 'regular user can change to down vote' => [null, 200, -2],
227 ];
228 }
229
230 public static function putVoteDownDataProvider()
231 {
232 return [
233 'bng can down vote' => ['bng', 200, 1, -1],
234 'regular user can down vote' => [null, 200, 1, -1],
235 ];
236 }
237
238 protected function setUp(): void
239 {
240 parent::setUp();
241
242 $mapper = User::factory()->create();
243 $user = User::factory()->create();
244 $beatmapset = Beatmapset::factory()->pending()->owner($mapper)->create();
245 // TODO: adding beatmap to beatmapset should probably copy come attributes.
246 $beatmap = $beatmapset->beatmaps()->save(Beatmap::factory()->make(['user_id' => $mapper]));
247 $this->discussion = BeatmapDiscussion::factory()->timeline()->create([
248 'beatmapset_id' => $beatmapset,
249 'beatmap_id' => $beatmap,
250 'user_id' => $user,
251 ]);
252 }
253
254 private function currentScore()
255 {
256 return (int) $this->discussion->fresh()->beatmapDiscussionVotes()->sum('score');
257 }
258
259 private function putVote(?User $user, string $score)
260 {
261 return $this
262 ->actingAsVerified($user)
263 ->put(route('beatmapsets.discussions.vote', $this->discussion), [
264 'beatmap_discussion_vote' => ['score' => $score],
265 ]);
266 }
267}