feat(security): add session timeout + transaction wrapping (B1)
Session timeout: configurable idle (default 30min) and absolute (default 8h) timeouts via DB settings, enforced in web/index.php on every request. Timestamps set at login in action layer; graceful fallback for pre-existing sessions. Transaction wrapping: UserAccountService.createFromAdmin/register, roles create/edit, and permissions edit now wrap multi-step DB operations in transactions to guarantee atomicity. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
98
tests/Service/Settings/SettingsSessionGatewayTest.php
Normal file
98
tests/Service/Settings/SettingsSessionGatewayTest.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Settings;
|
||||
|
||||
use MintyPHP\Repository\Settings\SettingRepositoryInterface;
|
||||
use MintyPHP\Service\Settings\SettingsMetadataGateway;
|
||||
use MintyPHP\Service\Settings\SettingsSessionGateway;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class SettingsSessionGatewayTest extends TestCase
|
||||
{
|
||||
public function testGetSessionIdleTimeoutMinutesReturnsFallbackWhenNotSet(): void
|
||||
{
|
||||
$gateway = $this->createGateway(null);
|
||||
$this->assertSame(30, $gateway->getSessionIdleTimeoutMinutes());
|
||||
}
|
||||
|
||||
public function testGetSessionAbsoluteTimeoutHoursReturnsFallbackWhenNotSet(): void
|
||||
{
|
||||
$gateway = $this->createGateway(null);
|
||||
$this->assertSame(8, $gateway->getSessionAbsoluteTimeoutHours());
|
||||
}
|
||||
|
||||
public function testGetSessionIdleTimeoutMinutesReturnsStoredValue(): void
|
||||
{
|
||||
$gateway = $this->createGateway('15', 'session_idle_timeout_minutes');
|
||||
$this->assertSame(15, $gateway->getSessionIdleTimeoutMinutes());
|
||||
}
|
||||
|
||||
public function testGetSessionAbsoluteTimeoutHoursReturnsStoredValue(): void
|
||||
{
|
||||
$gateway = $this->createGateway('12', 'session_absolute_timeout_hours');
|
||||
$this->assertSame(12, $gateway->getSessionAbsoluteTimeoutHours());
|
||||
}
|
||||
|
||||
public function testGetSessionIdleTimeoutMinutesReturnsFallbackForOutOfBoundsValue(): void
|
||||
{
|
||||
$gateway = $this->createGateway('2', 'session_idle_timeout_minutes');
|
||||
$this->assertSame(30, $gateway->getSessionIdleTimeoutMinutes());
|
||||
}
|
||||
|
||||
public function testGetSessionAbsoluteTimeoutHoursReturnsFallbackForOutOfBoundsValue(): void
|
||||
{
|
||||
$gateway = $this->createGateway('100', 'session_absolute_timeout_hours');
|
||||
$this->assertSame(8, $gateway->getSessionAbsoluteTimeoutHours());
|
||||
}
|
||||
|
||||
public function testSetSessionIdleTimeoutMinutesRejectsBelowMinimum(): void
|
||||
{
|
||||
$settings = $this->createMock(SettingRepositoryInterface::class);
|
||||
$settings->expects($this->never())->method('set');
|
||||
$gateway = new SettingsSessionGateway(new SettingsMetadataGateway($settings));
|
||||
$this->assertFalse($gateway->setSessionIdleTimeoutMinutes(2));
|
||||
}
|
||||
|
||||
public function testSetSessionIdleTimeoutMinutesRejectsAboveMaximum(): void
|
||||
{
|
||||
$settings = $this->createMock(SettingRepositoryInterface::class);
|
||||
$settings->expects($this->never())->method('set');
|
||||
$gateway = new SettingsSessionGateway(new SettingsMetadataGateway($settings));
|
||||
$this->assertFalse($gateway->setSessionIdleTimeoutMinutes(2000));
|
||||
}
|
||||
|
||||
public function testSetSessionAbsoluteTimeoutHoursRejectsZero(): void
|
||||
{
|
||||
$settings = $this->createMock(SettingRepositoryInterface::class);
|
||||
$settings->expects($this->never())->method('set');
|
||||
$gateway = new SettingsSessionGateway(new SettingsMetadataGateway($settings));
|
||||
$this->assertFalse($gateway->setSessionAbsoluteTimeoutHours(0));
|
||||
}
|
||||
|
||||
public function testGetSessionIdleTimeoutSecondsConvertsCorrectly(): void
|
||||
{
|
||||
$gateway = $this->createGateway('15', 'session_idle_timeout_minutes');
|
||||
$this->assertSame(900, $gateway->getSessionIdleTimeoutSeconds());
|
||||
}
|
||||
|
||||
public function testGetSessionAbsoluteTimeoutSecondsConvertsCorrectly(): void
|
||||
{
|
||||
$gateway = $this->createGateway('4', 'session_absolute_timeout_hours');
|
||||
$this->assertSame(14400, $gateway->getSessionAbsoluteTimeoutSeconds());
|
||||
}
|
||||
|
||||
private function createGateway(?string $returnValue, string $forKey = ''): SettingsSessionGateway
|
||||
{
|
||||
$settings = $this->createMock(SettingRepositoryInterface::class);
|
||||
$settings->method('getValue')->willReturnCallback(
|
||||
static function (string $key) use ($returnValue, $forKey): ?string {
|
||||
if ($forKey !== '' && $key === $forKey) {
|
||||
return $returnValue;
|
||||
}
|
||||
return $returnValue !== null && $forKey === '' ? $returnValue : null;
|
||||
}
|
||||
);
|
||||
|
||||
return new SettingsSessionGateway(new SettingsMetadataGateway($settings));
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace MintyPHP\Tests\Service\User;
|
||||
|
||||
use MintyPHP\Repository\Support\DatabaseSessionRepository;
|
||||
use MintyPHP\Repository\User\UserListQueryRepositoryInterface;
|
||||
use MintyPHP\Repository\User\UserReadRepositoryInterface;
|
||||
use MintyPHP\Repository\User\UserWriteRepositoryInterface;
|
||||
@@ -418,7 +419,8 @@ class UserAccountServiceTest extends TestCase
|
||||
?UserSettingsGateway $settingsGateway = null,
|
||||
?UserScopeGateway $scopeGateway = null,
|
||||
?UserDirectoryGateway $directoryGateway = null,
|
||||
?SystemAuditService $auditService = null
|
||||
?SystemAuditService $auditService = null,
|
||||
?DatabaseSessionRepository $databaseSessionRepository = null
|
||||
): UserAccountService {
|
||||
$readRepository = $readRepository ?? $this->createMock(UserReadRepositoryInterface::class);
|
||||
$writeRepository = $writeRepository ?? $this->createMock(UserWriteRepositoryInterface::class);
|
||||
@@ -463,6 +465,7 @@ class UserAccountServiceTest extends TestCase
|
||||
}
|
||||
|
||||
$auditService = $auditService ?? $this->createMock(SystemAuditService::class);
|
||||
$databaseSessionRepository = $databaseSessionRepository ?? $this->createMock(DatabaseSessionRepository::class);
|
||||
|
||||
return new UserAccountService(
|
||||
$readRepository,
|
||||
@@ -473,7 +476,8 @@ class UserAccountServiceTest extends TestCase
|
||||
$settingsGateway,
|
||||
$scopeGateway,
|
||||
$directoryGateway,
|
||||
$auditService
|
||||
$auditService,
|
||||
$databaseSessionRepository
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user