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\Beatmapset;
12use App\Models\User;
13use Illuminate\Support\Facades\Event;
14use Tests\TestCase;
15
16class BeatmapDiscussionPostsControllerTest extends TestCase
17{
18 private Beatmap $beatmap;
19 private BeatmapDiscussion $beatmapDiscussion;
20 private BeatmapDiscussionPost $beatmapDiscussionPost;
21 private Beatmapset $beatmapset;
22 private User $mapper;
23 private User $user;
24
25 public function testPostStoreNewDiscussionInactiveBeatmapset()
26 {
27 $beatmapset = Beatmapset::factory()->owner()->inactive()->create();
28
29 $this
30 ->actingAsVerified($this->user)
31 ->post(route('beatmapsets.discussions.posts.store'), $this->makeBeatmapsetDiscussionPostParams($beatmapset, 'praise'))
32 ->assertStatus(404);
33 }
34
35 public function testPostUpdate()
36 {
37 $beatmapDiscussionPost = BeatmapDiscussionPost::factory()->create([
38 'beatmap_discussion_id' => $this->beatmapDiscussion->id,
39 'user_id' => $this->user->user_id,
40 ]);
41
42 $initialMessage = $beatmapDiscussionPost->message;
43 $editedMessage = "{$initialMessage} Edited";
44
45 $otherUser = User::factory()->create();
46
47 // invalid user
48 $this
49 ->putPost($editedMessage, $beatmapDiscussionPost, $otherUser)
50 ->assertStatus(403);
51
52 $beatmapDiscussionPost = $beatmapDiscussionPost->fresh();
53
54 $this->assertSame($initialMessage, $beatmapDiscussionPost->message);
55
56 // correct user
57 $this
58 ->putPost($editedMessage, $beatmapDiscussionPost, $this->user)
59 ->assertStatus(200);
60
61 $beatmapDiscussionPost = $beatmapDiscussionPost->fresh();
62
63 $this->assertSame($editedMessage, $beatmapDiscussionPost->message);
64 }
65
66 public function testPostUpdateNotLoggedIn()
67 {
68 $post = BeatmapDiscussionPost::factory()->create([
69 'beatmap_discussion_id' => $this->beatmapDiscussion,
70 'user_id' => $this->user,
71 ]);
72 $initialMessage = $post->message;
73
74 $this->putPost('', $post)
75 ->assertViewIs('users.login')
76 ->assertStatus(401);
77
78 $this->assertSame($initialMessage, $post->fresh()->message);
79 }
80
81 public function testPostUpdateWhenBeatmapsetDiscussionIsLocked()
82 {
83 $reply = $this->beatmapDiscussion->beatmapDiscussionPosts()->save(
84 BeatmapDiscussionPost::factory()->timeline()->make([
85 'user_id' => $this->user,
86 ])
87 );
88 $message = $reply->message;
89
90 $this->beatmapset->update(['discussion_locked' => true]);
91
92 $this->putPost("{$message} edited", $reply, $this->user)->assertStatus(403);
93 $this->assertSame($message, $reply->fresh()->message);
94 }
95
96 public function testPostUpdateWhenDiscussionResolved()
97 {
98 // reply made before resolve
99 $reply1 = $this->beatmapDiscussion->beatmapDiscussionPosts()->save(
100 BeatmapDiscussionPost::factory()->timeline()->make([
101 'user_id' => $this->user,
102 ])
103 );
104 $message1 = $reply1->message;
105
106 $this->postResolveDiscussion(true, $this->user);
107
108 // reply made after resolve
109 $reply2 = $this->beatmapDiscussion->beatmapDiscussionPosts()->save(
110 BeatmapDiscussionPost::factory()->timeline()->make([
111 'user_id' => $this->user,
112 ])
113 );
114 $message2 = $reply2->message;
115
116 $this->putPost("{$message1} edited", $reply1, $this->user)->assertStatus(403);
117 $this->putPost("{$message2} edited", $reply2, $this->user)->assertSuccessful();
118 $this->assertSame($message1, $reply1->fresh()->message);
119 $this->assertSame("{$message2} edited", $reply2->fresh()->message);
120 }
121
122 public function testPostUpdateWhenDiscussionReResolved()
123 {
124 // reply made before resolve
125 $reply1 = $this->beatmapDiscussion->beatmapDiscussionPosts()->save(
126 BeatmapDiscussionPost::factory()->timeline()->make([
127 'user_id' => $this->user,
128 ])
129 );
130 $message1 = $reply1->message;
131
132 $this->postResolveDiscussion(true, $this->user);
133 $this->postResolveDiscussion(false, $this->user);
134
135 // still should not be able to edit reply made before first resolve.
136 $this->putPost("{$message1} edited", $reply1, $this->user)->assertStatus(403);
137 $this->assertSame($message1, $reply1->fresh()->message);
138
139 // reply made after resolve
140 $reply2 = $this->beatmapDiscussion->beatmapDiscussionPosts()->save(
141 BeatmapDiscussionPost::factory()->timeline()->make([
142 'user_id' => $this->user,
143 ])
144 );
145 $message2 = $reply2->message;
146
147 $this->postResolveDiscussion(true, $this->user);
148
149 // should not be able to edit either reply.
150 $this->putPost("{$message1} edited", $reply1, $this->user)->assertStatus(403);
151 $this->putPost("{$message2} edited", $reply2, $this->user)->assertStatus(403);
152 $this->assertSame($message1, $reply1->fresh()->message);
153 $this->assertSame($message2, $reply2->fresh()->message);
154 }
155
156 public function testStartingPostUpdate()
157 {
158 $post = $this->beatmapDiscussionPost;
159
160 $previousTimestamp = $post->beatmapDiscussion->timestamp;
161
162 // removing timestamp isn't allowed
163 $this
164 ->putPost('Missing timestamp.', $post, $this->user)
165 ->assertStatus(422);
166
167 $post = $post->fresh();
168 $this->assertSame($previousTimestamp, $post->beatmapDiscussion->timestamp);
169
170 $newTimestamp = $post->beatmapDiscussion->beatmap->total_length * 1000;
171 $newTimestampString = beatmap_timestamp_format($newTimestamp);
172
173 // changing timestamp is allowed
174 $this
175 ->actingAs($this->user)
176 ->put(route('beatmapsets.discussions.posts.update', $post->id), [
177 'beatmap_discussion_post' => [
178 'message' => "{$newTimestampString} Edited timestamp.",
179 ],
180 ])
181 ->assertStatus(200);
182
183 $post = $post->fresh();
184 $this->assertSame($newTimestamp, $post->beatmapDiscussion->timestamp);
185 }
186
187 public function testPostDestroy()
188 {
189 $reply = $this->beatmapDiscussion->beatmapDiscussionPosts()->save(
190 BeatmapDiscussionPost::factory()->timeline()->make([
191 'user_id' => $this->user,
192 ])
193 );
194
195 $this->deletePost($reply, $this->user)->assertStatus(200);
196 $this->assertTrue($reply->fresh()->trashed());
197 }
198
199 public function testPostDestroyNotLoggedIn()
200 {
201 $reply = $this->beatmapDiscussion->beatmapDiscussionPosts()->save(
202 BeatmapDiscussionPost::factory()->timeline()->make([
203 'user_id' => $this->user,
204 ])
205 );
206
207 $this->deletePost($reply)
208 ->assertViewIs('users.login')
209 ->assertStatus(401);
210
211 $this->assertFalse($reply->fresh()->trashed());
212 }
213
214 public function testPostDestroyWhenBeatmapsetDiscussionIsLocked()
215 {
216 $reply = $this->beatmapDiscussion->beatmapDiscussionPosts()->save(
217 BeatmapDiscussionPost::factory()->timeline()->make([
218 'user_id' => $this->user,
219 ])
220 );
221
222 $this->beatmapset->update(['discussion_locked' => true]);
223
224 $this->deletePost($reply, $this->user)->assertStatus(403);
225 $this->assertFalse($reply->fresh()->trashed());
226 }
227
228 public function testPostDestroyWhenDiscussionResolved()
229 {
230 // reply made before resolve
231 $reply1 = $this->beatmapDiscussion->beatmapDiscussionPosts()->save(
232 BeatmapDiscussionPost::factory()->timeline()->make([
233 'user_id' => $this->user,
234 ])
235 );
236
237 $this->postResolveDiscussion(true, $this->user);
238
239 // reply made after resolve
240 $reply2 = $this->beatmapDiscussion->beatmapDiscussionPosts()->save(
241 BeatmapDiscussionPost::factory()->timeline()->make([
242 'user_id' => $this->user,
243 ])
244 );
245
246 $this->deletePost($reply1, $this->user)->assertStatus(403);
247 $this->deletePost($reply2, $this->user)->assertSuccessful();
248 $this->assertFalse($reply1->fresh()->trashed());
249 $this->assertTrue($reply2->fresh()->trashed());
250 }
251
252 public function testPostDestroyWhenDiscussionReResolved()
253 {
254 // reply made before resolve
255 $reply1 = $this->beatmapDiscussion->beatmapDiscussionPosts()->save(
256 BeatmapDiscussionPost::factory()->timeline()->make([
257 'user_id' => $this->user,
258 ])
259 );
260
261 $this->postResolveDiscussion(true, $this->user);
262 $this->postResolveDiscussion(false, $this->user);
263
264 // still should not be able to delete reply made before first resolve.
265 $this->deletePost($reply1, $this->user)->assertStatus(403);
266 $this->assertFalse($reply1->fresh()->trashed());
267
268 // reply made after resolve
269 $reply2 = $this->beatmapDiscussion->beatmapDiscussionPosts()->save(
270 BeatmapDiscussionPost::factory()->timeline()->make([
271 'user_id' => $this->user,
272 ])
273 );
274
275 $this->postResolveDiscussion(true, $this->user);
276
277 // should not be able to delete either reply.
278 $this->deletePost($reply1, $this->user)->assertStatus(403);
279 $this->deletePost($reply2, $this->user)->assertStatus(403);
280 $this->assertFalse($reply1->fresh()->trashed());
281 $this->assertFalse($reply2->fresh()->trashed());
282 }
283
284 public function testPostWithoutResolveFlag()
285 {
286 $this->beatmapDiscussion->update([
287 'resolved' => false,
288 ]);
289
290 $otherUser = User::factory()->withPlays()->create();
291
292 foreach ([$this->user, $otherUser] as $user) {
293 $lastDiscussionPosts = BeatmapDiscussionPost::count();
294
295 $this
296 ->postDiscussionWithoutResolveFlag($user)
297 ->assertStatus(200);
298
299 // No resolve change, therefore no system posts
300 $this->assertSame($lastDiscussionPosts + 1, BeatmapDiscussionPost::count());
301 // Should stay unresolved.
302 $this->assertSame(false, $this->beatmapDiscussion->fresh()->resolved);
303 }
304
305 $this
306 ->postResolveDiscussion(true, $this->user)
307 ->assertStatus(200);
308
309 foreach ([$this->user, $otherUser] as $user) {
310 $lastDiscussionPosts = BeatmapDiscussionPost::count();
311
312 $this
313 ->postDiscussionWithoutResolveFlag($user)
314 ->assertStatus(200);
315
316 $this->assertSame($lastDiscussionPosts + 1, BeatmapDiscussionPost::count());
317 // Should stay resolved now.
318 $this->assertSame(true, $this->beatmapDiscussion->fresh()->resolved);
319 }
320 }
321
322 protected function setUp(): void
323 {
324 parent::setUp();
325
326 Event::fake();
327
328 $this->mapper = User::factory()->withPlays()->create();
329 $this->user = User::factory()->withPlays()->create();
330
331 $this->beatmapset = Beatmapset::factory()->owner($this->mapper)->create();
332 $this->beatmap = $this->beatmapset->beatmaps()->save(Beatmap::factory()->make([
333 'user_id' => $this->mapper->getKey(),
334 ]));
335 $this->beatmapDiscussion = BeatmapDiscussion::factory()->timeline()->create([
336 'beatmapset_id' => $this->beatmapset,
337 'beatmap_id' => $this->beatmap,
338 'user_id' => $this->user,
339 ]);
340 $post = BeatmapDiscussionPost::factory()->timeline()->make([
341 'user_id' => $this->user,
342 ]);
343 $this->beatmapDiscussionPost = $this->beatmapDiscussion->beatmapDiscussionPosts()->save($post);
344 }
345
346 private function deletePost(BeatmapDiscussionPost $post, ?User $user = null)
347 {
348 return ($user === null ? $this : $this->actingAsVerified($user))
349 ->delete(route('beatmapsets.discussions.posts.destroy', $post->id));
350 }
351
352 private function postResolveDiscussion(bool $resolved, User $user)
353 {
354 return $this
355 ->actingAsVerified($user)
356 ->post(route('beatmapsets.discussions.posts.store'), [
357 'beatmap_discussion_id' => $this->beatmapDiscussion->id,
358 'beatmap_discussion' => [
359 'resolved' => $resolved,
360 ],
361 'beatmap_discussion_post' => [
362 'message' => 'Hello',
363 ],
364 ]);
365 }
366
367 private function postDiscussionWithoutResolveFlag(User $user)
368 {
369 return $this
370 ->actingAsVerified($user)
371 ->post(route('beatmapsets.discussions.posts.store'), [
372 'beatmap_discussion_id' => $this->beatmapDiscussion->id,
373 'beatmap_discussion' => [],
374 'beatmap_discussion_post' => [
375 'message' => 'Hello',
376 ],
377 ]);
378 }
379
380 private function putPost(string $message, BeatmapDiscussionPost $post, ?User $user = null)
381 {
382 return ($user === null ? $this : $this->actingAsVerified($user))
383 ->put(route('beatmapsets.discussions.posts.update', $post->id), [
384 'beatmap_discussion_post' => [
385 'message' => $message,
386 ],
387 ]);
388 }
389}