the browser-facing portion of osu!
at master 15 kB view raw
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\VerificationRequiredException; 13use App\Jobs\Notifications\BeatmapsetDiscussionPostNew; 14use App\Jobs\Notifications\BeatmapsetDiscussionQualifiedProblem; 15use App\Jobs\Notifications\BeatmapsetDisqualify; 16use App\Jobs\Notifications\BeatmapsetResetNominations; 17use App\Libraries\BeatmapsetDiscussion\Discussion; 18use App\Models\Beatmap; 19use App\Models\BeatmapDiscussion; 20use App\Models\BeatmapDiscussionPost; 21use App\Models\Beatmapset; 22use App\Models\Notification; 23use App\Models\User; 24use App\Models\UserNotification; 25use Event; 26use Queue; 27use Tests\TestCase; 28 29class DiscussionTest extends TestCase 30{ 31 private const TEST_MESSAGE = 'not important'; 32 33 private User $mapper; 34 35 /** 36 * @dataProvider minPlaysVerificationDataProvider 37 */ 38 public function testMinPlaysVerification(\Closure $minPlays, bool $verified, bool $success) 39 { 40 config_set('osu.user.post_action_verification', false); 41 42 $user = User::factory()->withPlays($minPlays())->create(); 43 $beatmapset = $this->beatmapsetFactory()->create(); 44 $beatmapset->watches()->create(['user_id' => User::factory()->create()->getKey()]); 45 46 $change = $success ? 1 : 0; 47 $this->expectCountChange(fn () => BeatmapDiscussion::count(), $change, BeatmapDiscussion::class); 48 $this->expectCountChange(fn () => BeatmapDiscussionPost::count(), $change, BeatmapDiscussionPost::class); 49 $this->expectCountChange(fn () => Notification::count(), $change, Notification::class); 50 $this->expectCountChange(fn () => UserNotification::count(), $change, UserNotification::class); 51 52 if ($verified) { 53 $user->markSessionVerified(); 54 } 55 56 if (!$success) { 57 $this->expectException(VerificationRequiredException::class); 58 } 59 60 (new Discussion($user, $beatmapset, $this->makeParams('praise'), static::TEST_MESSAGE))->handle(); 61 } 62 63 /** 64 * See testReopeningProblemDoesNotDisqualifyOrResetNominations for assertions 65 * jobs are not queued when reopening a resolved discussion. 66 * 67 * @dataProvider newDiscussionQueuesJobsDataProvider 68 */ 69 public function testNewDiscussionQueuesJobs(string $state, ?string $group, array $queued, array $notQueued) 70 { 71 $user = User::factory()->withGroup($group)->create()->markSessionVerified(); 72 73 $beatmapset = $this->beatmapsetFactory() 74 ->withNominations() 75 ->$state() 76 ->create(); 77 78 Queue::fake(); 79 80 (new Discussion($user, $beatmapset, $this->makeParams('problem'), static::TEST_MESSAGE))->handle(); 81 82 foreach ($queued as $class) { 83 Queue::assertPushed($class); 84 } 85 86 foreach ($notQueued as $class) { 87 Queue::assertNotPushed($class); 88 } 89 } 90 91 /** 92 * @dataProvider shouldDisqualifyOrResetNominationsDataProvider 93 */ 94 public function testShouldDisqualifyOrResetNominations(string $state, ?string $group, string $messageType, bool $expects) 95 { 96 $user = User::factory()->withGroup($group)->create()->markSessionVerified(); 97 98 $beatmapset = $this->beatmapsetFactory() 99 ->withNominations() 100 ->$state() 101 ->create(); 102 103 $subject = new Discussion($user, $beatmapset, $this->makeParams($messageType), static::TEST_MESSAGE); 104 105 $value = $this->invokeMethod($subject, 'shouldDisqualifyOrResetNominations'); 106 $this->assertSame($expects, $value); 107 } 108 109 public function testWatchersGetNotification() 110 { 111 $user = User::factory()->create()->markSessionVerified(); 112 $watcher = User::factory()->create(); 113 $beatmapset = $this->beatmapsetFactory()->create(); 114 $beatmapset->watches()->create(['user_id' => $watcher->getKey()]); 115 116 Queue::fake(); 117 118 (new Discussion($user, $beatmapset, $this->makeParams('praise'), static::TEST_MESSAGE))->handle(); 119 120 Queue::assertPushed( 121 BeatmapsetDiscussionPostNew::class, 122 fn (BeatmapsetDiscussionPostNew $job) => ( 123 $this->inReceivers($watcher, $job) 124 && !$this->inReceivers($user, $job) 125 ) 126 ); 127 128 $this->runFakeQueue(); 129 130 // TODO: this should probably be changed to asserting "if job queued, then event is broadcast to receivers with option set" 131 Event::assertDispatched( 132 NewPrivateNotificationEvent::class, 133 fn (NewPrivateNotificationEvent $event) => ( 134 $this->inReceivers($watcher, $event) 135 && !$this->inReceivers($user, $event) 136 ) 137 ); 138 } 139 140 //region Posting mapper notes 141 142 public function testNewMapperNote() 143 { 144 $beatmapset = $this->beatmapsetFactory()->create(); 145 146 $this->expectCountChange(fn () => BeatmapDiscussion::count(), 1, BeatmapDiscussion::class); 147 $this->expectCountChange(fn () => BeatmapDiscussionPost::count(), 1, BeatmapDiscussionPost::class); 148 149 (new Discussion($this->mapper, $beatmapset, $this->makeParams('mapper_note'), static::TEST_MESSAGE))->handle(); 150 } 151 152 /** 153 * @dataProvider newMapperNoteByOtherUsersDataProvider 154 */ 155 public function testNewMapperNoteByOtherUsers(?string $group, bool $expected) 156 { 157 $user = User::factory()->withGroup($group)->create()->markSessionVerified(); 158 $beatmapset = $this->beatmapsetFactory()->create(); 159 160 $change = $expected ? 1 : 0; 161 $this->expectCountChange(fn () => BeatmapDiscussion::count(), $change, BeatmapDiscussion::class); 162 $this->expectCountChange(fn () => BeatmapDiscussionPost::count(), $change, BeatmapDiscussionPost::class); 163 164 if (!$expected) { 165 $this->expectException(AuthorizationException::class); 166 } 167 168 (new Discussion($user, $beatmapset, $this->makeParams('mapper_note'), static::TEST_MESSAGE))->handle(); 169 } 170 171 public function testNewMapperNoteNoteByGuestOnGuestBeatmap() 172 { 173 $user = User::factory()->create()->markSessionVerified(); 174 $beatmapset = $this->beatmapsetFactory(['user_id' => $user])->create(); 175 $beatmap = $beatmapset->beatmaps->first(); 176 177 $this->expectCountChange(fn () => BeatmapDiscussion::count(), 1, BeatmapDiscussion::class); 178 $this->expectCountChange(fn () => BeatmapDiscussionPost::count(), 1, BeatmapDiscussionPost::class); 179 180 (new Discussion( 181 $user, 182 $beatmapset, 183 $this->makeParams('mapper_note', $beatmap), 184 static::TEST_MESSAGE 185 ))->handle(); 186 } 187 188 public function testNewMapperNoteNoteByMapperOnGuestBeatmap() 189 { 190 $user = User::factory()->create()->markSessionVerified(); 191 $beatmapset = $this->beatmapsetFactory(['user_id' => $user])->create(); 192 $beatmap = $beatmapset->beatmaps->first(); 193 194 $this->expectCountChange(fn () => BeatmapDiscussion::count(), 1, BeatmapDiscussion::class); 195 $this->expectCountChange(fn () => BeatmapDiscussionPost::count(), 1, BeatmapDiscussionPost::class); 196 197 (new Discussion( 198 $this->mapper, 199 $beatmapset, 200 $this->makeParams('mapper_note', $beatmap), 201 static::TEST_MESSAGE 202 ))->handle(); 203 } 204 205 //endregion 206 207 //region Reporting problem on a beatmap 208 209 /** 210 * @dataProvider problemOnQualifiedBeatmapsetDataProvider 211 */ 212 public function testProblemOnQualifiedBeatmapset(string $state, string $assertMethod) 213 { 214 $user = User::factory()->create()->markSessionVerified(); 215 216 $beatmapset = $this->beatmapsetFactory() 217 ->$state() 218 ->create(); 219 220 User::factory()->create()->notificationOptions()->create([ 221 'name' => Notification::BEATMAPSET_DISCUSSION_QUALIFIED_PROBLEM, 222 'details' => ['modes' => array_keys(Beatmap::MODES)], 223 ]); 224 225 (new Discussion($user, $beatmapset, $this->makeParams('problem'), static::TEST_MESSAGE))->handle(); 226 227 $assertMethod(NewPrivateNotificationEvent::class); 228 } 229 230 public function testSecondProblemOnQualifiedBeatmapset() 231 { 232 // TODO: add test for hasPriorOpenProblems? 233 234 $user = User::factory()->create()->markSessionVerified(); 235 236 $beatmapset = $this->beatmapsetFactory() 237 ->qualified() 238 ->has(BeatmapDiscussion::factory()->general()->problem()->state([ 239 'user_id' => $user, 240 ])) 241 ->create(); 242 243 User::factory()->create()->notificationOptions()->create([ 244 'name' => Notification::BEATMAPSET_DISCUSSION_QUALIFIED_PROBLEM, 245 'details' => ['modes' => array_keys(Beatmap::MODES)], 246 ]); 247 248 (new Discussion($user, $beatmapset, $this->makeParams('problem'), static::TEST_MESSAGE))->handle(); 249 250 Event::assertNotDispatched(NewPrivateNotificationEvent::class); 251 } 252 253 /** 254 * @dataProvider problemOnQualifiedBeatmapsetModesNotificationDataProvider 255 * 256 * @return void 257 */ 258 public function testProblemOnQualifiedBeatmapsetModesNotification(string $mode, array $notificationModes, bool $expectsNotification) 259 { 260 $user = User::factory()->create()->markSessionVerified(); 261 262 $beatmapset = $this->beatmapsetFactory(['playmode' => Beatmap::MODES[$mode]]) 263 ->qualified() 264 ->create(); 265 266 $watcher = User::factory()->create(); 267 $watcher->notificationOptions()->create([ 268 'name' => Notification::BEATMAPSET_DISCUSSION_QUALIFIED_PROBLEM, 269 'details' => ['modes' => $notificationModes], 270 ]); 271 272 // TODO: only test the handleProblemDiscussion() part? 273 (new Discussion($user, $beatmapset, $this->makeParams('problem'), static::TEST_MESSAGE))->handle(); 274 275 if ($expectsNotification) { 276 Event::assertDispatched( 277 NewPrivateNotificationEvent::class, 278 fn (NewPrivateNotificationEvent $event) => $this->inReceivers($watcher, $event) 279 ); 280 } else { 281 Event::assertNotDispatched(NewPrivateNotificationEvent::class); 282 } 283 } 284 285 //endregion 286 287 public static function minPlaysVerificationDataProvider() 288 { 289 return [ 290 [fn () => $GLOBALS['cfg']['osu']['user']['min_plays_for_posting'] - 1, false, false], 291 [fn () => $GLOBALS['cfg']['osu']['user']['min_plays_for_posting'] - 1, true, true], 292 [fn () => null, false, true], 293 [fn () => null, true, true], 294 ]; 295 } 296 297 public static function problemOnQualifiedBeatmapsetDataProvider() 298 { 299 return [ 300 ['pending', 'Event::assertNotDispatched'], 301 ['qualified', 'Event::assertDispatched'], 302 ]; 303 } 304 305 public static function problemOnQualifiedBeatmapsetModesNotificationDataProvider() 306 { 307 return [ 308 'with matching notification mode' => ['osu', ['osu'], true], 309 'wihtout matching notification mode' => ['osu', ['taiko'], false], 310 ]; 311 } 312 313 public static function newDiscussionQueuesJobsDataProvider() 314 { 315 return [ 316 [ 317 'qualified', 318 'bng', 319 [BeatmapsetDisqualify::class, BeatmapsetDiscussionPostNew::class], 320 [BeatmapsetDiscussionQualifiedProblem::class, BeatmapsetResetNominations::class], 321 ], 322 [ 323 'qualified', 324 'bng_limited', 325 [BeatmapsetDiscussionPostNew::class, BeatmapsetDiscussionQualifiedProblem::class], 326 [BeatmapsetDisqualify::class, BeatmapsetResetNominations::class], 327 ], 328 [ 329 'qualified', 330 null, 331 [BeatmapsetDiscussionPostNew::class, BeatmapsetDiscussionQualifiedProblem::class], 332 [BeatmapsetDisqualify::class, BeatmapsetResetNominations::class], 333 ], 334 [ 335 'pending', 336 'bng', 337 [BeatmapsetResetNominations::class, BeatmapsetDiscussionPostNew::class], 338 [BeatmapsetDiscussionQualifiedProblem::class, BeatmapsetDisqualify::class], 339 ], 340 [ 341 'pending', 342 'bng_limited', 343 [BeatmapsetResetNominations::class, BeatmapsetDiscussionPostNew::class], 344 [BeatmapsetDiscussionQualifiedProblem::class, BeatmapsetDisqualify::class], 345 ], 346 [ 347 'pending', 348 null, 349 [BeatmapsetDiscussionPostNew::class], 350 [BeatmapsetDiscussionQualifiedProblem::class, BeatmapsetDisqualify::class, BeatmapsetResetNominations::class], 351 ], 352 ]; 353 } 354 355 public static function newMapperNoteByOtherUsersDataProvider() 356 { 357 return [ 358 ['bng', true], 359 ['bng_limited', true], 360 ['gmt', true], 361 ['nat', true], 362 [null, false], 363 ]; 364 } 365 366 public static function shouldDisqualifyOrResetNominationsDataProvider() 367 { 368 return [ 369 ['pending', 'bng', 'problem', true], 370 ['pending', 'bng', 'suggestion', false], 371 ['pending', 'bng_limited', 'problem', true], 372 ['pending', 'bng_limited', 'suggestion', false], 373 ['pending', null, 'problem', false], 374 ['pending', null, 'suggestion', false], 375 376 // similar to pending except bng_limited cannot disqualify 377 ['qualified', 'bng', 'problem', true], 378 ['qualified', 'bng', 'suggestion', false], 379 ['qualified', 'bng_limited', 'problem', false], // cannot disqualify 380 ['qualified', 'bng_limited', 'suggestion', false], 381 ['qualified', null, 'problem', false], 382 ['qualified', null, 'suggestion', false], 383 ]; 384 } 385 386 public static function userGroupsDataProvider() 387 { 388 return [ 389 ['admin'], 390 ['bng'], 391 ['bng_limited'], 392 ['gmt'], 393 ['nat'], 394 [null], 395 ]; 396 } 397 398 protected function setUp(): void 399 { 400 parent::setUp(); 401 402 Event::fake(); 403 404 config_set('osu.beatmapset.required_nominations', 1); 405 406 $this->mapper = User::factory()->create()->markSessionVerified(); 407 } 408 409 private function beatmapsetFactory(array $beatmapState = []) 410 { 411 $factory = Beatmapset::factory() 412 ->owner($this->mapper) 413 ->has(Beatmap::factory()->state(array_merge([ 414 'user_id' => $this->mapper, 415 ], $beatmapState))); 416 417 return $factory; 418 } 419 420 private function makeParams(string $messageType, ?Beatmap $beatmap = null) 421 { 422 return [ 423 'beatmap_id' => $beatmap !== null ? $beatmap->getKey() : null, 424 'message_type' => $messageType, 425 ]; 426 } 427}