tenant = Tenant::factory()->create(); }); test('login_success пишет запись в auth_log', function () { User::factory()->create([ 'tenant_id' => $this->tenant->id, 'email' => 'log-success@example.ru', 'password_hash' => Hash::make('right-password'), ]); $this->postJson('/api/auth/login', [ 'email' => 'log-success@example.ru', 'password' => 'right-password', ])->assertOk(); $row = DB::table('auth_log') ->where('email', 'log-success@example.ru') ->where('event', 'login_success') ->first(); expect($row)->not->toBeNull(); expect($row->actor_type)->toBe('tenant_user'); expect($row->tenant_id)->toBe($this->tenant->id); }); test('login_failed (wrong password) пишет в auth_log с failure_reason=invalid_password', function () { User::factory()->create([ 'tenant_id' => $this->tenant->id, 'email' => 'log-fail@example.ru', 'password_hash' => Hash::make('right-password'), ]); $this->postJson('/api/auth/login', [ 'email' => 'log-fail@example.ru', 'password' => 'wrong-pass-attempt', ])->assertStatus(422); $row = DB::table('auth_log') ->where('email', 'log-fail@example.ru') ->where('event', 'login_failed') ->first(); expect($row)->not->toBeNull(); expect($row->failure_reason)->toBe('invalid_password'); }); test('login_failed для unknown email пишет с failure_reason=unknown_email', function () { $this->postJson('/api/auth/login', [ 'email' => 'nobody@example.ru', 'password' => 'wrong-pass-attempt', ])->assertStatus(422); $row = DB::table('auth_log') ->where('email', 'nobody@example.ru') ->where('event', 'login_failed') ->first(); expect($row)->not->toBeNull(); expect($row->failure_reason)->toBe('unknown_email'); expect($row->user_id)->toBeNull(); }); test('IP-lockout: после 10 login_failed с одного IP за час — следующий → 429', function () { User::factory()->create([ 'tenant_id' => $this->tenant->id, 'email' => 'ip-lockout-victim@example.ru', 'password_hash' => Hash::make('right-password'), ]); // Эмулируем 10 неудачных попыток с разных email с одного IP // (через прямой INSERT в auth_log, чтобы не триггерить email-rate-limit). for ($i = 0; $i < 10; $i++) { DB::table('auth_log')->insert([ 'actor_type' => 'tenant_user', 'event' => 'login_failed', 'email' => "victim{$i}@example.ru", 'ip_address' => '127.0.0.1', 'failure_reason' => 'unknown_email', 'created_at' => now()->subMinutes(5), ]); } // Следующий login (даже с правильным паролем) → 429. $r = $this->postJson('/api/auth/login', [ 'email' => 'ip-lockout-victim@example.ru', 'password' => 'right-password', ]); $r->assertStatus(429); expect($r->json('message'))->toContain('с этого IP'); expect($r->headers->get('Retry-After'))->toBe('3600'); }); test('IP-lockout не срабатывает на 9 неудач (под порогом)', function () { User::factory()->create([ 'tenant_id' => $this->tenant->id, 'email' => 'ok@example.ru', 'password_hash' => Hash::make('right-password'), ]); for ($i = 0; $i < 9; $i++) { DB::table('auth_log')->insert([ 'actor_type' => 'tenant_user', 'event' => 'login_failed', 'email' => "any{$i}@example.ru", 'ip_address' => '127.0.0.1', 'failure_reason' => 'unknown_email', 'created_at' => now()->subMinutes(5), ]); } $this->postJson('/api/auth/login', [ 'email' => 'ok@example.ru', 'password' => 'right-password', ])->assertOk(); }); test('IP-lockout: окно 1 час — старые записи (>1ч) не блокируют', function () { User::factory()->create([ 'tenant_id' => $this->tenant->id, 'email' => 'old-fails@example.ru', 'password_hash' => Hash::make('right-password'), ]); // 15 неудач, но старше часа → не учитываются. for ($i = 0; $i < 15; $i++) { DB::table('auth_log')->insert([ 'actor_type' => 'tenant_user', 'event' => 'login_failed', 'email' => "old{$i}@example.ru", 'ip_address' => '127.0.0.1', 'failure_reason' => 'unknown_email', 'created_at' => now()->subHours(2), ]); } $this->postJson('/api/auth/login', [ 'email' => 'old-fails@example.ru', 'password' => 'right-password', ])->assertOk(); });