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\Beatmapset;
9use App\Models\Comment;
10use App\Models\Follow;
11use App\Models\Notification;
12use App\Models\User;
13use Tests\TestCase;
14
15class CommentsControllerTest extends TestCase
16{
17 private $user;
18 private $minPlays;
19 private $beatmapset;
20 private $params;
21
22 /**
23 * @dataProvider pinPermissionsDataProvider
24 */
25 public function testPin(?string $groupIdentifier, bool $onBeatmapset, bool $asBeatmapsetOwner, bool $asCommentOwner, bool $withPinned, bool $expectAllowed): void
26 {
27 $user = User::factory()->withGroup($groupIdentifier)->create();
28 $comment = Comment::factory()->create([
29 'commentable_type' => $onBeatmapset ? 'beatmapset' : 'build',
30 'user_id' => $asCommentOwner ? $user->getKey() : User::factory(),
31 ]);
32
33 if ($asBeatmapsetOwner) {
34 $comment->commentable->update(['user_id' => $user->getKey()]);
35 }
36
37 if ($withPinned) {
38 $comment->commentable->comments()->save(Comment::factory()->make(['pinned' => true]));
39 }
40
41 $this
42 ->actingAsVerified($user)
43 ->post(route('comments.pin', $comment->getKey()))
44 ->assertStatus($expectAllowed ? 200 : 403);
45
46 $this->assertSame($comment->fresh()->pinned, $expectAllowed);
47 }
48
49 public function testPinReply(): void
50 {
51 $comment = Comment::factory()->reply()->create();
52 $user = User::factory()->withGroup('admin')->create();
53
54 $this
55 ->actingAsVerified($user)
56 ->post(route('comments.pin', $comment->getKey()))
57 ->assertStatus(422);
58
59 $this->assertFalse($comment->fresh()->pinned);
60 }
61
62 public function testStore()
63 {
64 $this->prepareForStore();
65 $otherUser = User::factory()->create();
66
67 $follow = Follow::create([
68 'notifiable' => $this->beatmapset,
69 'user' => $otherUser,
70 'subtype' => 'comment',
71 ]);
72
73 $previousComments = Comment::count();
74 $previousNotifications = Notification::count();
75
76 $this
77 ->be($this->user)
78 ->post(route('comments.store'), $this->params)
79 ->assertSuccessful();
80
81 $this->assertSame($previousComments + 1, Comment::count());
82 $this->assertSame($previousNotifications + 1, Notification::count());
83 }
84
85 public function testStoreDownloadLimitedBeatmapset()
86 {
87 $this->prepareForStore();
88 $this->beatmapset->update(['download_disabled_url' => 'https://hello.world']);
89
90 $this->expectCountChange(fn () => Comment::count(), 0);
91
92 $this
93 ->be($this->user)
94 ->post(route('comments.store'), $this->params)
95 ->assertStatus(403);
96 }
97
98 public function testStoreNotEnoughPlays()
99 {
100 $this->prepareForStore();
101 $this->user->statisticsOsu()->update(['playcount' => $this->minPlays - 1]);
102 $previousComments = Comment::count();
103
104 $this
105 ->be($this->user)
106 ->post(route('comments.store'), $this->params)
107 ->assertStatus(401)
108 ->assertViewIs('users.verify');
109
110 $this->assertSame($previousComments, Comment::count());
111 }
112
113 public function testStoreNotEnoughPlaysVerified()
114 {
115 $this->prepareForStore();
116 $this->user->statisticsOsu()->update(['playcount' => $this->minPlays - 1]);
117 $previousComments = Comment::count();
118
119 $this
120 ->actingAsVerified($this->user)
121 ->post(route('comments.store'), $this->params)
122 ->assertStatus(200);
123
124 $this->assertSame($previousComments + 1, Comment::count());
125 }
126
127 public function testStoreGuest()
128 {
129 $this->prepareForStore();
130 $previousComments = Comment::count();
131
132 $this
133 ->post(route('comments.store'), $this->params)
134 ->assertStatus(401);
135
136 $this->assertSame($previousComments, Comment::count());
137 }
138
139 public function testStoreReply()
140 {
141 $this->prepareForStore();
142 $parent = $this->beatmapset->comments()->create([
143 'user_id' => $this->user->getKey(),
144 'message' => 'Hello.',
145 ]);
146
147 $params = ['comment' => [
148 'parent_id' => $parent->getKey(),
149 'message' => 'This is a reply.',
150 ]];
151
152 $previousComments = $this->beatmapset->comments()->count();
153
154 $this
155 ->actingAsVerified($this->user)
156 ->post(route('comments.store'), $params)
157 ->assertStatus(200);
158
159 $this->assertSame($previousComments + 1, $this->beatmapset->comments()->count());
160 }
161
162 public function testStoreReplyDownloadLimitedBeatmapset()
163 {
164 $this->prepareForStore();
165 $parent = $this->beatmapset->comments()->create([
166 'user_id' => $this->user->getKey(),
167 'message' => 'Hello.',
168 ]);
169 $this->beatmapset->update(['download_disabled_url' => 'https://hello.world']);
170
171 $params = ['comment' => [
172 'parent_id' => $parent->getKey(),
173 'message' => 'This is a reply.',
174 ]];
175
176 $this->expectCountChange(fn () => Comment::count(), 0);
177
178 $this
179 ->actingAsVerified($this->user)
180 ->post(route('comments.store'), $params)
181 ->assertStatus(403);
182 }
183
184 public function testUpdate()
185 {
186 $this->prepareForStore();
187 $comment = $this->beatmapset->comments()->create([
188 'user_id' => $this->user->getKey(),
189 'message' => 'Hello.',
190 ]);
191
192 $newMessage = 'Edited.';
193 $params = ['comment' => [
194 'message' => $newMessage,
195 ]];
196
197 $this
198 ->actingAsVerified($this->user)
199 ->put(route('comments.update', $comment), $params)
200 ->assertSuccessful();
201
202 $this->assertSame($newMessage, $comment->fresh()->message);
203 }
204
205 public function testUpdateDownloadLimitedBeatmapset()
206 {
207 $this->prepareForStore();
208 $oldMessage = 'Hello.';
209 $comment = $this->beatmapset->comments()->create([
210 'user_id' => $this->user->getKey(),
211 'message' => $oldMessage,
212 ]);
213 $this->beatmapset->update(['download_disabled_url' => 'https://hello.world']);
214
215 $params = ['comment' => [
216 'message' => 'Edited.',
217 ]];
218
219 $this
220 ->actingAsVerified($this->user)
221 ->put(route('comments.update', $comment), $params)
222 ->assertStatus(403);
223
224 $this->assertSame($oldMessage, $comment->fresh()->message);
225 }
226
227 public function testApiUnauthenticatedUserCanViewIndex()
228 {
229 $this
230 ->json('GET', route('api.comments.index'))
231 ->assertSuccessful();
232 }
233
234 public function testApiUnauthenticatedUserCanViewComment()
235 {
236 $comment = Comment::factory()->create();
237
238 $this
239 ->json('GET', route('api.comments.show', ['comment' => $comment->getKey()]))
240 ->assertSuccessful();
241 }
242
243 /**
244 * @dataProvider apiRequiresAuthenticationDataProvider
245 */
246 public function testApiRequiresAuthentication($method, $routeName)
247 {
248 $this
249 ->json($method, route("api.{$routeName}", ['comment' => 1]))
250 ->assertUnauthorized();
251 }
252
253 public static function apiRequiresAuthenticationDataProvider()
254 {
255 return [
256 ['DELETE', 'comments.vote'],
257 ['POST', 'comments.vote'],
258 ['POST', 'comments.store'],
259 ['PUT', 'comments.update'],
260 ['DELETE', 'comments.destroy'],
261 ];
262 }
263
264 /**
265 * Data in order:
266 * - User's group identifier
267 * - Whether the commentable is a beatmapset
268 * - Whether the user is the beatmapset's creator
269 * - Whether the user is the comment's creator
270 * - Whether the commentable already has a pinned comment
271 * - Whether pinning should be allowed
272 */
273 public static function pinPermissionsDataProvider(): array
274 {
275 return [
276 ['admin', true, true, true, true, true],
277 ['admin', true, true, true, false, true],
278 ['admin', true, true, false, true, true],
279 ['admin', true, true, false, false, true],
280 ['admin', true, false, true, true, true],
281 ['admin', true, false, true, false, true],
282 ['admin', true, false, false, true, true],
283 ['admin', true, false, false, false, true],
284 ['admin', false, false, true, true, true],
285 ['admin', false, false, true, false, true],
286 ['admin', false, false, false, true, true],
287 ['admin', false, false, false, false, true],
288 ['gmt', true, true, true, true, false],
289 ['gmt', true, true, true, false, true],
290 ['gmt', true, true, false, true, false],
291 ['gmt', true, true, false, false, true],
292 ['gmt', true, false, true, true, false],
293 ['gmt', true, false, true, false, true],
294 ['gmt', true, false, false, true, false],
295 ['gmt', true, false, false, false, true],
296 ['gmt', false, false, true, true, false],
297 ['gmt', false, false, true, false, false],
298 ['gmt', false, false, false, true, false],
299 ['gmt', false, false, false, false, false],
300 [null, true, true, true, true, false],
301 [null, true, true, true, false, true],
302 [null, true, true, false, true, false],
303 [null, true, true, false, false, false],
304 [null, true, false, true, true, false],
305 [null, true, false, true, false, false],
306 [null, true, false, false, true, false],
307 [null, true, false, false, false, false],
308 [null, false, false, true, true, false],
309 [null, false, false, true, false, false],
310 [null, false, false, false, true, false],
311 [null, false, false, false, false, false],
312 ];
313 }
314
315 private function prepareForStore()
316 {
317 config_set('osu.user.post_action_verification', false);
318 $this->minPlays = $GLOBALS['cfg']['osu']['user']['min_plays_for_posting'];
319
320 $this->user = User::factory()->withPlays()->create();
321
322 $this->beatmapset = Beatmapset::factory()->create();
323
324 $this->params = ['comment' => [
325 'commentable_type' => 'beatmapset',
326 'commentable_id' => $this->beatmapset->getKey(),
327 'message' => 'Hello.',
328 ]];
329 }
330}