the browser-facing portion of osu!
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Allow changing country from account settings

nanaya 438d7348 397945a5

+541 -5
+3
.env.example
··· 318 318 # OSU_URL_LAZER_WINDOWS_X64='https://github.com/ppy/osu/releases/latest/download/install.exe' 319 319 # OSU_URL_LAZER_INFO= 320 320 # OSU_URL_USER_RESTRICTION=/wiki/Help_centre/Account_restrictions 321 + 322 + # USER_COUNTRY_CHANGE_MAX_MIXED_MONTHS=2 323 + # USER_COUNTRY_CHANGE_MIN_MONTHS=6
+18
app/Http/Controllers/AccountController.php
··· 7 7 8 8 use App\Exceptions\ImageProcessorException; 9 9 use App\Exceptions\ModelNotSavedException; 10 + use App\Libraries\User\CountryChange; 11 + use App\Libraries\User\CountryChangeTarget; 10 12 use App\Libraries\UserVerification; 11 13 use App\Libraries\UserVerificationState; 12 14 use App\Mail\UserEmailUpdated; ··· 40 42 'except' => [ 41 43 'edit', 42 44 'reissueCode', 45 + 'updateCountry', 43 46 'updateEmail', 44 47 'updateNotificationOptions', 45 48 'updateOptions', ··· 154 157 } 155 158 156 159 return json_item($user, new CurrentUserTransformer()); 160 + } 161 + 162 + public function updateCountry() 163 + { 164 + $newCountry = get_string(Request('country_acronym')); 165 + $user = Auth::user(); 166 + 167 + if (CountryChangeTarget::get($user) !== $newCountry) { 168 + abort(403, 'specified country_acronym is not allowed'); 169 + } 170 + 171 + CountryChange::do($user, $newCountry, 'account settings'); 172 + \Session::flash('popup', osu_trans('common.saved')); 173 + 174 + return ext_view('layout.ujs-reload', [], 'js'); 157 175 } 158 176 159 177 public function updateEmail()
+46
app/Libraries/User/CountryChange.php
··· 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 + 6 + declare(strict_types=1); 7 + 8 + namespace App\Libraries\User; 9 + 10 + use App\Exceptions\InvariantException; 11 + use App\Models\Beatmap; 12 + use App\Models\Country; 13 + use App\Models\User; 14 + use App\Models\UserAccountHistory; 15 + 16 + class CountryChange 17 + { 18 + public static function do(User $user, string $newCountry, string $reason): void 19 + { 20 + // Assert valid country acronym 21 + $country = Country::find($newCountry); 22 + if ($country === null) { 23 + throw new InvariantException('invalid country specified'); 24 + } 25 + 26 + if ($user->country_acronym === $newCountry) { 27 + return; 28 + } 29 + 30 + $user->getConnection()->transaction(function () use ($newCountry, $reason, $user) { 31 + $oldCountry = $user->country_acronym; 32 + $user->update(['country_acronym' => $newCountry]); 33 + foreach (Beatmap::MODES as $ruleset => $_rulesetId) { 34 + $user->statistics($ruleset, true)->update(['country_acronym' => $newCountry]); 35 + $user->scoresBest($ruleset, true)->update(['country_acronym' => $newCountry]); 36 + } 37 + UserAccountHistory::addNote($user, "Changing country from {$oldCountry} to {$newCountry} ({$reason})"); 38 + }); 39 + 40 + \Artisan::queue('es:index-scores:queue', [ 41 + '--all' => true, 42 + '--no-interaction' => true, 43 + '--user' => $user->getKey(), 44 + ]); 45 + } 46 + }
+106
app/Libraries/User/CountryChangeTarget.php
··· 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 + 6 + declare(strict_types=1); 7 + 8 + namespace App\Libraries\User; 9 + 10 + use App\Models\Tournament; 11 + use App\Models\TournamentRegistration; 12 + use App\Models\User; 13 + use App\Models\UserCountryHistory; 14 + use Carbon\CarbonImmutable; 15 + 16 + class CountryChangeTarget 17 + { 18 + const MIN_DAYS_MONTH = 15; 19 + 20 + public static function currentMonth(): CarbonImmutable 21 + { 22 + $now = CarbonImmutable::now(); 23 + $subMonths = $now->day > static::MIN_DAYS_MONTH ? 0 : 1; 24 + 25 + return $now->startOfMonth()->subMonths($subMonths); 26 + } 27 + 28 + public static function maxMixedMonths(): int 29 + { 30 + return config('osu.user.country_change.max_mixed_months'); 31 + } 32 + 33 + public static function minMonths(): int 34 + { 35 + return config('osu.user.country_change.min_months'); 36 + } 37 + 38 + public static function get(User $user): ?string 39 + { 40 + $minMonths = static::minMonths(); 41 + $now = CarbonImmutable::now(); 42 + $until = static::currentMonth(); 43 + $since = $until->subMonths($minMonths - 1); 44 + 45 + if (static::isUserInTournament($user)) { 46 + return null; 47 + } 48 + 49 + $history = $user 50 + ->userCountryHistory() 51 + ->whereBetween('year_month', [ 52 + UserCountryHistory::formatDate($since), 53 + UserCountryHistory::formatDate($until), 54 + ])->whereHas('country') 55 + ->get(); 56 + 57 + // First group countries by year_month 58 + $byMonth = []; 59 + foreach ($history as $entry) { 60 + $byMonth[$entry->year_month] ??= []; 61 + $byMonth[$entry->year_month][] = $entry->country_acronym; 62 + } 63 + 64 + // For each year_month, summarise each countries 65 + $byCountry = []; 66 + foreach ($byMonth as $month => $countries) { 67 + $mixed = count($countries) > 1; 68 + foreach ($countries as $country) { 69 + $byCountry[$country] ??= [ 70 + 'total' => 0, 71 + 'mixed' => 0, 72 + ]; 73 + $byCountry[$country]['total']++; 74 + if ($mixed) { 75 + $byCountry[$country]['mixed']++; 76 + } 77 + } 78 + } 79 + 80 + // Finally find the first country which fulfills the requirement 81 + foreach ($byCountry as $country => $data) { 82 + if ($data['total'] === $minMonths && $data['mixed'] <= static::maxMixedMonths()) { 83 + if ($user->country_acronym === $country) { 84 + return null; 85 + } else { 86 + return $country; 87 + } 88 + } 89 + } 90 + 91 + return null; 92 + } 93 + 94 + private static function isUserInTournament(User $user): bool 95 + { 96 + return TournamentRegistration 97 + ::where('user_id', $user->getKey()) 98 + ->whereIn( 99 + 'tournament_id', 100 + Tournament 101 + ::where('end_date', '>', CarbonImmutable::now()) 102 + ->orWhereNull('end_date') 103 + ->select('tournament_id'), 104 + )->exists(); 105 + } 106 + }
+7
app/Models/User.php
··· 139 139 * @property int $user_avatar_width 140 140 * @property string $user_birthday 141 141 * @property string|null $user_colour 142 + * @property-read Collection<UserCountryHistory> $userCountryHistory 142 143 * @property string $user_dateformat 143 144 * @property string|null $user_discord 144 145 * @property int $user_dst ··· 271 272 private $validateEmailConfirmation = false; 272 273 273 274 private $isSessionVerified; 275 + 276 + public function userCountryHistory(): HasMany 277 + { 278 + return $this->hasMany(UserCountryHistory::class); 279 + } 274 280 275 281 public function getAuthPassword() 276 282 { ··· 920 926 'tokens', 921 927 'topicWatches', 922 928 'userAchievements', 929 + 'userCountryHistory', 923 930 'userGroups', 924 931 'userNotifications', 925 932 'userPage',
+52
app/Models/UserCountryHistory.php
··· 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 + 6 + declare(strict_types=1); 7 + 8 + namespace App\Models; 9 + 10 + use Illuminate\Database\Eloquent\Relations\BelongsTo; 11 + 12 + /** 13 + * @property int $count 14 + * @property Country $country 15 + * @property string $country_acronym 16 + * @property User $user 17 + * @property int $user_id 18 + * @property \Carbon\Carbon $last_updated 19 + * @property string $year_month 20 + */ 21 + class UserCountryHistory extends Model 22 + { 23 + public $incrementing = false; 24 + public $timestamps = false; 25 + 26 + protected $casts = ['last_updated' => 'datetime']; 27 + protected $primaryKey = ':composite'; 28 + protected $primaryKeys = ['user_id', 'year_month', 'country_acronym']; 29 + protected $table = 'user_country_history'; 30 + 31 + public static function formatDate(\DateTimeInterface $date): string 32 + { 33 + return $date->format('ym'); 34 + } 35 + 36 + public function country(): BelongsTo 37 + { 38 + return $this->belongsTo(Country::class, 'country_acronym'); 39 + } 40 + 41 + public function user(): BelongsTo 42 + { 43 + return $this->belongsTo(User::class, 'user_id'); 44 + } 45 + 46 + public function setYearMonthAttribute(\DateTimeInterface|string $value): void 47 + { 48 + $this->attributes['year_month'] = $value instanceof \DateTimeInterface 49 + ? static::formatDate($value) 50 + : $value; 51 + } 52 + }
+5
config/osu.php
··· 264 264 'registration_mode' => presence(env('REGISTRATION_MODE')) ?? 'client', 265 265 'super_friendly' => array_map('intval', explode(' ', env('SUPER_FRIENDLY', '3'))), 266 266 'ban_persist_days' => get_int(env('BAN_PERSIST_DAYS')) ?? 28, 267 + 268 + 'country_change' => [ 269 + 'max_mixed_months' => get_int(env('USER_COUNTRY_CHANGE_MAX_MIXED_MONTHS')) ?? 2, 270 + 'min_months' => get_int(env('USER_COUNTRY_CHANGE_MIN_MONTHS')) ?? 6, 271 + ], 267 272 ], 268 273 'user_report_notification' => [ 269 274 'endpoint_cheating' => presence(env('USER_REPORT_NOTIFICATION_ENDPOINT_CHEATING')),
+17
database/factories/UserFactory.php
··· 8 8 namespace Database\Factories; 9 9 10 10 use App\Libraries\Fulfillments\ApplySupporterTag; 11 + use App\Libraries\User\CountryChangeTarget; 11 12 use App\Models\Country; 12 13 use App\Models\User; 13 14 use App\Models\UserAccountHistory; 15 + use App\Models\UserCountryHistory; 14 16 use App\Models\UserStatistics\Model as UserStatisticsModel; 15 17 16 18 class UserFactory extends Factory 17 19 { 18 20 const DEFAULT_PASSWORD = 'password'; 21 + 22 + public static function createRecentCountryHistory(User $user, ?string $country, ?int $months): void 23 + { 24 + $months ??= CountryChangeTarget::minMonths(); 25 + $country ??= Country::factory()->create()->getKey(); 26 + $currentMonth = CountryChangeTarget::currentMonth(); 27 + $userId = $user->getKey(); 28 + for ($i = 0; $i < $months; $i++) { 29 + UserCountryHistory::create([ 30 + 'country_acronym' => $country, 31 + 'user_id' => $userId, 32 + 'year_month' => $currentMonth->subMonths($i), 33 + ]); 34 + } 35 + } 19 36 20 37 private static function defaultPasswordHash() 21 38 {
+36
database/migrations/2023_07_20_062848_create_user_country_history.php
··· 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 + 6 + declare(strict_types=1); 7 + 8 + use Illuminate\Database\Migrations\Migration; 9 + use Illuminate\Database\Schema\Blueprint; 10 + use Illuminate\Support\Facades\Schema; 11 + 12 + return new class extends Migration 13 + { 14 + /** 15 + * Run the migrations. 16 + */ 17 + public function up(): void 18 + { 19 + Schema::create('user_country_history', function (Blueprint $table) { 20 + $table->unsignedInteger('user_id'); 21 + $table->char('year_month', 4); 22 + $table->char('country_acronym', 2); 23 + $table->unsignedInteger('count')->default(1); 24 + $table->timestamp('last_updated')->useCurrent()->useCurrentOnUpdate(); 25 + $table->primary(['user_id', 'year_month', 'country_acronym']); 26 + }); 27 + } 28 + 29 + /** 30 + * Reverse the migrations. 31 + */ 32 + public function down(): void 33 + { 34 + Schema::dropIfExists('user_country_history'); 35 + } 36 + };
+6
resources/lang/en/accounts.php
··· 34 34 ], 35 35 36 36 'profile' => [ 37 + 'country' => 'country', 37 38 'title' => 'Profile', 39 + 40 + 'country_change' => [ 41 + '_' => "It looks like your account country doesn't match your country of residence. :update_link.", 42 + 'update_link' => 'Update to :country', 43 + ], 38 44 39 45 'user' => [ 40 46 'user_discord' => 'discord',
+43
resources/views/accounts/_edit_country.blade.php
··· 1 + {{-- 2 + Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0. 3 + See the LICENCE file in the repository root for full licence text. 4 + --}} 5 + @php 6 + use App\Libraries\User\CountryChangeTarget; 7 + use App\Models\Country; 8 + 9 + $countryChangeTarget = CountryChangeTarget::get($user); 10 + @endphp 11 + <div class="account-edit-entry account-edit-entry--read-only"> 12 + <div class="account-edit-entry__label account-edit-entry__label--top-pinned"> 13 + {{ osu_trans('accounts.edit.profile.country') }} 14 + </div> 15 + <div> 16 + <p> 17 + @include('objects._flag_country', [ 18 + 'countryCode' => $user->country_acronym, 19 + 'countryName' => null, 20 + 'modifiers' => 'wiki', 21 + ]) 22 + {{ $user->country->name }} 23 + </p> 24 + @if ($countryChangeTarget !== null) 25 + <p> 26 + {!! osu_trans('accounts.edit.profile.country_change._', [ 27 + 'update_link' => tag( 28 + 'a', 29 + [ 30 + 'data-confirm' => osu_trans('common.confirmation'), 31 + 'data-method' => 'PUT', 32 + 'data-remote' => '1', 33 + 'href' => route('account.country', ['country_acronym' => $countryChangeTarget]), 34 + ], 35 + osu_trans('accounts.edit.profile.country_change.update_link', [ 36 + 'country' => Country::find($countryChangeTarget)->name, 37 + ]), 38 + ), 39 + ]) !!} 40 + </p> 41 + @endif 42 + </div> 43 + </div>
+10 -4
resources/views/accounts/edit.blade.php
··· 2 2 Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0. 3 3 See the LICENCE file in the repository root for full licence text. 4 4 --}} 5 + @php 6 + $user = Auth::user(); 7 + $isSilenced = $user->isSilenced(); 8 + @endphp 9 + 5 10 @extends('master', ['titlePrepend' => osu_trans('accounts.edit.title_compact')]) 6 11 7 12 @section('content') 8 - @if (Auth::user()->isSilenced() && !Auth::user()->isRestricted()) 13 + @if ($isSilenced && !$user->isRestricted()) 9 14 @include('objects._notification_banner', [ 10 15 'type' => 'alert', 11 16 'title' => osu_trans('users.silenced_banner.title'), ··· 30 35 {{ osu_trans('accounts.edit.username') }} 31 36 </div> 32 37 <div class="account-edit-entry__input"> 33 - {{ Auth::user()->username }} 38 + {{ $user->username }} 34 39 </div> 35 40 36 41 <div class="account-edit-entry__button"> ··· 47 52 </a> 48 53 </div> 49 54 </div> 55 + @include('accounts._edit_country') 50 56 </div> 51 57 <div class="account-edit__input-group"> 52 58 @include('accounts._edit_entry_simple', ['field' => 'user_from']) ··· 89 95 90 96 <label 91 97 class="btn-osu-big btn-osu-big--account-edit" 92 - @if (Auth::user()->isSilenced()) 98 + @if ($isSilenced) 93 99 disabled 94 100 @endif 95 101 > ··· 108 114 type="file" 109 115 name="avatar_file" 110 116 data-url="{{ route('account.avatar') }}" 111 - @if (Auth::user()->isSilenced()) 117 + @if ($isSilenced) 112 118 disabled 113 119 @endif 114 120 >
+1 -1
resources/views/layout/popup-container.blade.php
··· 3 3 See the LICENCE file in the repository root for full licence text. 4 4 --}} 5 5 @php 6 - $popup = Session::get('popup'); 6 + $popup = Session('popup'); 7 7 @endphp 8 8 <div id="popup-container"> 9 9 <div class="alert alert-dismissable popup-clone col-md-6 col-md-offset-3 text-center" style="display: none">
+1
routes/web.php
··· 204 204 // Reference: https://bugs.php.net/bug.php?id=55815 205 205 // Note that hhvm behaves differently (the same as POST). 206 206 Route::post('avatar', 'AccountController@avatar')->name('avatar'); 207 + Route::put('country', 'AccountController@updateCountry')->name('country'); 207 208 Route::post('cover', 'AccountController@cover')->name('cover'); 208 209 Route::put('email', 'AccountController@updateEmail')->name('email'); 209 210 Route::put('notification-options', 'AccountController@updateNotificationOptions')->name('notification-options');
+31
tests/Controllers/AccountControllerTest.php
··· 7 7 8 8 namespace Tests\Controllers; 9 9 10 + use App\Libraries\User\CountryChangeTarget; 10 11 use App\Mail\UserEmailUpdated; 11 12 use App\Mail\UserPasswordUpdated; 13 + use App\Models\Country; 12 14 use App\Models\User; 13 15 use App\Models\UserProfileCustomization; 14 16 use App\Models\WeakPassword; 17 + use Database\Factories\UserFactory; 15 18 use Hash; 16 19 use Mail; 17 20 use Tests\TestCase; ··· 62 65 'order' => $newOrderWithInvalid, 63 66 ]) 64 67 ->assertJsonFragment(['profile_order' => $newOrder]); 68 + } 69 + 70 + /** 71 + * @dataProvider dataProviderForUpdateCountry 72 + * 73 + * More complete tests are done through CountryChange and CountryChangeTarget. 74 + */ 75 + public function testUpdateCountry(int $months, bool $success): void 76 + { 77 + $user = $this->user(); 78 + $targetCountry = Country::factory()->create()->getKey(); 79 + UserFactory::createRecentCountryHistory($user, $targetCountry, $months); 80 + 81 + $resultCountry = $success ? $targetCountry : $user->country_acronym; 82 + 83 + $this->actingAsVerified($user) 84 + ->json('PUT', route('account.country', ['country_acronym' => $targetCountry])) 85 + ->assertStatus($success ? 200 : 403); 86 + 87 + $this->assertSame($user->fresh()->country_acronym, $resultCountry); 65 88 } 66 89 67 90 public function testUpdateEmail() ··· 188 211 ], 189 212 ]) 190 213 ->assertStatus(422); 214 + } 215 + 216 + public function dataProviderForUpdateCountry(): array 217 + { 218 + return [ 219 + [CountryChangeTarget::minMonths(), true], 220 + [CountryChangeTarget::minMonths() - 1, false], 221 + ]; 191 222 } 192 223 193 224 protected function setUp(): void
+90
tests/Libraries/User/CountryChangeTargetTest.php
··· 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 + 6 + declare(strict_types=1); 7 + 8 + namespace Tests\Libraries\User; 9 + 10 + use App\Libraries\User\CountryChangeTarget; 11 + use App\Models\Country; 12 + use App\Models\Tournament; 13 + use App\Models\User; 14 + use Carbon\CarbonImmutable; 15 + use Database\Factories\UserFactory; 16 + use Tests\TestCase; 17 + 18 + class CountryChangeTargetTest extends TestCase 19 + { 20 + public function testGetNoData(): void 21 + { 22 + $user = User::factory()->create(); 23 + 24 + $this->assertNull(CountryChangeTarget::get($user)); 25 + } 26 + 27 + public function testGetNotEnoughMonths(): void 28 + { 29 + $user = User::factory()->create(); 30 + $targetCountry = Country::factory()->create()->getKey(); 31 + UserFactory::createRecentCountryHistory($user, $targetCountry, CountryChangeTarget::minMonths() - 1); 32 + 33 + $this->assertNull(CountryChangeTarget::get($user)); 34 + } 35 + 36 + public function testGetEnoughMonths(): void 37 + { 38 + $user = User::factory()->create(); 39 + $targetCountry = Country::factory()->create()->getKey(); 40 + UserFactory::createRecentCountryHistory($user, $targetCountry, null); 41 + 42 + $this->assertSame($targetCountry, CountryChangeTarget::get($user)); 43 + } 44 + 45 + public function testGetEnoughMonthsMixed(): void 46 + { 47 + $user = User::factory()->create(); 48 + $targetCountry = Country::factory()->create()->getKey(); 49 + UserFactory::createRecentCountryHistory($user, $targetCountry, null); 50 + UserFactory::createRecentCountryHistory($user, null, CountryChangeTarget::maxMixedMonths()); 51 + 52 + $this->assertSame($targetCountry, CountryChangeTarget::get($user)); 53 + } 54 + 55 + public function testGetEnoughMonthsMixedTooMany(): void 56 + { 57 + $user = User::factory()->create(); 58 + $targetCountry = Country::factory()->create()->getKey(); 59 + UserFactory::createRecentCountryHistory($user, $targetCountry, null); 60 + UserFactory::createRecentCountryHistory($user, null, CountryChangeTarget::maxMixedMonths() + 1); 61 + 62 + $this->assertNull(CountryChangeTarget::get($user)); 63 + } 64 + 65 + public function testGetInRunningTournament(): void 66 + { 67 + $user = User::factory()->create(); 68 + $targetCountry = Country::factory()->create()->getKey(); 69 + UserFactory::createRecentCountryHistory($user, $targetCountry, null); 70 + $tournament = Tournament::factory()->create(); 71 + $tournament->registrations()->create([ 72 + 'user_id' => $user->getKey(), 73 + ]); 74 + 75 + $this->assertNull(CountryChangeTarget::get($user)); 76 + } 77 + 78 + public function testGetInPastTournament(): void 79 + { 80 + $user = User::factory()->create(); 81 + $targetCountry = Country::factory()->create()->getKey(); 82 + UserFactory::createRecentCountryHistory($user, $targetCountry, null); 83 + $tournament = Tournament::factory()->create(['end_date' => CarbonImmutable::now()->subMonths(1)]); 84 + $tournament->registrations()->create([ 85 + 'user_id' => $user->getKey(), 86 + ]); 87 + 88 + $this->assertSame($targetCountry, CountryChangeTarget::get($user)); 89 + } 90 + }
+58
tests/Libraries/User/CountryChangeTest.php
··· 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 + 6 + declare(strict_types=1); 7 + 8 + namespace Tests\Libraries\User; 9 + 10 + use App\Exceptions\InvariantException; 11 + use App\Libraries\User\CountryChange; 12 + use App\Models\Beatmap; 13 + use App\Models\Country; 14 + use App\Models\Score\Best\Model as ScoreBestModel; 15 + use App\Models\User; 16 + use Tests\TestCase; 17 + 18 + class CountryChangeTest extends TestCase 19 + { 20 + public function testDo(): void 21 + { 22 + $user = User::factory(); 23 + foreach (Beatmap::MODES as $ruleset => $_rulesetId) { 24 + $user = $user->withPlays(rand(1, 20), $ruleset); 25 + } 26 + $user = $user->create(); 27 + foreach (Beatmap::MODES as $ruleset => $_rulesetId) { 28 + ScoreBestModel 29 + ::getClass($ruleset) 30 + ::factory(['user_id' => $user, 'country_acronym' => $user->country_acronym]) 31 + ->count(rand(1, 5)) 32 + ->create(); 33 + } 34 + $targetCountry = Country::factory()->create()->getKey(); 35 + 36 + $this->expectCountChange(fn () => $user->accountHistories()->count(), 1); 37 + CountryChange::do($user, $targetCountry, 'test'); 38 + 39 + $user->refresh(); 40 + $this->assertSame($user->country_acronym, $targetCountry); 41 + foreach (Beatmap::MODES as $ruleset => $_rulesetId) { 42 + $this->assertSame($user->statistics($ruleset)->country_acronym, $targetCountry); 43 + 44 + foreach ($user->scoresBest($ruleset) as $score) { 45 + $this->assertSame($score->country_acronym, $targetCountry); 46 + } 47 + } 48 + } 49 + 50 + public function testDoInvalidCountry(): void 51 + { 52 + $user = User::factory()->create(); 53 + $oldCountry = $user->country_acronym; 54 + 55 + $this->expectException(InvariantException::class); 56 + CountryChange::do($user, '__', 'test'); 57 + } 58 + }
+11
tests/Models/ModelCompositePrimaryKeysTest.php
··· 17 17 use App\Models\LegacyMatch; 18 18 use App\Models\UserAchievement; 19 19 use App\Models\UserClient; 20 + use App\Models\UserCountryHistory; 20 21 use App\Models\UserDonation; 21 22 use App\Models\UserGroup; 22 23 use App\Models\UserRelation; ··· 211 212 ], 212 213 [], 213 214 ['user_id', [0, 1], 2], 215 + ], 216 + [ 217 + UserCountryHistory::class, 218 + [ 219 + 'user_id' => 0, 220 + 'year_month' => '2301', 221 + 'country_acronym' => 'JP', 222 + ], 223 + ['user_id' => 1], 224 + ['count', [1, 2], 3], 214 225 ], 215 226 [ 216 227 UserDonation::class,