This commit is contained in:
2026-10-03 14:33:08 +04:00
parent 6a73d760c4
commit 93a9442e1c
67 changed files with 941 additions and 698 deletions
+4 -4
View File
@@ -3,7 +3,7 @@
namespace Tests\Feature;
use App\Models\GameAccountUser;
use App\Services\AzerothCoreAccountService;
use App\Services\VMangosAccountService;
use App\Services\InviteCodeService;
use Illuminate\Support\Collection;
use Inertia\Testing\AssertableInertia as Assert;
@@ -28,7 +28,7 @@ class AdminAccountsFeatureTest extends TestCase
public function test_admin_can_open_accounts_page(): void
{
$this->mock(AzerothCoreAccountService::class, function (MockInterface $mock): void {
$this->mock(VMangosAccountService::class, function (MockInterface $mock): void {
$mock->shouldReceive('accountTypeStats')
->once()
->with(null)
@@ -87,7 +87,7 @@ class AdminAccountsFeatureTest extends TestCase
public function test_admin_can_open_bots_tab(): void
{
$this->mock(AzerothCoreAccountService::class, function (MockInterface $mock): void {
$this->mock(VMangosAccountService::class, function (MockInterface $mock): void {
$mock->shouldReceive('accountTypeStats')
->once()
->with(null)
@@ -137,7 +137,7 @@ class AdminAccountsFeatureTest extends TestCase
public function test_admin_can_update_access_level(): void
{
$this->mock(AzerothCoreAccountService::class, function (MockInterface $mock): void {
$this->mock(VMangosAccountService::class, function (MockInterface $mock): void {
$mock->shouldReceive('findUserById')
->once()
->with(12)
+2 -2
View File
@@ -4,7 +4,7 @@ namespace Tests\Feature;
use App\Models\BalanceTransaction;
use App\Models\GameAccountUser;
use App\Services\AzerothCoreAccountService;
use App\Services\VMangosAccountService;
use App\Services\BalanceService;
use Illuminate\Support\Collection;
use Inertia\Testing\AssertableInertia as Assert;
@@ -40,7 +40,7 @@ class AdminBalancesFeatureTest extends TestCase
$admin = $this->makeUser(3);
$player = $this->makeUser(0, 7, 'PLAYER');
$this->mock(AzerothCoreAccountService::class, function (MockInterface $mock) use ($player): void {
$this->mock(VMangosAccountService::class, function (MockInterface $mock) use ($player): void {
$mock->shouldReceive('findUserByUsername')->once()->with('PLAYER')->andReturn($player);
});
$this->mock(BalanceService::class, function (MockInterface $mock) use ($admin): void {
+6 -6
View File
@@ -15,8 +15,8 @@ class BalancePaymentsFeatureTest extends TestCase
parent::setUp();
config([
'database.default' => 'azerothcore_auth',
'database.connections.azerothcore_auth' => [
'database.default' => 'vmangos_auth',
'database.connections.vmangos_auth' => [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
@@ -38,7 +38,7 @@ class BalancePaymentsFeatureTest extends TestCase
'services.robokassa.tears_per_ruble' => 10,
]);
DB::purge('azerothcore_auth');
DB::purge('vmangos_auth');
$migration = require database_path('migrations/2026_07_19_000001_create_moonwell_balance_tables.php');
$migration->up();
}
@@ -60,12 +60,12 @@ class BalancePaymentsFeatureTest extends TestCase
$this->assertSame('1', $parameters['IsTest']);
$this->assertSame('1000.00', app(BalanceService::class)->balance(7));
$this->assertDatabaseCount('balance_transactions', 1, 'azerothcore_auth');
$this->assertDatabaseCount('balance_transactions', 1, 'vmangos_auth');
}
public function test_game_spending_is_atomic_and_idempotent(): void
{
DB::connection('azerothcore_auth')->table('account_balances')->insert([
DB::connection('vmangos_auth')->table('account_balances')->insert([
'account_id' => 7,
'balance' => '100.00',
'created_at' => now(),
@@ -78,7 +78,7 @@ class BalancePaymentsFeatureTest extends TestCase
$this->assertSame($first->id, $second->id);
$this->assertSame('74.50', $balances->balance(7));
$this->assertDatabaseCount('balance_transactions', 1, 'azerothcore_auth');
$this->assertDatabaseCount('balance_transactions', 1, 'vmangos_auth');
}
public function test_production_mode_uses_production_credentials_and_disables_test_flag(): void
+4 -4
View File
@@ -3,7 +3,7 @@
namespace Tests\Feature;
use App\Models\GameAccountUser;
use App\Services\AzerothCoreAccountService;
use App\Services\VMangosAccountService;
use App\Services\BalanceService;
use App\Services\GameClientDownloadService;
use App\Services\StoreAvailabilityService;
@@ -34,7 +34,7 @@ class CabinetFeatureTest extends TestCase
{
$user = $this->makeUser();
$this->mock(AzerothCoreAccountService::class, function (MockInterface $mock) use ($user): void {
$this->mock(VMangosAccountService::class, function (MockInterface $mock) use ($user): void {
$mock->shouldReceive('charactersForAccount')
->once()
->with($user->id)
@@ -90,7 +90,7 @@ class CabinetFeatureTest extends TestCase
{
$user = $this->makeUser();
$this->mock(AzerothCoreAccountService::class, function (MockInterface $mock) use ($user): void {
$this->mock(VMangosAccountService::class, function (MockInterface $mock) use ($user): void {
$mock->shouldReceive('validatePassword')
->once()
->with($user, 'OldPass123')
@@ -115,7 +115,7 @@ class CabinetFeatureTest extends TestCase
{
$user = $this->makeUser();
$this->mock(AzerothCoreAccountService::class, function (MockInterface $mock) use ($user): void {
$this->mock(VMangosAccountService::class, function (MockInterface $mock) use ($user): void {
$mock->shouldReceive('validatePassword')
->once()
->with($user, 'WrongPass123')
+11 -4
View File
@@ -4,7 +4,7 @@ namespace Tests\Feature;
use App\Exceptions\DuplicateGameAccountException;
use App\Exceptions\InviteCodeException;
use App\Services\AzerothCoreAccountRegistrar;
use App\Services\VMangosAccountRegistrar;
use App\Services\InviteCodeService;
use Closure;
use Mockery\MockInterface;
@@ -12,6 +12,13 @@ use Tests\TestCase;
class GameAccountRegistrationTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
config(['moonwell.registration.require_invite_code' => true]);
}
public function test_registration_form_submits_successfully(): void
{
$this->mock(InviteCodeService::class, function (MockInterface $mock): void {
@@ -21,7 +28,7 @@ class GameAccountRegistrationTest extends TestCase
->andReturnUsing(fn (string $code, string $username, Closure $callback): array => $callback());
});
$this->mock(AzerothCoreAccountRegistrar::class, function (MockInterface $mock): void {
$this->mock(VMangosAccountRegistrar::class, function (MockInterface $mock): void {
$mock->shouldReceive('register')
->once()
->with('playerone', 'player@example.com', 'Pass1234')
@@ -63,7 +70,7 @@ class GameAccountRegistrationTest extends TestCase
$this->mock(InviteCodeService::class, function (MockInterface $mock): void {
$mock->shouldNotReceive('redeemForRegistration');
});
$this->mock(AzerothCoreAccountRegistrar::class, function (MockInterface $mock): void {
$this->mock(VMangosAccountRegistrar::class, function (MockInterface $mock): void {
$mock->shouldReceive('register')
->once()
->with('playerone', 'player@example.com', 'Pass1234')
@@ -87,7 +94,7 @@ class GameAccountRegistrationTest extends TestCase
->andReturnUsing(fn (string $code, string $username, Closure $callback): array => $callback());
});
$this->mock(AzerothCoreAccountRegistrar::class, function (MockInterface $mock): void {
$this->mock(VMangosAccountRegistrar::class, function (MockInterface $mock): void {
$mock->shouldReceive('register')
->once()
->andThrow(DuplicateGameAccountException::forUsername());
+1 -1
View File
@@ -13,7 +13,7 @@ class LandingPageTest extends TestCase
$response->assertOk();
$response->assertSee('<title>MoonWell — Лунный колодец</title>', false);
$response->assertSee('<meta name="description" content="Пройди историю Азерота — от Классики до Короля-лича — в своём темпе, в одиночку или с небольшим отрядом.">', false);
$response->assertSee('<meta name="description" content="Вернись в классический Азерот — в одиночку или с небольшим отрядом.">', false);
$response->assertInertia(fn (Assert $page) => $page
->component('Landing/Main', false)
->where('realm.server_name', 'MoonWell')
+42 -94
View File
@@ -3,7 +3,7 @@
namespace Tests\Feature;
use App\Models\GameAccount;
use App\Services\AzerothCoreSrpService;
use App\Services\VMangosSrpService;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
@@ -11,115 +11,63 @@ use Tests\TestCase;
class LauncherGameTicketFeatureTest extends TestCase
{
private const string SESSION_TOKEN = 'launcher-session-access-token';
protected function setUp(): void
{
parent::setUp();
config(['auth.guards.api' => [
'driver' => 'session',
'provider' => 'game_accounts_eloquent',
]]);
config([
'app.key' => 'base64:'.base64_encode(str_repeat('m', 32)),
'auth.guards.api' => [
'driver' => 'session',
'provider' => 'game_accounts_eloquent',
],
'moonwell.auth_connection' => 'azerothcore_auth',
'moonwell.launcher.game_ticket_ttl' => 60,
'moonwell.launcher.client_build' => 12340,
'database.connections.azerothcore_auth' => [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
],
'moonwell.auth_connection' => 'vmangos_auth',
'database.connections.vmangos_auth' => ['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => ''],
]);
DB::purge('azerothcore_auth');
Schema::connection('azerothcore_auth')->create('launcher_ticket', function (Blueprint $table): void {
DB::purge('vmangos_auth');
Schema::connection('vmangos_auth')->create('launcher_ticket', function (Blueprint $table): void {
$table->unsignedInteger('account_id')->primary();
$table->binary('generation_id')->unique();
$table->binary('generation_id');
$table->binary('srp_salt');
$table->binary('srp_verifier');
$table->unsignedSmallInteger('client_build');
$table->binary('launcher_session_hash')->nullable();
$table->timestamp('issued_at');
$table->timestamp('expires_at');
$table->binary('launcher_session_hash');
$table->dateTime('issued_at');
$table->dateTime('expires_at');
$table->binary('claim_nonce')->nullable();
});
}
public function test_authenticated_launcher_receives_ticket_and_database_only_stores_srp_data(): void
{
$account = $this->authenticate(7, 'PlayerOne');
$response = $this->postJson('/api/launcher/game-ticket', ['client_build' => 12340], [
'Authorization' => 'Bearer '.self::SESSION_TOKEN,
])->assertOk()->assertHeader('Cache-Control', 'no-store, private');
$ticket = $response->json('ticket');
$response->assertJsonPath('account', 'PLAYERONE');
$this->assertMatchesRegularExpression('/^[A-Z0-9]{16}$/', $ticket);
$this->assertNotNull($response->json('expires_at'));
$row = DB::connection('azerothcore_auth')->table('launcher_ticket')->where('account_id', $account->id)->first();
$this->assertNotNull($row);
$this->assertSame(16, strlen($row->generation_id));
$this->assertSame(32, strlen($row->srp_salt));
$this->assertSame(32, strlen($row->srp_verifier));
$this->assertSame(12340, $row->client_build);
$this->assertSame(hash('sha256', self::SESSION_TOKEN, true), $row->launcher_session_hash);
$this->assertTrue(app(AzerothCoreSrpService::class)->credentialsMatch(
'PLAYERONE',
$ticket,
bin2hex($row->srp_salt),
bin2hex($row->srp_verifier),
));
$this->assertStringNotContainsString($ticket, serialize($row));
}
public function test_new_generation_revokes_previous_ticket(): void
{
$account = $this->authenticate(9, 'DevPlayer');
$headers = ['Authorization' => 'Bearer '.self::SESSION_TOKEN];
$firstTicket = $this->postJson('/api/launcher/game-ticket', ['client_build' => 12340], $headers)
->assertOk()->json('ticket');
$firstGeneration = DB::connection('azerothcore_auth')->table('launcher_ticket')
->where('account_id', $account->id)->value('generation_id');
$secondTicket = $this->postJson('/api/launcher/game-ticket', ['client_build' => 12340], $headers)
->assertOk()->json('ticket');
$row = DB::connection('azerothcore_auth')->table('launcher_ticket')
->where('account_id', $account->id)->first();
$srp = app(AzerothCoreSrpService::class);
$this->assertNotSame($firstTicket, $secondTicket);
$this->assertNotSame($firstGeneration, $row->generation_id);
$this->assertFalse($srp->credentialsMatch('DEVPLAYER', $firstTicket, bin2hex($row->srp_salt), bin2hex($row->srp_verifier)));
$this->assertTrue($srp->credentialsMatch('DEVPLAYER', $secondTicket, bin2hex($row->srp_salt), bin2hex($row->srp_verifier)));
}
public function test_ticket_endpoint_requires_launcher_authentication(): void
{
$this->postJson('/api/launcher/game-ticket', ['client_build' => 12340])->assertUnauthorized();
}
public function test_ticket_endpoint_rejects_wrong_client_build(): void
{
$this->authenticate(10, 'PlayerTwo');
$this->postJson('/api/launcher/game-ticket', ['client_build' => 99999], [
'Authorization' => 'Bearer '.self::SESSION_TOKEN,
])->assertUnprocessable();
$this->assertSame(0, DB::connection('azerothcore_auth')->table('launcher_ticket')->count());
}
private function authenticate(int $id, string $username): GameAccount
public function test_game_ticket_is_issued_and_replaces_previous_generation(): void
{
$account = new GameAccount;
$account->id = $id;
$account->username = $username;
$account->id = 7;
$account->username = 'PLAYERONE';
$this->actingAs($account, 'api');
return $account;
$response = $this->withToken('launcher-token')->postJson('/api/launcher/game-ticket', ['client_build' => 5875])->assertOk();
$first = $response->json('ticket');
$this->assertMatchesRegularExpression('/^[A-Z0-9]{16}$/', $first);
$this->assertSame('PLAYERONE', $response->json('account'));
$row = DB::connection('vmangos_auth')->table('launcher_ticket')->where('account_id', 7)->first();
$this->assertTrue(app(VMangosSrpService::class)->credentialsMatch('PLAYERONE', $first, strtoupper(bin2hex($row->srp_salt)), strtoupper(bin2hex($row->srp_verifier))));
$generation = $row->generation_id;
$this->withToken('launcher-token')->postJson('/api/launcher/game-ticket', ['client_build' => 5875])->assertOk();
$this->assertSame(1, DB::connection('vmangos_auth')->table('launcher_ticket')->count());
$this->assertNotSame($generation, DB::connection('vmangos_auth')->table('launcher_ticket')->value('generation_id'));
}
public function test_ticket_rejects_wrong_build(): void
{
$account = new GameAccount;
$account->id = 7;
$account->username = 'PLAYERONE';
$this->actingAs($account, 'api');
$this->withToken('launcher-token')->postJson('/api/launcher/game-ticket', ['client_build' => 12340])->assertUnprocessable();
}
public function test_game_ticket_still_requires_launcher_authentication(): void
{
$this->postJson('/api/launcher/game-ticket', ['client_build' => 5875])->assertUnauthorized();
}
}
+11 -8
View File
@@ -16,18 +16,18 @@ class LauncherRealmsFeatureTest extends TestCase
parent::setUp();
config([
'moonwell.auth_connection' => 'azerothcore_auth',
'moonwell.auth_connection' => 'vmangos_auth',
'moonwell.launcher.realm_status_cache' => 0,
'moonwell.launcher.realm_status_host' => null,
'database.connections.azerothcore_auth' => [
'database.connections.vmangos_auth' => [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
],
]);
DB::purge('azerothcore_auth');
DB::purge('vmangos_auth');
Schema::connection('azerothcore_auth')->create('realmlist', function (Blueprint $table): void {
Schema::connection('vmangos_auth')->create('realmlist', function (Blueprint $table): void {
$table->unsignedInteger('id')->primary();
$table->string('name');
$table->string('address');
@@ -36,13 +36,14 @@ class LauncherRealmsFeatureTest extends TestCase
$table->unsignedInteger('flag')->default(0);
$table->unsignedInteger('timezone')->default(0);
$table->float('population')->default(0);
$table->unsignedInteger('gamebuild')->default(12340);
$table->unsignedInteger('gamebuild_min')->default(5875);
$table->unsignedInteger('gamebuild_max')->default(5875);
});
}
public function test_launcher_can_get_realms_and_their_status_without_authentication(): void
{
DB::connection('azerothcore_auth')->table('realmlist')->insert([
DB::connection('vmangos_auth')->table('realmlist')->insert([
[
'id' => 1,
'name' => 'MoonWell',
@@ -52,7 +53,8 @@ class LauncherRealmsFeatureTest extends TestCase
'flag' => 0,
'timezone' => 4,
'population' => 0.5,
'gamebuild' => 12340,
'gamebuild_min' => 5875,
'gamebuild_max' => 5875,
],
[
'id' => 2,
@@ -63,7 +65,8 @@ class LauncherRealmsFeatureTest extends TestCase
'flag' => 0,
'timezone' => 4,
'population' => 0,
'gamebuild' => 12340,
'gamebuild_min' => 5875,
'gamebuild_max' => 5875,
],
]);
$this->mock(RealmStatusProbe::class, function (MockInterface $mock): void {
@@ -3,7 +3,7 @@
namespace Tests\Feature;
use App\Exceptions\DuplicateGameAccountException;
use App\Services\AzerothCoreAccountRegistrar;
use App\Services\VMangosAccountRegistrar;
use App\Services\InviteCodeService;
use Closure;
use Mockery\MockInterface;
@@ -26,7 +26,7 @@ class LauncherRegistrationFeatureTest extends TestCase
->with('INVITE-1234', 'playerone', \Mockery::type(Closure::class))
->andReturnUsing(fn (string $code, string $username, Closure $callback): array => $callback());
});
$this->mock(AzerothCoreAccountRegistrar::class, function (MockInterface $mock): void {
$this->mock(VMangosAccountRegistrar::class, function (MockInterface $mock): void {
$mock->shouldReceive('register')
->once()
->with('playerone', 'player@example.com', 'Pass1234')
@@ -77,7 +77,7 @@ class LauncherRegistrationFeatureTest extends TestCase
->once()
->andReturnUsing(fn (string $code, string $username, Closure $callback): array => $callback());
});
$this->mock(AzerothCoreAccountRegistrar::class, function (MockInterface $mock): void {
$this->mock(VMangosAccountRegistrar::class, function (MockInterface $mock): void {
$mock->shouldReceive('register')
->once()
->andThrow(DuplicateGameAccountException::forUsername());
@@ -0,0 +1,128 @@
<?php
namespace Tests\Feature;
use App\Services\VMangosAccountRegistrar;
use App\Services\VMangosAccountService;
use App\Services\VMangosSrpService;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Tests\TestCase;
class VMangosAccountIntegrationTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
config([
'moonwell.auth_connection' => 'vmangos_auth',
'moonwell.characters_connection' => 'vmangos_characters',
'moonwell.registration.realm_id' => -1,
'moonwell.registration.gmlevel' => 0,
'database.connections.vmangos_auth' => ['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => ''],
'database.connections.vmangos_characters' => ['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => ''],
]);
DB::purge('vmangos_auth');
DB::purge('vmangos_characters');
Schema::connection('vmangos_auth')->create('account', function (Blueprint $table): void {
$table->increments('id');
$table->string('username', 32)->unique();
$table->text('s');
$table->text('v');
$table->unsignedTinyInteger('expansion')->default(0);
$table->string('email')->nullable();
$table->dateTime('joindate');
$table->dateTime('last_login')->nullable();
$table->boolean('locked')->default(false);
$table->boolean('online')->default(false);
$table->string('sessionkey')->nullable();
});
Schema::connection('vmangos_auth')->create('realmlist', function (Blueprint $table): void {
$table->increments('id');
});
Schema::connection('vmangos_auth')->create('realmcharacters', function (Blueprint $table): void {
$table->unsignedInteger('realmid');
$table->unsignedInteger('acctid');
$table->unsignedTinyInteger('numchars');
$table->primary(['realmid', 'acctid']);
});
Schema::connection('vmangos_auth')->create('account_access', function (Blueprint $table): void {
$table->unsignedInteger('id');
$table->integer('RealmID');
$table->unsignedTinyInteger('gmlevel');
$table->primary(['id', 'RealmID']);
});
Schema::connection('vmangos_auth')->create('site_credentials', function (Blueprint $table): void {
$table->unsignedInteger('account_id')->primary();
$table->unsignedInteger('legacy_account_id')->nullable()->unique();
$table->char('salt_hex', 64);
$table->char('verifier_hex', 64);
$table->timestamps();
});
Schema::connection('vmangos_characters')->create('characters', function (Blueprint $table): void {
$table->unsignedInteger('guid')->primary();
$table->unsignedInteger('account');
$table->string('name');
$table->unsignedTinyInteger('race');
$table->unsignedTinyInteger('class');
$table->unsignedTinyInteger('gender');
$table->unsignedTinyInteger('level');
$table->boolean('online');
$table->unsignedInteger('money');
$table->unsignedInteger('played_time_total');
});
}
public function test_srp_matches_vmangos_core_control_vector(): void
{
$srp = app(VMangosSrpService::class);
$salt = 'D04E6342FE6CB5FAE54C6182F885778D0AEFE4BFCDDC6B5C7DF7DC25FF6E3C2D';
$verifier = '46FD48476D925C4422DD1761C299C79FD6C92B5243E4F3C6C62576C0DABD8260';
$this->assertSame($verifier, $srp->makeVerifierHex('VMANGOS', '4EVER', $salt));
$this->assertTrue($srp->credentialsMatch('vmangos', '4ever', $salt, $verifier));
$this->assertFalse($srp->credentialsMatch('vmangos', 'wrong', $salt, $verifier));
}
public function test_registration_login_password_access_and_characters_use_vmangos_schema(): void
{
DB::connection('vmangos_auth')->table('realmlist')->insert([['id' => 1], ['id' => 2]]);
$registered = app(VMangosAccountRegistrar::class)->register('PlayerOne', 'player@example.com', 'Pass1234');
$id = $registered['id'];
$this->assertSame('PLAYERONE', $registered['username']);
$this->assertDatabaseHas('account', ['id' => $id, 'username' => 'PLAYERONE', 'expansion' => 0], 'vmangos_auth');
$this->assertSame(2, DB::connection('vmangos_auth')->table('realmcharacters')->where('acctid', $id)->count());
$this->assertDatabaseHas('account_access', ['id' => $id, 'RealmID' => -1, 'gmlevel' => 0], 'vmangos_auth');
$gameCredential = DB::connection('vmangos_auth')->table('account')->where('id', $id)->first();
$this->assertFalse(app(VMangosSrpService::class)->credentialsMatch('PLAYERONE', 'Pass1234', $gameCredential->s, $gameCredential->v));
$accounts = app(VMangosAccountService::class);
$user = $accounts->findUserByUsername('playerone');
$this->assertNotNull($user);
$this->assertTrue($accounts->validatePassword($user, 'Pass1234'));
$this->assertFalse($accounts->validatePassword($user, 'Wrong1234'));
DB::connection('vmangos_auth')->table('account')->where('id', $id)->update(['last_login' => '0000-00-00 00:00:00']);
$this->assertSame('—', $accounts->findUserById($id)->lastLoginLabel());
$accounts->updateAccessLevel($id, 3);
$this->assertSame(3, $accounts->findUserById($id)->gmLevel);
DB::connection('vmangos_characters')->table('characters')->insert([
'guid' => 11, 'account' => $id, 'name' => 'Hero', 'race' => 1, 'class' => 1,
'gender' => 0, 'level' => 60, 'online' => 1, 'money' => 12345, 'played_time_total' => 7200,
]);
$character = $accounts->charactersForAccount($id)->first();
$this->assertSame('Hero', $character['name']);
$this->assertSame('2ч 0м', $character['played']);
$accounts->updatePassword($user, 'NewPass123');
$fresh = $accounts->findUserById($id);
$this->assertFalse($accounts->validatePassword($fresh, 'Pass1234'));
$this->assertTrue($accounts->validatePassword($fresh, 'NewPass123'));
$this->assertSame($gameCredential->v, DB::connection('vmangos_auth')->table('account')->where('id', $id)->value('v'));
}
}