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\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}