the browser-facing portion of osu!
at master 13 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\Controllers; 9 10use App\Libraries\Search\ScoreSearch; 11use App\Models\Beatmap; 12use App\Models\Beatmapset; 13use App\Models\Country; 14use App\Models\Genre; 15use App\Models\Group; 16use App\Models\Language; 17use App\Models\OAuth; 18use App\Models\Solo\Score as SoloScore; 19use App\Models\User; 20use App\Models\UserGroup; 21use App\Models\UserGroupEvent; 22use App\Models\UserRelation; 23use Tests\TestCase; 24 25class BeatmapsControllerSoloScoresTest extends TestCase 26{ 27 protected $connectionsToTransact = []; 28 29 private static Beatmap $beatmap; 30 private static User $otherUser; 31 private static array $scores; 32 private static User $user; 33 34 public static function setUpBeforeClass(): void 35 { 36 static::withDbAccess(function () { 37 static::$user = User::factory()->create(['osu_subscriber' => true]); 38 static::$otherUser = User::factory()->create(['country_acronym' => Country::factory()]); 39 $friend = User::factory()->create(['country_acronym' => Country::factory()]); 40 static::$beatmap = Beatmap::factory()->qualified()->create(); 41 42 $countryAcronym = static::$user->country_acronym; 43 44 $otherUser2 = User::factory()->create(['country_acronym' => Country::factory()]); 45 $otherUser3SameCountry = User::factory()->create(['country_acronym' => $countryAcronym]); 46 47 static::$scores = []; 48 $scoreFactory = SoloScore::factory()->state(['build_id' => 0]); 49 foreach (['solo' => false, 'legacy' => true] as $type => $isLegacy) { 50 $scoreFactory = $scoreFactory->state([ 51 'legacy_score_id' => $isLegacy ? rand() : null, 52 ]); 53 $makeMods = fn (array $modNames): array => array_map( 54 fn (string $modName): array => [ 55 'acronym' => $modName, 56 'settings' => [], 57 ], 58 [...$modNames, ...($isLegacy ? ['CL'] : [])], 59 ); 60 61 $makeTotalScores = fn (int $totalScore): array => [ 62 'legacy_total_score' => $totalScore * ($isLegacy ? 1 : 0), 63 'total_score' => $totalScore + ($isLegacy ? -1 : 0), 64 ]; 65 66 static::$scores = [ 67 ...static::$scores, 68 "{$type}:userModsLowerScore" => $scoreFactory->withData([ 69 'mods' => $makeMods(['DT', 'HD']), 70 ])->create([ 71 ...$makeTotalScores(1000), 72 'beatmap_id' => static::$beatmap, 73 'preserve' => true, 74 'user_id' => static::$user, 75 ]), 76 "{$type}:otherUserModsNCPFHigherScore" => $scoreFactory->withData([ 77 'mods' => $makeMods(['NC', 'PF']), 78 ])->create([ 79 ...$makeTotalScores(1010), 80 'beatmap_id' => static::$beatmap, 81 'preserve' => true, 82 'user_id' => static::$otherUser, 83 ]), 84 "{$type}:userMods" => $scoreFactory->withData([ 85 'mods' => $makeMods(['DT', 'HD']), 86 ])->create([ 87 ...$makeTotalScores(1050), 88 'beatmap_id' => static::$beatmap, 89 'preserve' => true, 90 'user_id' => static::$user, 91 ]), 92 "{$type}:userModsNC" => $scoreFactory->withData([ 93 'mods' => $makeMods(['NC']), 94 ])->create([ 95 ...$makeTotalScores(1050), 96 'beatmap_id' => static::$beatmap, 97 'preserve' => true, 98 'user_id' => static::$user, 99 ]), 100 "{$type}:user" => $scoreFactory->create([ 101 ...$makeTotalScores(1100), 102 'beatmap_id' => static::$beatmap, 103 'preserve' => true, 104 'user_id' => static::$user, 105 ]), 106 "{$type}:friend" => $scoreFactory->create([ 107 ...$makeTotalScores(1000), 108 'beatmap_id' => static::$beatmap, 109 'preserve' => true, 110 'user_id' => $friend, 111 ]), 112 // With preference mods 113 "{$type}:otherUser" => $scoreFactory->withData([ 114 'mods' => $makeMods(['PF']), 115 ])->create([ 116 ...$makeTotalScores(1000), 117 'beatmap_id' => static::$beatmap, 118 'preserve' => true, 119 'user_id' => static::$otherUser, 120 ]), 121 "{$type}:otherUserMods" => $scoreFactory->withData([ 122 'mods' => $makeMods(['HD', 'PF', 'NC']), 123 ])->create([ 124 ...$makeTotalScores(1000), 125 'beatmap_id' => static::$beatmap, 126 'preserve' => true, 127 'user_id' => static::$otherUser, 128 ]), 129 "{$type}:otherUserModsExtraNonPreferences" => $scoreFactory->withData([ 130 'mods' => $makeMods(['DT', 'HD', 'HR']), 131 ])->create([ 132 ...$makeTotalScores(1000), 133 'beatmap_id' => static::$beatmap, 134 'preserve' => true, 135 'user_id' => static::$otherUser, 136 ]), 137 "{$type}:otherUserModsUnrelated" => $scoreFactory->withData([ 138 'mods' => $makeMods(['FL']), 139 ])->create([ 140 ...$makeTotalScores(1000), 141 'beatmap_id' => static::$beatmap, 142 'preserve' => true, 143 'user_id' => static::$otherUser, 144 ]), 145 // Same total score but achieved later so it should come up after earlier score 146 "{$type}:otherUser2Later" => $scoreFactory->create([ 147 ...$makeTotalScores(1000), 148 'beatmap_id' => static::$beatmap, 149 'preserve' => true, 150 'user_id' => $otherUser2, 151 ]), 152 "{$type}:otherUser3SameCountry" => $scoreFactory->create([ 153 ...$makeTotalScores(1000), 154 'beatmap_id' => static::$beatmap, 155 'preserve' => true, 156 'user_id' => $otherUser3SameCountry, 157 ]), 158 // Non-preserved score should be filtered out 159 "{$type}:nonPreserved" => $scoreFactory->create([ 160 'beatmap_id' => static::$beatmap, 161 'preserve' => false, 162 'user_id' => User::factory()->state(['country_acronym' => Country::factory()]), 163 ]), 164 // Unrelated score 165 "{$type}:unrelated" => $scoreFactory->create([ 166 'user_id' => User::factory()->state(['country_acronym' => Country::factory()]), 167 ]), 168 ]; 169 } 170 171 UserRelation::create([ 172 'friend' => true, 173 'user_id' => static::$user->getKey(), 174 'zebra_id' => $friend->getKey(), 175 ]); 176 177 static::reindexScores(); 178 }); 179 } 180 181 public static function tearDownAfterClass(): void 182 { 183 parent::tearDownAfterClass(); 184 185 static::withDbAccess(function () { 186 Beatmap::truncate(); 187 Beatmapset::truncate(); 188 Country::truncate(); 189 Genre::truncate(); 190 Language::truncate(); 191 OAuth\Client::truncate(); 192 OAuth\Token::truncate(); 193 SoloScore::select()->delete(); // TODO: revert to truncate after the table is actually renamed 194 User::truncate(); 195 UserGroup::truncate(); 196 UserGroupEvent::truncate(); 197 UserRelation::truncate(); 198 (new ScoreSearch())->deleteAll(); 199 }); 200 } 201 202 /** 203 * @dataProvider dataProviderForTestQuery 204 * @group RequiresScoreIndexer 205 */ 206 public function testQuery(array $scoreKeys, array $params, string $route) 207 { 208 $resp = $this->actingAs(static::$user) 209 ->json('GET', route("beatmaps.{$route}", static::$beatmap), $params) 210 ->assertSuccessful(); 211 212 $json = json_decode($resp->getContent(), true); 213 $this->assertSame(count($scoreKeys), count($json['scores'])); 214 foreach ($json['scores'] as $i => $jsonScore) { 215 $this->assertSame(static::$scores[$scoreKeys[$i]]->getKey(), $jsonScore['id']); 216 } 217 } 218 219 /** 220 * @group RequiresScoreIndexer 221 */ 222 public function testUserScore() 223 { 224 $url = route('api.beatmaps.user.score', [ 225 'beatmap' => static::$beatmap->getKey(), 226 'legacy_only' => 1, 227 'mods' => ['DT', 'HD'], 228 'user' => static::$user->getKey(), 229 ]); 230 $this->actAsScopedUser(static::$user); 231 $this 232 ->json('GET', $url) 233 ->assertJsonPath('score.id', static::$scores['legacy:userMods']->legacy_score_id); 234 } 235 236 /** 237 * @group RequiresScoreIndexer 238 */ 239 public function testUserScoreInvalidRulesetName() 240 { 241 $url = route('api.beatmaps.user.score', [ 242 'beatmap' => static::$beatmap->getKey(), 243 'legacy_only' => 1, 244 'mode' => '_invalid', 245 'mods' => ['DT', 'HD'], 246 'user' => static::$user->getKey(), 247 ]); 248 $this->actAsScopedUser(static::$user); 249 $this 250 ->json('GET', $url) 251 ->assertStatus(422); 252 } 253 254 /** 255 * @group RequiresScoreIndexer 256 */ 257 public function testUserScoreAll() 258 { 259 $url = route('api.beatmaps.user.scores', [ 260 'beatmap' => static::$beatmap->getKey(), 261 'legacy_only' => 1, 262 'user' => static::$user->getKey(), 263 ]); 264 $this->actAsScopedUser(static::$user); 265 $this 266 ->json('GET', $url) 267 ->assertJsonCount(4, 'scores') 268 ->assertJsonPath( 269 'scores.*.id', 270 array_map(fn (string $key): int => static::$scores[$key]->legacy_score_id, [ 271 'legacy:user', 272 'legacy:userMods', 273 'legacy:userModsNC', 274 'legacy:userModsLowerScore', 275 ]) 276 ); 277 } 278 279 public static function dataProviderForTestQuery(): array 280 { 281 $ret = []; 282 foreach (['solo' => 'solo-scores', 'legacy' => 'scores'] as $type => $route) { 283 $ret = array_merge($ret, [ 284 "{$type}: no parameters" => [[ 285 "{$type}:user", 286 "{$type}:otherUserModsNCPFHigherScore", 287 "{$type}:friend", 288 "{$type}:otherUser2Later", 289 "{$type}:otherUser3SameCountry", 290 ], [], $route], 291 "{$type}: by country" => [[ 292 "{$type}:user", 293 "{$type}:otherUser3SameCountry", 294 ], ['type' => 'country'], $route], 295 "{$type}: by friend" => [[ 296 "{$type}:user", 297 "{$type}:friend", 298 ], ['type' => 'friend'], $route], 299 "{$type}: mods filter" => [[ 300 "{$type}:userMods", 301 "{$type}:otherUserMods", 302 ], ['mods' => ['DT', 'HD']], $route], 303 "{$type}: mods with implied filter" => [[ 304 "{$type}:userModsNC", 305 "{$type}:otherUserModsNCPFHigherScore", 306 ], ['mods' => ['NC']], $route], 307 "{$type}: mods with nomods" => [[ 308 "{$type}:user", 309 "{$type}:otherUserModsNCPFHigherScore", 310 "{$type}:friend", 311 "{$type}:otherUser2Later", 312 "{$type}:otherUser3SameCountry", 313 ], ['mods' => ['NC', 'NM']], $route], 314 "{$type}: nomods filter" => [[ 315 "{$type}:user", 316 "{$type}:friend", 317 "{$type}:otherUser", 318 "{$type}:otherUser2Later", 319 "{$type}:otherUser3SameCountry", 320 ], ['mods' => ['NM']], $route], 321 ]); 322 } 323 324 return $ret; 325 } 326}