'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->unsignedBigInteger('web_user_id')->nullable()->unique(); $table->char('salt_hex', 64)->nullable(); $table->char('verifier_hex', 64)->nullable(); $table->timestamps(); }); Schema::connection('vmangos_auth')->create('launcher_ticket', function (Blueprint $table): void { $table->unsignedInteger('account_id')->primary(); }); 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']); $webUser = User::where('name', 'PLAYERONE')->firstOrFail(); $this->assertTrue(Hash::check('Pass1234', $webUser->password)); $this->assertDatabaseHas('site_credentials', ['account_id' => $id, 'web_user_id' => $webUser->id, 'salt_hex' => null, 'verifier_hex' => null], 'vmangos_auth'); $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')); $this->assertFalse($accounts->validatePassword($user, 'pass1234')); $this->assertSame($webUser->id, $user->webUserId); 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->assertTrue(Hash::check('NewPass123', $webUser->fresh()->password)); $this->assertSame($gameCredential->v, DB::connection('vmangos_auth')->table('account')->where('id', $id)->value('v')); } public function test_import_reads_web_users_and_preserves_web_hashes_and_repeated_game_credentials(): void { DB::connection('vmangos_auth')->table('realmlist')->insert(['id' => 1]); $webUser = User::create(['name' => 'Existing', 'email' => 'existing@example.com', 'password' => 'ExistingWebPassword123']); $hash = $webUser->password; $this->artisan('vmangos:import-web-accounts', ['--dry-run' => true]) ->expectsOutput('Created: 1; linked: 0; already imported: 0.')->assertSuccessful(); $this->assertSame(0, DB::connection('vmangos_auth')->table('account')->count()); $this->artisan('vmangos:import-web-accounts')->assertSuccessful(); $accounts = app(VMangosAccountService::class); $user = $accounts->findUserByUsername('existing'); $this->assertSame($webUser->id, $user->webUserId); $this->assertSame($hash, $webUser->fresh()->password); $this->assertTrue($accounts->validatePassword($user, 'ExistingWebPassword123')); $game = DB::connection('vmangos_auth')->table('account')->where('id', $user->id)->first(); $this->assertFalse(app(VMangosSrpService::class)->credentialsMatch('EXISTING', 'ExistingWebPassword123', $game->s, $game->v)); $this->assertDatabaseHas('account_access', ['id' => $user->id, 'RealmID' => -1, 'gmlevel' => 0], 'vmangos_auth'); $this->assertDatabaseHas('realmcharacters', ['acctid' => $user->id, 'realmid' => 1], 'vmangos_auth'); $this->artisan('vmangos:import-web-accounts') ->expectsOutput('Created: 0; linked: 0; already imported: 1.')->assertSuccessful(); $this->assertSame($game->v, DB::connection('vmangos_auth')->table('account')->where('id', $user->id)->value('v')); $this->post('/login', ['username' => 'existing', 'password' => 'ExistingWebPassword123'])->assertRedirect(); $this->assertAuthenticated(); } public function test_linking_existing_account_keeps_privileges_and_revokes_sessions(): void { $target = DB::connection('vmangos_auth'); $id = $target->table('account')->insertGetId([ 'username' => 'ADMIN', 'email' => 'admin@example.com', 's' => 'old', 'v' => 'old', 'sessionkey' => 'old-session', 'joindate' => '2025-01-01 00:00:00', ]); $target->table('account_access')->insert(['id' => $id, 'RealmID' => -1, 'gmlevel' => 3]); $target->table('launcher_ticket')->insert(['account_id' => $id]); [$salt, $verifier] = app(AzerothCoreSrpService::class)->makeRegistrationData('ADMIN', 'OldPass123'); $target->table('site_credentials')->insert([ 'account_id' => $id, 'legacy_account_id' => 8, 'salt_hex' => $salt, 'verifier_hex' => $verifier, ]); $webUser = User::create(['name' => 'Admin', 'email' => 'admin@example.com', 'password' => 'WebPass123']); DB::table('oauth_access_tokens')->insert([ 'id' => 'old-token', 'user_id' => 99, 'client_id' => 'old-client', 'scopes' => '[]', 'revoked' => false, ]); DB::table('oauth_refresh_tokens')->insert(['id' => 'old-refresh', 'access_token_id' => 'old-token', 'revoked' => false]); $this->artisan('vmangos:import-web-accounts', ['--dry-run' => true])->assertSuccessful(); $this->assertDatabaseHas('oauth_access_tokens', ['id' => 'old-token', 'revoked' => false]); $this->artisan('vmangos:import-web-accounts') ->expectsOutput('Created: 0; linked: 1; already imported: 0.')->assertSuccessful(); $accounts = app(VMangosAccountService::class); $user = $accounts->findUserByUsername('admin'); $this->assertSame($id, $user->id); $this->assertSame($webUser->id, $user->webUserId); $this->assertSame(3, $user->gmLevel); $this->assertTrue($accounts->validatePassword($user, 'WebPass123')); $this->assertFalse($accounts->validatePassword($user, 'OldPass123')); $this->assertDatabaseHas('site_credentials', ['account_id' => $id, 'salt_hex' => null, 'verifier_hex' => null], 'vmangos_auth'); $this->assertNull($target->table('account')->where('id', $id)->value('sessionkey')); $this->assertSame(0, $target->table('launcher_ticket')->count()); $this->assertDatabaseHas('oauth_access_tokens', ['id' => 'old-token', 'revoked' => true]); $this->assertDatabaseHas('oauth_refresh_tokens', ['id' => 'old-refresh', 'revoked' => true]); } public function test_import_conflict_does_not_create_other_users(): void { User::create(['name' => 'FirstUser', 'email' => 'first@example.com', 'password' => 'WebPass123']); User::create(['name' => 'SecondUser', 'email' => 'second@example.com', 'password' => 'WebPass123']); DB::connection('vmangos_auth')->table('account')->insert([ 'username' => 'SECONDUSER', 'email' => 'someone-else@example.com', 's' => 'old', 'v' => 'old', 'joindate' => '2025-01-01 00:00:00', ]); $this->artisan('vmangos:import-web-accounts')->assertFailed(); $this->assertSame(1, DB::connection('vmangos_auth')->table('account')->count()); $this->assertSame(0, DB::connection('vmangos_auth')->table('site_credentials')->count()); } public function test_import_rejects_duplicate_normalized_web_names(): void { User::create(['name' => 'Duplicate', 'email' => 'first@example.com', 'password' => 'WebPass123']); User::create(['name' => 'duplicate', 'email' => 'second@example.com', 'password' => 'WebPass123']); $this->artisan('vmangos:import-web-accounts')->assertFailed(); $this->assertSame(0, DB::connection('vmangos_auth')->table('account')->count()); } public function test_missing_web_user_cannot_use_legacy_password_fallback(): void { $result = app(VMangosAccountRegistrar::class)->register('PlayerOne', 'player@example.com', 'Pass1234'); $accounts = app(VMangosAccountService::class); $user = $accounts->findUserById($result['id']); User::findOrFail($user->webUserId)->delete(); $this->assertFalse($accounts->validatePassword($user, 'Pass1234')); } public function test_registration_duplicate_web_email_does_not_leave_game_account(): void { User::create(['name' => 'Existing', 'email' => 'player@example.com', 'password' => 'Pass1234']); try { app(VMangosAccountRegistrar::class)->register('PlayerOne', 'player@example.com', 'Pass1234'); $this->fail('Expected duplicate email to be rejected.'); } catch (DuplicateGameAccountException $exception) { $this->assertSame(0, DB::connection('vmangos_auth')->table('account')->count()); $this->assertSame(1, User::count()); } } public function test_vmangos_dates_use_configured_timezone_in_profile_and_admin(): void { config(['moonwell.timezone' => 'Europe/Samara']); $result = app(VMangosAccountRegistrar::class)->register('PlayerOne', 'player@example.com', 'Pass1234'); DB::connection('vmangos_auth')->table('account')->where('id', $result['id'])->update([ 'joindate' => '2026-10-03 22:30:00', 'last_login' => '2026-10-04 01:15:00', ]); $accounts = app(VMangosAccountService::class); $user = $accounts->findUserById($result['id']); $this->assertSame('04.10.2026 02:30', $user->joinedAtLabel()); $this->assertSame('04.10.2026 05:15', $user->lastLoginLabel()); $row = $accounts->listAccounts()->first(); $this->assertSame('04.10.2026 02:30', $row['joined_at']); $this->assertSame('04.10.2026 05:15', $row['last_login_at']); } }