create([ 'contact_email' => 'door-test-tenant@example.ru', ]); $adminId = DB::table('saas_admin_users')->insertGetId([ 'email' => 'admin-door-test@liderra.ru', 'full_name' => 'Door Test Admin', 'password_hash' => '$2y$04$dummy-hash-for-test', 'role' => 'support', 'is_active' => true, 'sso_provider' => 'local', 'is_break_glass' => false, ]); $resp = $this->postJson('/api/admin/impersonation/init', [ 'tenant_id' => $tenant->id, 'requested_by' => $adminId, 'reason' => str_repeat('тест основания impersonation ', 2), ]); $resp->assertOk(); Mail::assertQueued(ImpersonationCodeMail::class, fn ($m) => $m->hasTo($tenant->contact_email)); }); it('verify входит как пользователь тенанта и возвращает машинный ключ', function () { $tenant = Tenant::factory()->create([ 'contact_email' => 'door-verify-test-tenant@example.ru', ]); $owner = User::factory()->create([ 'tenant_id' => $tenant->id, 'is_active' => true, ]); $adminId = DB::table('saas_admin_users')->insertGetId([ 'email' => 'admin-verify-test@liderra.ru', 'full_name' => 'Verify Test Admin', 'password_hash' => '$2y$04$dummy-hash-for-test', 'role' => 'support', 'is_active' => true, 'sso_provider' => 'local', 'is_break_glass' => false, ]); $init = $this->postJson('/api/admin/impersonation/init', [ 'tenant_id' => $tenant->id, 'requested_by' => $adminId, 'reason' => str_repeat('основание для теста impersonation ', 2), ])->assertOk()->json(); $verify = $this->postJson('/api/admin/impersonation/verify', [ 'token_id' => $init['token_id'], 'code' => $init['_dev_plain_code'], ])->assertOk()->json(); expect($verify)->toHaveKey('machine_token'); expect($verify['machine_token'])->toStartWith('lpimp_'.$init['token_id'].'_'); $this->assertAuthenticatedAs($owner); $token = ImpersonationToken::on('pgsql_supplier')->find($init['token_id']); expect($token->used_at)->not->toBeNull(); expect($token->session_token_hash)->not->toBeNull(); }); it('сессия старше 60 минут авто-истекает', function () { $tenant = Tenant::factory()->create([ 'contact_email' => 'door-expiry-test-tenant-'.uniqid().'@example.ru', ]); User::factory()->create([ 'tenant_id' => $tenant->id, 'is_active' => true, ]); $adminId = DB::table('saas_admin_users')->insertGetId([ 'email' => 'admin-expiry-'.uniqid().'@liderra.ru', 'full_name' => 'Expiry Test Admin', 'password_hash' => '$2y$04$dummy-hash-for-test', 'role' => 'support', 'is_active' => true, 'sso_provider' => 'local', 'is_break_glass' => false, ]); $init = $this->postJson('/api/admin/impersonation/init', [ 'tenant_id' => $tenant->id, 'requested_by' => $adminId, 'reason' => str_repeat('основание expiry теста impersonation ', 2), ])->assertOk()->json(); $this->postJson('/api/admin/impersonation/verify', [ 'token_id' => $init['token_id'], 'code' => $init['_dev_plain_code'], ])->assertOk(); // до истечения — me работает $this->getJson('/api/auth/me')->assertOk(); $this->travel(61)->minutes(); $this->getJson('/api/auth/me')->assertUnauthorized(); }); it('me отдаёт impersonation-контекст, leave завершает и разлогинивает', function () { Mail::fake(); $tenant = Tenant::factory()->create([ 'contact_email' => 'door-leave-test-tenant-'.uniqid().'@example.ru', ]); $owner = User::factory()->create([ 'tenant_id' => $tenant->id, 'is_active' => true, ]); $adminId = DB::table('saas_admin_users')->insertGetId([ 'email' => 'admin-leave-'.uniqid().'@liderra.ru', 'full_name' => 'Leave Test Admin', 'password_hash' => '$2y$04$dummy-hash-for-test', 'role' => 'support', 'is_active' => true, 'sso_provider' => 'local', 'is_break_glass' => false, ]); $init = $this->postJson('/api/admin/impersonation/init', [ 'tenant_id' => $tenant->id, 'requested_by' => $adminId, 'reason' => str_repeat('основание leave теста impersonation ', 2), ])->assertOk()->json(); $this->postJson('/api/admin/impersonation/verify', [ 'token_id' => $init['token_id'], 'code' => $init['_dev_plain_code'], ])->assertOk(); $imp = $this->getJson('/api/auth/me')->assertOk()->json('user.impersonation'); expect($imp['active'])->toBeTrue(); $this->postJson('/api/impersonation/leave')->assertOk(); $this->getJson('/api/auth/me')->assertUnauthorized(); });