limiterKeys() as $key) { RateLimiter::clear($key); } } protected function tearDown(): void { foreach ($this->limiterKeys() as $key) { RateLimiter::clear($key); } parent::tearDown(); } public function test_create_user_invite_returns_false_and_skips_db_when_user_cap_exceeded(): void { session(['userdata' => ['id' => self::INVITER_ID, 'name' => 'Inviter', 'mail' => 'inviter@example.com']]); // Exhaust the per-user hourly cap (default 10) on the exact key the service computes. [$userKey] = $this->limiterKeys(); for ($i = 0; $i < 10; $i++) { RateLimiter::hit($userKey, 3600); } // The DB layer must never be touched once the cap is hit. $userRepo = $this->createMock(UserRepository::class); $userRepo->expects($this->never())->method('addUser'); $service = new Users( $userRepo, $this->createMock(LanguageCore::class), $this->createMock(ProjectRepository::class), $this->createMock(ClientRepository::class), $this->createMock(AuthService::class), $this->createMock(Files::class), $this->createMock(Avatarcreator::class), $this->createMock(SettingService::class), $this->createMock(ThemeCore::class), $this->createMock(ProjectService::class), ); $result = $service->createUserInvite([ 'user' => 'newuser@example.com', 'firstname' => 'New', 'lastname' => 'User', 'role' => '20', ]); $this->assertFalse($result, 'createUserInvite must return false once the invite cap is exceeded'); } /** * The user + tenant limiter keys, computed exactly as Users::invitesRateLimited() does. * * @return array{0: string, 1: string} */ private function limiterKeys(): array { $scope = defined('BASE_URL') ? BASE_URL : 'default'; return [ 'invites:'.$scope.':user:'.self::INVITER_ID, 'invites:'.$scope.':tenant', ]; } }