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
6declare(strict_types=1);
7
8namespace Tests\Libraries\BeatmapsetDiscussion;
9
10use App\Events\NewPrivateNotificationEvent;
11use App\Exceptions\AuthorizationException;
12use App\Exceptions\InvariantException;
13use App\Jobs\Notifications\BeatmapsetDiscussionPostNew;
14use App\Jobs\Notifications\BeatmapsetDisqualify;
15use App\Jobs\Notifications\BeatmapsetResetNominations;
16use App\Libraries\BeatmapsetDiscussion\Reply;
17use App\Models\Beatmap;
18use App\Models\BeatmapDiscussion;
19use App\Models\BeatmapDiscussionPost;
20use App\Models\Beatmapset;
21use App\Models\User;
22use Event;
23use Queue;
24use Tests\TestCase;
25
26class ReplyTest extends TestCase
27{
28 private const TEST_MESSAGE = 'not important';
29
30 private User $mapper;
31
32 public function testWatchersGetNotification()
33 {
34 $user = User::factory()->create()->markSessionVerified();
35 $watcher = User::factory()->create();
36 $discussion = BeatmapDiscussion::factory()
37 ->general()
38 ->for($this->beatmapsetFactory())
39 ->create(['user_id' => $this->mapper]);
40
41 $discussion->beatmapset->watches()->create(['user_id' => $watcher->getKey()]);
42
43 (new Reply($user, $discussion, static::TEST_MESSAGE))->handle();
44
45 Queue::assertPushed(
46 BeatmapsetDiscussionPostNew::class,
47 fn (BeatmapsetDiscussionPostNew $job) => (
48 $this->inReceivers($watcher, $job)
49 && !$this->inReceivers($user, $job)
50 )
51 );
52
53 $this->runFakeQueue();
54
55 // TODO: this should probably be changed to asserting "if job queued, then event is broadcast to receivers with option set"
56 Event::assertDispatched(
57 NewPrivateNotificationEvent::class,
58 fn (NewPrivateNotificationEvent $event) => (
59 $this->inReceivers($watcher, $event)
60 && !$this->inReceivers($user, $event)
61 )
62 );
63 }
64
65 /**
66 * @dataProvider replyQueuesNotificationDataProviderToStarter
67 */
68 public function testReplyQueuesNotificationToStarter(string $messageType, bool $includeStarter)
69 {
70 $user = User::factory()->create()->markSessionVerified();
71 $starter = User::factory()->create();
72
73 $discussion = BeatmapDiscussion::factory()->general()->create([
74 'beatmapset_id' => $this->beatmapsetFactory(),
75 'message_type' => $messageType,
76 'user_id' => $starter,
77 ]);
78
79 (new Reply($user, $discussion, static::TEST_MESSAGE))->handle();
80
81 Queue::assertPushed(
82 BeatmapsetDiscussionPostNew::class,
83 fn (BeatmapsetDiscussionPostNew $job) => (
84 $includeStarter
85 ? $this->inReceivers($starter, $job)
86 : !$this->inReceivers($starter, $job)
87 )
88 );
89 }
90
91 /**
92 * @dataProvider userGroupsDataProvider
93 */
94 public function testReplyResolvedDiscussion(?string $group)
95 {
96 $user = User::factory()->withGroup($group)->create()->markSessionVerified();
97 $discussion = BeatmapDiscussion::factory()->general()->problem()->create([
98 'beatmapset_id' => $this->beatmapsetFactory(),
99 'resolved' => true,
100 'user_id' => User::factory(),
101 ]);
102
103 $this->expectCountChange(fn () => BeatmapDiscussionPost::count(), 1);
104
105 (new Reply($user, $discussion, static::TEST_MESSAGE))->handle();
106
107 Queue::assertPushed(BeatmapsetDiscussionPostNew::class);
108
109 $this->assertTrue($discussion->fresh()->resolved);
110 }
111
112 /**
113 * @dataProvider userGroupsDataProvider
114 */
115 public function testReplyUnresolvedDiscussion(?string $group)
116 {
117 $user = User::factory()->withGroup($group)->create()->markSessionVerified();
118 $discussion = BeatmapDiscussion::factory()->general()->problem()->create([
119 'beatmapset_id' => $this->beatmapsetFactory(),
120 'resolved' => false,
121 'user_id' => User::factory(),
122 ]);
123
124 $this->expectCountChange(fn () => BeatmapDiscussionPost::count(), 1);
125
126 (new Reply($user, $discussion, static::TEST_MESSAGE))->handle();
127
128 Queue::assertPushed(BeatmapsetDiscussionPostNew::class);
129
130 $this->assertFalse($discussion->fresh()->resolved);
131 }
132
133 /**
134 * @dataProvider resolveDiscussionByStarterDataProvider
135 */
136 public function testResolveDiscussionByStarter(string $messageType, bool $expected)
137 {
138 $user = User::factory()->create()->markSessionVerified();
139 $discussion = BeatmapDiscussion::factory()->general()->create([
140 'beatmapset_id' => $this->beatmapsetFactory(),
141 'message_type' => $messageType,
142 'user_id' => $user,
143 ]);
144
145 $this->expectCountChange(fn () => BeatmapDiscussionPost::count(), $expected ? 2 : 0);
146 if (!$expected) {
147 $this->expectException(InvariantException::class);
148 }
149
150 $posts = (new Reply($user, $discussion, static::TEST_MESSAGE, true))->handle();
151
152 Queue::assertPushed(BeatmapsetDiscussionPostNew::class);
153
154 $this->assertSame($expected, $discussion->fresh()->resolved);
155 $this->assertCount(1, $this->getSystemPosts($posts, $discussion));
156 }
157
158 /**
159 * @dataProvider resolveDiscussionByMapperDataProvider
160 */
161 public function testResolveDiscussionByMapper(string $state, bool $expected)
162 {
163 $starter = User::factory()->create();
164 $discussion = BeatmapDiscussion::factory()->general()->problem()->create([
165 'beatmapset_id' => $this->beatmapsetFactory()->$state(),
166 'user_id' => $starter,
167 ]);
168
169 $this->expectCountChange(fn () => BeatmapDiscussionPost::count(), $expected ? 2 : 0);
170 if (!$expected) {
171 $this->expectException(AuthorizationException::class);
172 }
173
174 $posts = (new Reply($this->mapper, $discussion, static::TEST_MESSAGE, true))->handle();
175
176 Queue::assertPushed(BeatmapsetDiscussionPostNew::class);
177
178 $this->assertSame($expected, $discussion->fresh()->resolved);
179 $this->assertCount(1, $this->getSystemPosts($posts, $discussion));
180 }
181
182 /**
183 * @dataProvider resolveDiscussionByMapperDataProvider
184 */
185 public function testResolveDiscussionByGuestMapper(string $state, bool $expected)
186 {
187 $user = User::factory()->create()->markSessionVerified();
188 $beatmapset = $this->beatmapsetFactory()->$state()->create(['user_id' => $user]);
189 $beatmap = $beatmapset->beatmaps->first();
190 $discussion = BeatmapDiscussion::factory()->general()->problem()->create([
191 'beatmap_id' => $beatmap,
192 'beatmapset_id' => $beatmapset,
193 'user_id' => User::factory(),
194 ]);
195
196 $this->expectCountChange(fn () => BeatmapDiscussionPost::count(), $expected ? 2 : 0);
197 if (!$expected) {
198 $this->expectException(AuthorizationException::class);
199 }
200
201 $posts = (new Reply($user, $discussion, static::TEST_MESSAGE, true))->handle();
202
203 Queue::assertPushed(BeatmapsetDiscussionPostNew::class);
204
205 $this->assertSame($expected, $discussion->fresh()->resolved);
206 $this->assertCount(1, $this->getSystemPosts($posts, $discussion));
207 }
208
209 /**
210 * @dataProvider resolveDiscussionByOtherUsersDataProvider
211 */
212 public function testResolveDiscussionByOtherUsers(?string $group, bool $expected)
213 {
214 $user = User::factory()->withGroup($group)->create()->markSessionVerified();
215 $discussion = BeatmapDiscussion::factory()->general()->problem()->create([
216 'beatmapset_id' => $this->beatmapsetFactory(),
217 'user_id' => User::factory(),
218 ]);
219
220 $this->expectCountChange(fn () => BeatmapDiscussionPost::count(), $expected ? 2 : 0);
221 if (!$expected) {
222 $this->expectException(AuthorizationException::class);
223 }
224
225 $posts = (new Reply($user, $discussion, static::TEST_MESSAGE, true))->handle();
226
227 Queue::assertPushed(BeatmapsetDiscussionPostNew::class);
228
229 $this->assertSame($expected, $discussion->fresh()->resolved);
230 $this->assertCount(1, $this->getSystemPosts($posts, $discussion));
231 }
232
233 /**
234 * @dataProvider reopeningProblemDoesNotDisqualifyOrResetNominationsDataProvider
235 */
236 public function testReopeningProblemDoesNotDisqualifyOrResetNominations(?string $group, string $state)
237 {
238 $user = User::factory()->withGroup($group)->create()->markSessionVerified();
239
240 $beatmapset = $this->beatmapsetFactory()
241 ->withNominations()
242 ->$state()
243 ->create();
244
245 $discussion = BeatmapDiscussion::factory()->general()->problem()->create([
246 'beatmapset_id' => $beatmapset,
247 'resolved' => true,
248 'user_id' => User::factory(),
249 ]);
250
251
252 (new Reply($user, $discussion, static::TEST_MESSAGE, false))->handle();
253
254 Queue::assertPushed(BeatmapsetDiscussionPostNew::class);
255 Queue::assertNotPushed(BeatmapsetDisqualify::class);
256 Queue::assertNotPushed(BeatmapsetResetNominations::class);
257
258 $this->assertFalse($discussion->fresh()->resolved);
259 $this->assertSame($state, $beatmapset->fresh()->status());
260 }
261
262 public function testReopenResolvedDiscussionByMapper()
263 {
264 $beatmapset = $this->beatmapsetFactory()->qualified()->create();
265 $discussion = BeatmapDiscussion::factory()->general()->problem()->create([
266 'beatmapset_id' => $beatmapset,
267 'resolved' => true,
268 'user_id' => $this->mapper,
269 ]);
270
271 $this->expectCountChange(fn () => BeatmapDiscussionPost::count(), 2);
272
273 $posts = (new Reply($this->mapper, $discussion, static::TEST_MESSAGE, false))->handle();
274
275 Queue::assertPushed(BeatmapsetDiscussionPostNew::class);
276
277 $this->assertFalse($discussion->fresh()->resolved);
278 $this->assertTrue($beatmapset->fresh()->isQualified());
279 $this->assertCount(1, $this->getSystemPosts($posts, $discussion));
280 }
281
282 public function testReopenResolvedDiscussionByStarter()
283 {
284 $user = User::factory()->create()->markSessionVerified();
285 $beatmapset = $this->beatmapsetFactory()->qualified()->create();
286 $discussion = BeatmapDiscussion::factory()->general()->problem()->create([
287 'beatmapset_id' => $beatmapset,
288 'resolved' => true,
289 'user_id' => $user,
290 ]);
291
292 $this->expectCountChange(fn () => BeatmapDiscussionPost::count(), 2);
293
294 $posts = (new Reply($user, $discussion, static::TEST_MESSAGE, false))->handle();
295
296 Queue::assertPushed(BeatmapsetDiscussionPostNew::class);
297
298 $this->assertFalse($discussion->fresh()->resolved);
299 $this->assertTrue($beatmapset->fresh()->isQualified());
300 $this->assertCount(1, $this->getSystemPosts($posts, $discussion));
301 }
302
303 /**
304 * @dataProvider userGroupsDataProvider
305 */
306 public function testReplyToMapperNoteByOtherUsers(?string $group)
307 {
308 $user = User::factory()->withGroup($group)->create()->markSessionVerified();
309
310 $discussion = BeatmapDiscussion::factory()->general()->mapperNote()->create([
311 'beatmapset_id' => $this->beatmapsetFactory(),
312 'user_id' => $this->mapper,
313 ]);
314
315 $this->expectCountChange(fn () => BeatmapDiscussion::count(), 0, BeatmapDiscussion::class);
316 $this->expectCountChange(fn () => BeatmapDiscussionPost::count(), 1, BeatmapDiscussionPost::class);
317
318 (new Reply($user, $discussion, static::TEST_MESSAGE))->handle();
319
320 Queue::assertPushed(BeatmapsetDiscussionPostNew::class);
321 }
322
323 public function testRequestingSameResolveStateDoesNotChangeResovled()
324 {
325 $discussion = BeatmapDiscussion::factory()->general()->problem()->create([
326 'beatmapset_id' => $this->beatmapsetFactory(),
327 'resolved' => true,
328 'user_id' => $this->mapper,
329 ]);
330
331 $discussionCopy = $discussion->fresh();
332
333 $reply = new Reply($this->mapper, $discussion, static::TEST_MESSAGE, true);
334
335 // unresolved after query
336 $discussionCopy->update(['resolved' => false]);
337
338 $reply->handle();
339
340 // stays unresolved
341 $this->assertFalse($discussion->fresh()->resolved);
342 }
343
344 public static function reopeningProblemDoesNotDisqualifyOrResetNominationsDataProvider()
345 {
346 return [
347 ['bng', 'pending'],
348 ['bng', 'qualified'],
349 ['bng_limited', 'pending'],
350 ['bng_limited', 'qualified'],
351 [null, 'pending'],
352 [null, 'qualified'],
353 ];
354 }
355
356 public static function replyQueuesNotificationDataProviderToStarter()
357 {
358 return [
359 ['praise', false],
360 ['problem', true],
361 ['suggestion', true],
362 ];
363 }
364
365 public static function resolveDiscussionByStarterDataProvider()
366 {
367 return [
368 ['praise', false],
369 ['problem', true],
370 ['suggestion', true],
371 ];
372 }
373
374 public static function resolveDiscussionByMapperDataProvider()
375 {
376 return [
377 ['pending', true],
378 ['qualified', false],
379 ];
380 }
381
382 public static function resolveDiscussionByOtherUsersDataProvider()
383 {
384 return [
385 ['bng', false],
386 ['bng_limited', false],
387 ['gmt', true],
388 ['nat', true],
389 [null, false],
390 ];
391 }
392
393 public static function userGroupsDataProvider()
394 {
395 return [
396 ['admin'],
397 ['bng'],
398 ['bng_limited'],
399 ['gmt'],
400 ['nat'],
401 [null],
402 ];
403 }
404
405 protected function setUp(): void
406 {
407 parent::setUp();
408
409 Queue::fake();
410 Event::fake();
411
412 config_set('osu.beatmapset.required_nominations', 1);
413
414 $this->mapper = User::factory()->create()->markSessionVerified();
415 }
416
417 private function beatmapsetFactory(array $beatmapState = [])
418 {
419 $factory = Beatmapset::factory()
420 ->owner($this->mapper)
421 ->has(Beatmap::factory()->state(array_merge([
422 'user_id' => $this->mapper,
423 ], $beatmapState)));
424
425 return $factory;
426 }
427
428 private function getSystemPosts(array $posts, BeatmapDiscussion $discussion)
429 {
430 return array_filter(
431 $posts,
432 fn (BeatmapDiscussionPost $post) => $post->system && $post->beatmap_discussion_id === $discussion->getKey()
433 );
434 }
435}