the browser-facing portion of osu!
at master 34 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\Models; 9 10use App\Exceptions\AuthorizationException; 11use App\Exceptions\InvariantException; 12use App\Exceptions\UnsupportedNominationException; 13use App\Jobs\CheckBeatmapsetCovers; 14use App\Jobs\Notifications\BeatmapsetDisqualify; 15use App\Jobs\Notifications\BeatmapsetResetNominations; 16use App\Libraries\Beatmapset\ChangeBeatmapOwners; 17use App\Libraries\Beatmapset\NominateBeatmapset; 18use App\Models\Beatmap; 19use App\Models\BeatmapDiscussion; 20use App\Models\Beatmapset; 21use App\Models\BeatmapsetNomination; 22use App\Models\Genre; 23use App\Models\Language; 24use App\Models\Notification; 25use App\Models\User; 26use App\Models\UserNotification; 27use Bus; 28use Database\Factories\BeatmapsetFactory; 29use Queue; 30use Tests\TestCase; 31 32class BeatmapsetTest extends TestCase 33{ 34 public static function disqualifyOrResetNominationsDataProvider() 35 { 36 return [ 37 ['pending', BeatmapsetResetNominations::class], 38 ['qualified', BeatmapsetDisqualify::class], 39 ]; 40 } 41 42 public static function dataProviderForTestRank(): array 43 { 44 return [ 45 ['pending', false], 46 ['qualified', true], 47 ]; 48 } 49 50 public static function mainRulesetHybridBeatmapsetSameCountDataProvider() 51 { 52 return [ 53 [[ 54 [['osu', 'taiko'], null], 55 [['taiko'], 'taiko'], 56 ]], 57 [[ 58 [['osu', 'taiko'], null], 59 [['taiko', 'fruits'], 'taiko'], 60 ]], 61 [[ 62 [['osu', 'taiko'], null], 63 [['fruits', 'mania'], null], 64 ]], 65 [[ 66 [['fruits'], 'fruits'], 67 [['osu'], 'fruits'], 68 ]], 69 ]; 70 } 71 72 public static function mainRulesetHybridBeatmapsetWithGuestMappersSameCountDataProvider() 73 { 74 return [ 75 [[ 76 [['osu', 'taiko'], null], 77 [['taiko'], null, 'too_many_non_main_ruleset'], 78 ]], 79 [[ 80 [['osu', 'taiko'], null], 81 [['taiko', 'fruits'], null, 'too_many_non_main_ruleset'], 82 ]], 83 [[ 84 [['osu', 'taiko'], null], 85 [['fruits', 'mania'], null], 86 [['fruits'], 'fruits'], 87 ]], 88 [[ 89 [['fruits'], 'fruits'], 90 [['mania'], 'fruits'], 91 ]], 92 ]; 93 } 94 95 public static function nominateDataProvider() 96 { 97 return [ 98 'bng nominate same ruleset' => ['bng', ['osu'], 'osu', true], 99 'bng nominate different ruleset' => ['bng', ['osu'], 'taiko', false], 100 'nat defaults to all rulesets' => ['nat', [], 'osu', true], 101 'nat nominate same ruleset' => ['nat', ['osu'], 'osu', true], 102 'nat nominate different ruleset' => ['nat', ['osu'], 'taiko', false], 103 ]; 104 } 105 106 public static function qualifyingNominationsDataProvider(): array 107 { 108 // existing nominations, qualifying nomination, expected 109 return [ 110 'Nomination requires at least one full nominator' => ['bng_limited', 'bng_limited', false], 111 112 // limited bngs can be the qualifying nomination 113 ['bng', 'bng_limited', true], 114 ['nat', 'bng_limited', true], 115 116 ['bng_limited', 'bng', true], 117 ['bng_limited', 'nat', true], 118 ]; 119 } 120 121 public static function qualifyingNominationsHybridDataProvider(): array 122 { 123 // existing nominations, qualifying nomination, expected 124 return [ 125 'Nomination requires at least one full nominator' => ['bng_limited', 'bng_limited', false], 126 'Limited BNs cannot nominate the hybrid mode #1' => ['bng', 'bng_limited', false], 127 'Limited BNs cannot nominate the hybrid mode #2' => ['nat', 'bng_limited', false], 128 129 ['bng_limited', 'bng', true], 130 ['bng_limited', 'nat', true], 131 ]; 132 } 133 134 public static function rankWithOpenIssueDataProvider() 135 { 136 return [ 137 ['problem'], 138 ['suggestion'], 139 ]; 140 } 141 142 public function testInvalidStatePending() 143 { 144 $user = User::factory()->withGroup('nat')->create(); 145 $beatmapset = $this->beatmapsetFactory()->qualified()->withBeatmaps()->create(); 146 147 $this->expectException(InvariantException::class); 148 $this->expectExceptionMessage(osu_trans('beatmaps.nominations.incorrect_state')); 149 150 $beatmapset->fresh()->nominate($user, $beatmapset->playmodesStr()); 151 } 152 153 public function testInvalidStateRequiredHype() 154 { 155 config_set('osu.beatmapset.required_hype', 2); 156 157 $user = User::factory()->withGroup('nat')->create(); 158 $beatmapset = $this->beatmapsetFactory()->withBeatmaps()->withHypes(1)->create(); 159 160 $this->expectException(InvariantException::class); 161 $this->expectExceptionMessage(osu_trans('beatmaps.nominations.not_enough_hype')); 162 163 $beatmapset->fresh()->nominate($user, $beatmapset->playmodesStr()); 164 } 165 166 public function testInvalidStateUnresolvedIssues() 167 { 168 $user = User::factory()->withGroup('nat')->create(); 169 170 $beatmapset = $this->beatmapsetFactory() 171 ->withBeatmaps() 172 ->has(BeatmapDiscussion::factory()->general()->problem()) 173 ->create(); 174 175 $this->expectException(InvariantException::class); 176 $this->expectExceptionMessage(osu_trans('beatmaps.nominations.unresolved_issues')); 177 178 $beatmapset->fresh()->nominate($user, $beatmapset->playmodesStr()); 179 } 180 181 public function testLove() 182 { 183 $user = User::factory()->create(); 184 $beatmapset = $this->beatmapsetFactory()->withBeatmaps()->create(); 185 $otherUser = User::factory()->create(); 186 $beatmapset->watches()->create(['user_id' => $otherUser->getKey()]); 187 188 $this->expectCountChange(fn () => $beatmapset->bssProcessQueues()->count(), 1); 189 $this->expectCountChange(fn () => Notification::count(), 1); 190 $this->expectCountChange(fn () => UserNotification::count(), 1); 191 192 $beatmapset = $beatmapset->fresh(); 193 $beatmapset->love($user); 194 195 $beatmapset = $beatmapset->fresh(); 196 $this->assertTrue($beatmapset->isLoved()); 197 $this->assertSame('loved', $beatmapset->beatmaps()->first()->status()); 198 199 Bus::assertDispatched(CheckBeatmapsetCovers::class); 200 } 201 202 public function testLoveBeatmapApprovedStates(): void 203 { 204 $user = User::factory()->create(); 205 $beatmapset = $this->beatmapsetFactory()->withBeatmaps()->create(); 206 207 $specifiedBeatmap = $beatmapset->beatmaps()->first(); 208 $beatmapset->beatmaps()->saveMany([ 209 $graveyardBeatmap = Beatmap::factory()->make(['approved' => Beatmapset::STATES['graveyard']]), 210 $pendingBeatmap = Beatmap::factory()->make(['approved' => Beatmapset::STATES['pending']]), 211 $wipBeatmap = Beatmap::factory()->make(['approved' => Beatmapset::STATES['wip']]), 212 $rankedBeatmap = Beatmap::factory()->make(['approved' => Beatmapset::STATES['ranked']]), 213 ]); 214 215 $beatmapset->fresh()->love($user, [$specifiedBeatmap->getKey()]); 216 217 $this->assertTrue($beatmapset->fresh()->isLoved()); 218 $this->assertSame('loved', $specifiedBeatmap->fresh()->status()); 219 $this->assertSame('graveyard', $graveyardBeatmap->fresh()->status()); 220 $this->assertSame('graveyard', $pendingBeatmap->fresh()->status()); 221 $this->assertSame('graveyard', $wipBeatmap->fresh()->status()); 222 $this->assertSame('ranked', $rankedBeatmap->fresh()->status()); 223 224 Bus::assertDispatched(CheckBeatmapsetCovers::class); 225 } 226 227 public function testMainRulesetSingleBeatmap() 228 { 229 $beatmapset = $this->beatmapsetFactory()->withBeatmaps('taiko')->create(); 230 231 $this->assertSame('taiko', $beatmapset->mainRuleset()); 232 } 233 234 public function testMainRulesetHybridBeatmapset() 235 { 236 $beatmapset = $this->beatmapsetFactory() 237 ->withBeatmaps('osu', 1) 238 ->withBeatmaps('taiko', 2) 239 ->withBeatmaps('fruits', 3) 240 ->withBeatmaps('mania', 1) 241 ->create(); 242 243 $this->assertSame('fruits', $beatmapset->mainRuleset()); 244 } 245 246 /** 247 * @dataProvider mainRulesetHybridBeatmapsetSameCountDataProvider 248 */ 249 public function testMainRulesetHybridBeatmapsetSameCount(array $steps) 250 { 251 $userFactory = User::factory()->withGroup('bng', ['osu', 'taiko', 'fruits', 'mania']); 252 253 $beatmapset = $this->beatmapsetFactory() 254 ->withBeatmaps('osu') 255 ->withBeatmaps('taiko') 256 ->withBeatmaps('fruits') 257 ->withBeatmaps('mania') 258 ->create(); 259 260 $this->assertSame(null, $beatmapset->mainRuleset()); 261 262 foreach ($steps as $step) { 263 $nominatedRulesets = $step[0]; 264 $expectedMainRuleset = $step[1]; 265 266 $beatmapset->fresh()->nominate($userFactory->create(), $nominatedRulesets); 267 268 $this->assertSame($expectedMainRuleset, $beatmapset->fresh()->mainRuleset()); 269 } 270 } 271 272 public function testMainRulesetHybridBeatmapsetWithGuestMappers() 273 { 274 $guest = User::factory()->create(); 275 276 $beatmapset = $this->beatmapsetFactory() 277 ->withBeatmaps('osu', 1, $guest) 278 ->withBeatmaps('taiko', 3, $guest) 279 ->withBeatmaps('taiko', 1) 280 ->withBeatmaps('fruits', 2, $guest) 281 ->withBeatmaps('fruits', 2) 282 ->withBeatmaps('mania', 1) 283 ->create(); 284 285 $this->assertSame('fruits', $beatmapset->mainRuleset()); 286 } 287 288 /** 289 * @dataProvider mainRulesetHybridBeatmapsetWithGuestMappersSameCountDataProvider 290 */ 291 public function testMainRulesetHybridBeatmapsetWithGuestMappersSameCount(array $steps) 292 { 293 $userFactory = User::factory()->withGroup('bng', ['osu', 'taiko', 'fruits', 'mania']); 294 $guest = User::factory()->create(); 295 296 // possible main ruleset will be catch or mania. 297 $beatmapset = $this->beatmapsetFactory() 298 ->withBeatmaps('osu', 1) 299 ->withBeatmaps('taiko', 1, $guest) 300 ->withBeatmaps('taiko', 1) 301 ->withBeatmaps('fruits', 2, $guest) 302 ->withBeatmaps('fruits', 2) 303 ->withBeatmaps('mania', 2, $guest) 304 ->withBeatmaps('mania', 2) 305 ->create(); 306 307 $this->assertSame(null, $beatmapset->mainRuleset()); 308 309 foreach ($steps as $step) { 310 $nominatedRulesets = $step[0]; 311 $expectedMainRuleset = $step[1]; 312 $expectedErrorMessage = $step[2] ?? null; 313 314 if ($expectedErrorMessage !== null) { 315 $this->expectException(InvariantException::class); 316 $this->expectExceptionMessage(osu_trans("beatmapsets.nominate.{$expectedErrorMessage}")); 317 } 318 319 $beatmapset->fresh()->nominate($userFactory->create(), $nominatedRulesets); 320 321 $this->assertSame($expectedMainRuleset, $beatmapset->fresh()->mainRuleset()); 322 } 323 } 324 325 public function testNominationsByType() 326 { 327 $beatmapset = $this->beatmapsetFactory() 328 ->withBeatmaps('osu') 329 ->withBeatmaps('taiko') 330 ->withBeatmaps('fruits') 331 ->withBeatmaps('mania') 332 ->create(); 333 334 $userFactory = User::factory()->withGroup('bng', ['osu', 'taiko', 'fruits', 'mania']); 335 $countFilter = fn ($array, $mode) => array_filter($array, fn ($item) => $item === $mode); 336 337 $beatmapset->fresh()->nominate($userFactory->create(), ['osu']); 338 $this->assertCount(1, $beatmapset->nominationsByType()['full']); 339 $this->assertCount(1, $countFilter($beatmapset->nominationsByType()['full'], 'osu')); 340 341 $beatmapset->fresh()->nominate($userFactory->create(), ['taiko']); 342 $this->assertCount(2, $beatmapset->nominationsByType()['full']); 343 $this->assertCount(1, $countFilter($beatmapset->nominationsByType()['full'], 'taiko')); 344 345 $beatmapset->fresh()->nominate($userFactory->create(), ['fruits']); 346 $this->assertCount(3, $beatmapset->nominationsByType()['full']); 347 $this->assertCount(1, $countFilter($beatmapset->nominationsByType()['full'], 'fruits')); 348 349 $beatmapset->fresh()->nominate($userFactory->create(), ['mania']); 350 $this->assertCount(4, $beatmapset->nominationsByType()['full']); 351 $this->assertCount(1, $countFilter($beatmapset->nominationsByType()['full'], 'mania')); 352 353 $this->assertCount(0, $beatmapset->nominationsByType()['limited']); 354 355 $beatmapset->fresh()->nominate( 356 User::factory()->withGroup('bng_limited', ['osu'])->create(), 357 ['osu'] 358 ); 359 360 $beatmapset = $beatmapset->fresh(); 361 $this->assertCount(4, $beatmapset->nominationsByType()['full']); 362 $this->assertCount(1, $beatmapset->nominationsByType()['limited']); 363 $this->assertCount(1, $countFilter($beatmapset->nominationsByType()['limited'], 'osu')); 364 } 365 366 //region single-playmode beatmap sets 367 368 /** 369 * @dataProvider nominateDataProvider 370 */ 371 public function testNominate(string $group, array $groupPlaymodes, string $ruleset, bool $success) 372 { 373 $beatmapset = $this->beatmapsetFactory()->withBeatmaps($ruleset)->create(); 374 $user = User::factory()->withGroup($group, $groupPlaymodes)->create(); 375 $otherUser = User::factory()->create(); 376 $beatmapset->watches()->create(['user_id' => $otherUser->getKey()]); 377 $nominatedModes = [$ruleset]; 378 379 $this->assertNotificationChanges($success); 380 $this->assertNominationChanges($beatmapset, $success); 381 382 $this->expectExceptionCallable( 383 fn () => $beatmapset->fresh()->nominate($user, $nominatedModes), 384 $success ? null : InvariantException::class 385 ); 386 387 $beatmapset = $beatmapset->fresh(); 388 389 if ($success) { 390 $this->assertSame($nominatedModes, $beatmapset->beatmapsetNominations()->current()->first()->modes); 391 } 392 393 // Assertions that nomination doesn't qualify 394 $this->assertTrue($beatmapset->fresh()->isPending()); 395 Bus::assertNotDispatched(CheckBeatmapsetCovers::class); 396 } 397 398 public function testNominateMainRulesetInvariant() 399 { 400 $beatmapset = $this->beatmapsetFactory() 401 ->withBeatmaps('osu') 402 ->withBeatmaps('taiko') 403 ->withNominations(['osu', 'taiko'], 1) 404 ->create(); 405 406 $user = User::factory()->withGroup('bng', ['osu', 'taiko'])->create(); 407 408 $this->assertNominationChanges($beatmapset, false); 409 410 $this->expectExceptionCallable( 411 fn () => $beatmapset->fresh()->nominate($user, ['osu', 'taiko']), 412 InvariantException::class, 413 osu_trans('beatmapsets.nominate.too_many_non_main_ruleset') 414 ); 415 416 $this->assertTrue($beatmapset->fresh()->isPending()); 417 Bus::assertNotDispatched(CheckBeatmapsetCovers::class); 418 } 419 420 public function testQualify() 421 { 422 $beatmapset = $this->beatmapsetFactory()->withBeatmaps()->create(); 423 $user = User::factory()->withGroup('bng', $beatmapset->playmodesStr())->create(); 424 $otherUser = User::factory()->create(); 425 $beatmapset->watches()->create(['user_id' => $otherUser->getKey()]); 426 427 $this->expectCountChange(fn () => $beatmapset->bssProcessQueues()->count(), 1); 428 $this->assertNotificationChanges(); 429 430 $beatmapset->fresh()->qualify($user); 431 432 $this->assertTrue($beatmapset->fresh()->isQualified()); 433 434 Bus::assertDispatched(CheckBeatmapsetCovers::class); 435 } 436 437 public function testNominateWithDefaultMetadata() 438 { 439 $beatmapset = $this->beatmapsetFactory()->withBeatmaps()->state([ 440 'genre_id' => Genre::UNSPECIFIED, 441 'language_id' => Language::UNSPECIFIED, 442 ])->create(); 443 $nominator = User::factory()->withGroup('bng', $beatmapset->playmodesStr())->create(); 444 445 $this->expectException(AuthorizationException::class); 446 $this->expectExceptionMessage(osu_trans('authorization.beatmap_discussion.nominate.set_metadata')); 447 priv_check_user($nominator, 'BeatmapsetNominate', $beatmapset)->ensureCan(); 448 } 449 450 /** 451 * @dataProvider qualifyingNominationsDataProvider 452 */ 453 public function testQualifyingNominations(string $initialGroup, string $qualifyingGroup, bool $success) 454 { 455 $ruleset = array_rand(Beatmap::MODES); 456 $beatmapset = $this->beatmapsetFactory()->withBeatmaps($ruleset)->create(); 457 $this->fillNominationsExceptLastForMainRuleset($beatmapset, $initialGroup); 458 459 $nominator = User::factory()->withGroup($qualifyingGroup, [$ruleset])->create(); 460 461 priv_check_user($nominator, 'BeatmapsetNominate', $beatmapset)->ensureCan(); 462 463 $this->expectCountChange(fn () => $beatmapset->bssProcessQueues()->count(), $success ? 1 : 0); 464 465 $this->expectExceptionCallable( 466 fn () => $beatmapset->fresh()->nominate($nominator, [$ruleset]), 467 $success ? null : InvariantException::class 468 ); 469 470 $this->assertSame($success, $beatmapset->fresh()->isQualified()); 471 472 if ($success) { 473 Bus::assertDispatched(CheckBeatmapsetCovers::class); 474 } else { 475 Bus::assertNotDispatched(CheckBeatmapsetCovers::class); 476 } 477 } 478 479 /** 480 * @dataProvider dataProviderForTestRank 481 */ 482 public function testRank(string $state, bool $success): void 483 { 484 $beatmapset = $this->beatmapsetFactory()->withBeatmaps()->$state()->create(); 485 $otherUser = User::factory()->create(); 486 $beatmap = $beatmapset->beatmaps()->first(); 487 $beatmap->scoresBest()->create([ 488 'user_id' => $otherUser->getKey(), 489 ]); 490 491 $beatmapset->watches()->create(['user_id' => $otherUser->getKey()]); 492 493 $this->expectCountChange(fn () => $beatmapset->bssProcessQueues()->count(), $success ? 1 : 0); 494 $this->expectCountChange(fn () => $beatmap->scoresBest()->count(), $success ? -1 : 0); 495 $this->assertNotificationChanges($success); 496 497 $res = $beatmapset->rank(); 498 499 $this->assertSame($success, $res); 500 $this->assertSame($success, $beatmapset->fresh()->isRanked()); 501 502 if ($success) { 503 Bus::assertDispatched(CheckBeatmapsetCovers::class); 504 } else { 505 Bus::assertNotDispatched(CheckBeatmapsetCovers::class); 506 } 507 } 508 509 /** 510 * @dataProvider rankWithOpenIssueDataProvider 511 */ 512 public function testRankWithOpenIssue(string $type): void 513 { 514 $beatmapset = $this->beatmapsetFactory()->withBeatmaps() 515 ->qualified() 516 ->has(BeatmapDiscussion::factory()->general()->messageType($type))->create(); 517 518 $this->expectCountChange(fn () => $beatmapset->bssProcessQueues()->count(), 0); 519 $this->assertNotificationChanges(false); 520 521 $this->assertFalse($beatmapset->rank()); 522 $this->assertFalse($beatmapset->fresh()->isRanked()); 523 524 Bus::assertNotDispatched(CheckBeatmapsetCovers::class); 525 } 526 527 public function testGlobalScopeActive() 528 { 529 $beatmapset = Beatmapset::factory()->inactive()->create(); 530 $id = $beatmapset->getKey(); 531 532 $this->assertNull(Beatmapset::find($id)); // global scope 533 $this->assertNull(Beatmapset::withoutGlobalScopes()->active()->find($id)); // scope still applies after removing global scope 534 $this->assertTrue($beatmapset->is(Beatmapset::withoutGlobalScopes()->find($id))); // no global scopes 535 } 536 537 public function testGlobalScopeSoftDelete() 538 { 539 $beatmapset = Beatmapset::factory()->inactive()->deleted()->create(); 540 $id = $beatmapset->getKey(); 541 542 $this->assertNull(Beatmapset::withTrashed()->find($id)); 543 } 544 //endregion 545 546 //region multi-playmode beatmap sets (aka hybrid) 547 public function testHybridLegacyNominate(): void 548 { 549 $user = User::factory()->withGroup('bng', ['osu'])->create(); 550 $beatmapset = $this->createHybridBeatmapset('taiko'); 551 552 // create legacy nomination event to enable legacy nomination mode 553 BeatmapsetNomination::factory()->create([ 554 'beatmapset_id' => $beatmapset, 555 'user_id' => User::factory()->withGroup('bng', $beatmapset->playmodesStr()), 556 ]); 557 558 $beatmapset->refreshCache(); 559 560 $otherUser = User::factory()->create(); 561 $beatmapset->watches()->create(['user_id' => $otherUser->getKey()]); 562 563 $this->assertNotificationChanges(false); 564 $this->assertNominationChanges($beatmapset, false); 565 $this->expectException(UnsupportedNominationException::class); 566 567 $beatmapset->fresh()->nominate($user); 568 } 569 570 public function testHybridLegacyQualify(): void 571 { 572 $beatmapset = $this->createHybridBeatmapset('taiko'); 573 574 // create legacy nomination event to enable legacy nomination mode 575 BeatmapsetNomination::factory()->create([ 576 'beatmapset_id' => $beatmapset, 577 'user_id' => User::factory()->withGroup('bng', $beatmapset->playmodesStr()), 578 ]); 579 580 $beatmapset->refreshCache(); 581 582 $this->expectException(UnsupportedNominationException::class); 583 // fill with legacy nominations 584 $count = $GLOBALS['cfg']['osu']['beatmapset']['required_nominations'] * $beatmapset->playmodeCount() - $beatmapset->currentNominationCount() - 1; 585 for ($i = 0; $i < $count; $i++) { 586 $beatmapset->fresh()->nominate(User::factory()->withGroup('bng', ['osu'])->create()); 587 } 588 } 589 590 public function testHybridNominateFullNominationRequired(): void 591 { 592 $user = User::factory()->withGroup('bng_limited', ['osu', 'taiko'])->create(); 593 $beatmapset = $this->createHybridBeatmapset('taiko'); 594 595 $this->assertNominationChanges($beatmapset, false); 596 597 $this->expectExceptionCallable( 598 fn () => $beatmapset->fresh()->nominate($user, ['osu']), 599 InvariantException::class, 600 osu_trans('beatmapsets.nominate.full_nomination_required') 601 ); 602 } 603 604 public function testHybridNominateWithBngLimitedMultipleRulesets(): void 605 { 606 $user = User::factory()->withGroup('bng_limited', ['osu', 'taiko'])->create(); 607 $beatmapset = $this->createHybridBeatmapset(); 608 $otherUser = User::factory()->create(); 609 $beatmapset->watches()->create(['user_id' => $otherUser->getKey()]); 610 611 $this->assertNotificationChanges(false); 612 $this->assertNominationChanges($beatmapset, false); 613 614 $this->expectExceptionCallable( 615 fn () => $beatmapset->fresh()->nominate($user, ['osu', 'taiko']), 616 InvariantException::class, 617 osu_trans('beatmapsets.nominate.bng_limited_too_many_rulesets') 618 ); 619 620 $this->assertTrue($beatmapset->fresh()->isPending()); 621 Bus::assertNotDispatched(CheckBeatmapsetCovers::class); 622 } 623 624 public function testHybridNominateWithNullPlaymode(): void 625 { 626 $user = User::factory()->withGroup('bng', ['osu'])->create(); 627 $beatmapset = $this->createHybridBeatmapset('taiko'); 628 $otherUser = User::factory()->create(); 629 $beatmapset->watches()->create(['user_id' => $otherUser->getKey()]); 630 631 $this->assertNotificationChanges(false); 632 $this->assertNominationChanges($beatmapset, false); 633 634 $this->expectExceptionCallable( 635 fn () => $beatmapset->fresh()->nominate($user, []), 636 InvariantException::class, 637 osu_trans('beatmapsets.nominate.hybrid_requires_modes') 638 ); 639 640 $this->assertTrue($beatmapset->fresh()->isPending()); 641 Bus::assertNotDispatched(CheckBeatmapsetCovers::class); 642 } 643 644 public function testHybridNominateWithNoPlaymodePermission(): void 645 { 646 $user = User::factory()->withGroup('bng', ['osu'])->create(); 647 $beatmapset = $this->createHybridBeatmapset('taiko'); 648 $otherUser = User::factory()->create(); 649 $beatmapset->watches()->create(['user_id' => $otherUser->getKey()]); 650 651 $this->assertNotificationChanges(false); 652 $this->assertNominationChanges($beatmapset, false); 653 654 $this->expectExceptionCallable( 655 fn () => $beatmapset->fresh()->nominate($user, ['taiko']), 656 InvariantException::class, 657 osu_trans('beatmapsets.nominate.incorrect_mode', ['mode' => 'taiko']) 658 ); 659 660 $this->assertTrue($beatmapset->fresh()->isPending()); 661 Bus::assertNotDispatched(CheckBeatmapsetCovers::class); 662 } 663 664 public function testHybridNominateWithPlaymodePermissionSingleMode(): void 665 { 666 $user = User::factory()->withGroup('bng', ['osu'])->create(); 667 $beatmapset = $this->createHybridBeatmapset('taiko'); 668 $otherUser = User::factory()->create(); 669 $beatmapset->watches()->create(['user_id' => $otherUser->getKey()]); 670 671 $this->assertNotificationChanges(); 672 $this->assertNominationChanges($beatmapset); 673 674 $beatmapset->fresh()->nominate($user, ['osu']); 675 676 $this->assertTrue($beatmapset->fresh()->isPending()); 677 Bus::assertNotDispatched(CheckBeatmapsetCovers::class); 678 } 679 680 public function testHybridNominateWithPlaymodePermissionTooMany(): void 681 { 682 $user = User::factory()->withGroup('bng', ['osu'])->create(); 683 $beatmapset = $this->createHybridBeatmapset('taiko'); 684 685 $this->fillNominationsExceptLastForMainRuleset($beatmapset, 'bng'); 686 687 $beatmapset->fresh()->nominate( 688 User::factory()->withGroup('bng', ['osu'])->create(), 689 ['osu'] 690 ); 691 692 $this->assertNotificationChanges(false); 693 $this->assertNominationChanges($beatmapset, false); 694 695 $this->expectExceptionCallable( 696 fn () => $beatmapset->fresh()->nominate($user, ['osu']), 697 InvariantException::class, 698 osu_trans('beatmapsets.nominate.too_many_non_main_ruleset') 699 ); 700 701 $this->assertTrue($beatmapset->fresh()->isPending()); 702 Bus::assertNotDispatched(CheckBeatmapsetCovers::class); 703 } 704 705 public function testHybridNominateWithPlaymodePermissionMultipleModes(): void 706 { 707 $user = User::factory()->withGroup('bng', ['osu', 'taiko'])->create(); 708 $beatmapset = $this->createHybridBeatmapset('taiko'); 709 $otherUser = User::factory()->create(); 710 $beatmapset->watches()->create(['user_id' => $otherUser->getKey()]); 711 712 $this->assertNotificationChanges(); 713 $this->assertNominationChanges($beatmapset, ['osu', 'taiko']); 714 715 $beatmapset->fresh()->nominate($user, ['osu', 'taiko']); 716 717 $this->assertTrue($beatmapset->fresh()->isPending()); 718 Bus::assertNotDispatched(CheckBeatmapsetCovers::class); 719 } 720 721 public function testQualifyingNominationBngLimited() 722 { 723 $beatmapset = $this->createHybridBeatmapset(); 724 $beatmapset->fresh()->nominate(User::factory()->withGroup('bng', ['osu', 'taiko'])->create(), ['osu', 'taiko']); 725 $nominator = User::factory()->withGroup('bng_limited', ['osu', 'taiko'])->create(); 726 727 $this->expectCountChange(fn () => $beatmapset->bssProcessQueues()->count(), 1); 728 729 $beatmapset->fresh()->nominate($nominator, ['taiko']); 730 731 $this->assertTrue($beatmapset->fresh()->isQualified()); 732 Bus::assertDispatched(CheckBeatmapsetCovers::class); 733 } 734 735 /** 736 * @dataProvider qualifyingNominationsHybridDataProvider 737 */ 738 public function testQualifyingNominationsHybrid(string $initialGroup, string $qualifyingGroup, bool $success) 739 { 740 $nominator = User::factory()->withGroup($qualifyingGroup, ['osu', 'taiko'])->create(); 741 $beatmapset = $this->createHybridBeatmapset('taiko'); 742 743 $this->fillNominationsExceptLastForMainRuleset($beatmapset, $initialGroup); 744 745 priv_check_user($nominator, 'BeatmapsetNominate', $beatmapset)->ensureCan(); 746 747 $this->expectCountChange(fn () => $beatmapset->bssProcessQueues()->count(), $success ? 1 : 0); 748 749 $this->expectExceptionCallable( 750 fn () => $beatmapset->fresh()->nominate($nominator, ['osu', 'taiko']), 751 $success ? null : InvariantException::class 752 ); 753 754 $this->assertSame($success, $beatmapset->fresh()->isQualified()); 755 756 if ($success) { 757 Bus::assertDispatched(CheckBeatmapsetCovers::class); 758 } else { 759 Bus::assertNotDispatched(CheckBeatmapsetCovers::class); 760 } 761 } 762 763 public function testQualifyingNominationSteps() 764 { 765 $bngFactory = User::factory()->withGroup('bng', ['taiko', 'fruits']); 766 $bngLimitedFactory = User::factory()->withGroup('bng_limited', ['taiko', 'fruits']); 767 $beatmapset = $this->createHybridBeatmapset(null, ['taiko', 'fruits']); 768 769 $beatmapset->fresh()->nominate($bngFactory->create(), ['fruits']); 770 $beatmapset->fresh()->nominate($bngLimitedFactory->create(), ['fruits']); 771 772 $this->assertTrue($beatmapset->fresh()->isPending()); 773 774 $beatmapset->fresh()->nominate($bngFactory->create(), ['taiko']); 775 776 $this->assertTrue($beatmapset->fresh()->isQualified()); 777 } 778 779 //endregion 780 781 //region disqualification 782 783 /** 784 * @dataProvider disqualifyOrResetNominationsDataProvider 785 */ 786 public function testDisqualifyOrResetNominations(string $state, string $pushed) 787 { 788 $user = User::factory()->withGroup('bng')->create(); 789 $beatmapset = Beatmapset::factory()->owner()->withDiscussion()->$state()->create(); 790 $discussion = $beatmapset->beatmapDiscussions()->first(); // contents only needed for logging. 791 792 Queue::fake(); 793 794 $beatmapset->disqualifyOrResetNominations($user, $discussion); 795 796 Queue::assertPushed($pushed); 797 } 798 799 //endregion 800 801 public function testChangingOwnerDoesNotQualify() 802 { 803 $guest = User::factory()->create(); 804 $bngUser1 = User::factory()->withGroup('bng', ['osu', 'taiko'])->create(); 805 $bngUser2 = User::factory()->withGroup('bng', ['osu', 'taiko'])->create(); 806 $bngLimitedUser = User::factory()->withGroup('bng_limited', ['osu', 'taiko'])->create(); 807 $natUser = User::factory()->withGroup('nat')->create(); 808 809 // make taiko tha main ruleset 810 $beatmapset = $this->beatmapsetFactory() 811 ->withBeatmaps('taiko', 1) 812 ->withBeatmaps('taiko', 1) 813 ->withBeatmaps('osu', 1) 814 ->withBeatmaps('osu', 1, $guest) 815 ->create(); 816 817 $this->assertSame('taiko', $beatmapset->mainRuleset()); 818 819 // valid nomination for taiko and osu 820 $beatmapset->fresh()->nominate($bngLimitedUser, ['taiko']); 821 $beatmapset->fresh()->nominate($bngUser1, ['osu']); 822 823 // main ruleset should now be osu 824 (new ChangeBeatmapOwners( 825 $beatmapset->beatmaps()->where('playmode', 1)->first(), 826 [$guest->getKey()], 827 $natUser 828 ))->handle(); 829 830 (new ChangeBeatmapOwners( 831 $beatmapset->beatmaps()->where('playmode', 0)->last(), 832 [$beatmapset->user_id], 833 $natUser 834 ))->handle(); 835 836 $beatmapset->refresh(); 837 838 $this->assertSame('osu', $beatmapset->mainRuleset()); 839 840 // nomination should not trigger qualification 841 $this->expectExceptionCallable( 842 fn () => $beatmapset->fresh()->nominate($bngUser2, ['osu']), 843 InvariantException::class, 844 osu_trans('beatmapsets.nominate.invalid_limited_nomination') 845 ); 846 847 $this->assertFalse($beatmapset->fresh()->isQualified()); 848 Bus::assertNotDispatched(CheckBeatmapsetCovers::class); 849 } 850 851 protected function setUp(): void 852 { 853 parent::setUp(); 854 855 Genre::factory()->create(['genre_id' => Genre::UNSPECIFIED]); 856 Language::factory()->create(['language_id' => Language::UNSPECIFIED]); 857 858 Bus::fake([CheckBeatmapsetCovers::class]); 859 } 860 861 private function beatmapsetFactory(): BeatmapsetFactory 862 { 863 return Beatmapset::factory()->owner()->pending(); 864 } 865 866 private function createHybridBeatmapset(string $mainRuleset = null, array $rulesets = ['osu', 'taiko']): Beatmapset 867 { 868 $factory = $this->beatmapsetFactory(); 869 870 foreach ($rulesets as $ruleset) { 871 $factory = $factory->withBeatmaps($ruleset, $mainRuleset === $ruleset ? 2 : 1); 872 } 873 874 return $factory->create(); 875 } 876 877 private function fillNominationsExceptLastForMainRuleset(Beatmapset $beatmapset, string $group): void 878 { 879 $ruleset = $beatmapset->mainRuleset(); 880 if ($ruleset === null) { 881 throw new \Exception('Cannot fill nominations without main ruleset.'); 882 } 883 884 $count = NominateBeatmapset::requiredNominationsConfig()['main_ruleset'] - 1; 885 for ($i = 0; $i < $count; $i++) { 886 $beatmapset->nominate(User::factory()->withGroup($group, [$ruleset])->create(), [$ruleset]); 887 } 888 } 889 890 private function assertNominationChanges(Beatmapset $beatmapset, bool|array $success = true) 891 { 892 $count = is_array($success) 893 ? count($success) 894 : ($success ? 1 : 0); 895 896 $this->expectCountChange(fn () => $beatmapset->fresh()->nominations, $count, 'nominations'); 897 $this->expectCountChange(fn () => $beatmapset->fresh()->beatmapsetNominations()->current()->count(), $success ? 1 : 0, 'nominations count'); 898 } 899 900 private function assertNotificationChanges(bool $success = true) 901 { 902 $this->expectCountChange(fn () => Notification::count(), $success ? 1 : 0, 'Notification count'); 903 $this->expectCountChange(fn () => UserNotification::count(), $success ? 1 : 0, 'UserNotification count'); 904 } 905}