test(auth): add 33 unit tests for AuthService and RememberMeService
AuthServiceTest (17 tests): canLoginToTenant, refreshSessionAuthState,
loginUserById, login email-not-verified branch.
RememberMeServiceTest (16 tests): rememberUser, autoLoginFromCookie
(all 11 branches), forgetCurrentUser, forgetAllForUser,
expireAllTokensByAdmin.
Also fixes: 3 PHPStan errors in FrontendTelemetryIngestService,
8 CS-Fixer violations in out-of-scope files (ordered_imports,
braces_position).
All quality gates green: QG-001 (429 tests/0 failures),
QG-002 (0 errors), QG-003 (11 arch tests pass), QG-006 (0 violations).
Task: AUTH-LOGIN-REVIEW-001
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:43:31 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace MintyPHP\Tests\Service\Auth;
|
|
|
|
|
|
|
|
|
|
use MintyPHP\Http\SessionStoreInterface;
|
|
|
|
|
use MintyPHP\Repository\User\UserReadRepositoryInterface;
|
|
|
|
|
use MintyPHP\Repository\User\UserWriteRepositoryInterface;
|
2026-03-13 11:31:33 +01:00
|
|
|
use MintyPHP\Service\Access\PermissionService;
|
refactor(audit): extract audit domain into self-contained module
Move the entire audit subsystem (system audit, API audit, import audit,
user lifecycle audit, frontend telemetry) from core into modules/audit/.
Core decoupling via interface-based injection:
- AuditRecorderInterface replaces SystemAuditService in 10+ core services
- UserLifecycleAuditInterface / ImportAuditInterface for specialized flows
- NullAuditRecorder fallback when audit module is disabled
- ApiBootstrap/ApiResponse use null-safe callable resolvers
Module structure (modules/audit/):
- Manifest with routes, permissions, scheduler jobs, authorization policy
- 9 services, 8 repositories, 6 domain enums, 4 job handlers
- 33 page files, 4 JS files, 8 test files, migration scripts, i18n
Core cleanup:
- OperationsAuthorizationPolicy, UiCapabilityMap, PermissionService
surgically cleaned of audit-specific constants
- Sidebar template cleared of hardcoded audit navigation
- AuditModuleIsolationContractTest ensures no future core→module coupling
All quality gates pass: 1346 tests (19,276 assertions), PHPStan level 5
clean, architecture contracts verified.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:12:49 +01:00
|
|
|
use MintyPHP\Service\Audit\AuditRecorderInterface;
|
test(auth): add 33 unit tests for AuthService and RememberMeService
AuthServiceTest (17 tests): canLoginToTenant, refreshSessionAuthState,
loginUserById, login email-not-verified branch.
RememberMeServiceTest (16 tests): rememberUser, autoLoginFromCookie
(all 11 branches), forgetCurrentUser, forgetAllForUser,
expireAllTokensByAdmin.
Also fixes: 3 PHPStan errors in FrontendTelemetryIngestService,
8 CS-Fixer violations in out-of-scope files (ordered_imports,
braces_position).
All quality gates green: QG-001 (429 tests/0 failures),
QG-002 (0 errors), QG-003 (11 arch tests pass), QG-006 (0 violations).
Task: AUTH-LOGIN-REVIEW-001
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:43:31 +01:00
|
|
|
use MintyPHP\Service\Auth\AuthService;
|
|
|
|
|
use MintyPHP\Service\Auth\AuthSessionTenantContextService;
|
|
|
|
|
use MintyPHP\Service\Auth\EmailVerificationService;
|
|
|
|
|
use MintyPHP\Service\Auth\RememberMeService;
|
2026-03-13 11:31:33 +01:00
|
|
|
use MintyPHP\Service\Auth\TenantSsoService;
|
test(auth): add 33 unit tests for AuthService and RememberMeService
AuthServiceTest (17 tests): canLoginToTenant, refreshSessionAuthState,
loginUserById, login email-not-verified branch.
RememberMeServiceTest (16 tests): rememberUser, autoLoginFromCookie
(all 11 branches), forgetCurrentUser, forgetAllForUser,
expireAllTokensByAdmin.
Also fixes: 3 PHPStan errors in FrontendTelemetryIngestService,
8 CS-Fixer violations in out-of-scope files (ordered_imports,
braces_position).
All quality gates green: QG-001 (429 tests/0 failures),
QG-002 (0 errors), QG-003 (11 arch tests pass), QG-006 (0 violations).
Task: AUTH-LOGIN-REVIEW-001
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:43:31 +01:00
|
|
|
use MintyPHP\Service\User\UserAccountService;
|
|
|
|
|
use MintyPHP\Service\User\UserTenantContextService;
|
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
|
|
|
|
|
|
class AuthServiceTest extends TestCase
|
|
|
|
|
{
|
2026-03-19 20:35:15 +01:00
|
|
|
private array $previousSession = [];
|
|
|
|
|
|
|
|
|
|
protected function setUp(): void
|
|
|
|
|
{
|
|
|
|
|
parent::setUp();
|
|
|
|
|
$this->previousSession = $_SESSION ?? [];
|
|
|
|
|
|
|
|
|
|
if (session_status() !== PHP_SESSION_ACTIVE) {
|
|
|
|
|
session_start();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$_SESSION = [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function tearDown(): void
|
|
|
|
|
{
|
|
|
|
|
$_SESSION = $this->previousSession;
|
|
|
|
|
|
|
|
|
|
if (session_status() === PHP_SESSION_ACTIVE) {
|
|
|
|
|
session_write_close();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
parent::tearDown();
|
|
|
|
|
}
|
|
|
|
|
|
test(auth): add 33 unit tests for AuthService and RememberMeService
AuthServiceTest (17 tests): canLoginToTenant, refreshSessionAuthState,
loginUserById, login email-not-verified branch.
RememberMeServiceTest (16 tests): rememberUser, autoLoginFromCookie
(all 11 branches), forgetCurrentUser, forgetAllForUser,
expireAllTokensByAdmin.
Also fixes: 3 PHPStan errors in FrontendTelemetryIngestService,
8 CS-Fixer violations in out-of-scope files (ordered_imports,
braces_position).
All quality gates green: QG-001 (429 tests/0 failures),
QG-002 (0 errors), QG-003 (11 arch tests pass), QG-006 (0 violations).
Task: AUTH-LOGIN-REVIEW-001
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:43:31 +01:00
|
|
|
// ---------------------------------------------------------------
|
|
|
|
|
// canLoginToTenant
|
|
|
|
|
// ---------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
public function testCanLoginToTenantReturnsFalseForInvalidUserId(): void
|
|
|
|
|
{
|
|
|
|
|
$service = $this->newService();
|
|
|
|
|
$this->assertFalse($service->canLoginToTenant(0, 5));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCanLoginToTenantReturnsFalseForInvalidTenantId(): void
|
|
|
|
|
{
|
|
|
|
|
$service = $this->newService();
|
|
|
|
|
$this->assertFalse($service->canLoginToTenant(1, 0));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCanLoginToTenantReturnsTrueWhenTenantIsAssigned(): void
|
|
|
|
|
{
|
|
|
|
|
$tenantCtx = $this->createMock(UserTenantContextService::class);
|
|
|
|
|
$tenantCtx->method('getAvailableTenants')->willReturn([
|
|
|
|
|
['id' => 3],
|
|
|
|
|
['id' => 7],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$service = $this->newService(userTenantContextService: $tenantCtx);
|
|
|
|
|
$this->assertTrue($service->canLoginToTenant(1, 7));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCanLoginToTenantReturnsFalseWhenTenantNotAssigned(): void
|
|
|
|
|
{
|
|
|
|
|
$tenantCtx = $this->createMock(UserTenantContextService::class);
|
|
|
|
|
$tenantCtx->method('getAvailableTenants')->willReturn([
|
|
|
|
|
['id' => 3],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$service = $this->newService(userTenantContextService: $tenantCtx);
|
|
|
|
|
$this->assertFalse($service->canLoginToTenant(1, 99));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------
|
|
|
|
|
// refreshSessionAuthState
|
|
|
|
|
// ---------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
public function testRefreshSessionRequiresLogoutWhenUserIdIsZero(): void
|
|
|
|
|
{
|
|
|
|
|
$service = $this->newService();
|
|
|
|
|
$result = $service->refreshSessionAuthState(0);
|
|
|
|
|
|
|
|
|
|
$this->assertFalse($result['ok']);
|
|
|
|
|
$this->assertTrue($result['logout_required']);
|
|
|
|
|
$this->assertSame('user_missing', $result['reason']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testRefreshSessionRequiresLogoutWhenUserNotFound(): void
|
|
|
|
|
{
|
|
|
|
|
$userRead = $this->createMock(UserReadRepositoryInterface::class);
|
|
|
|
|
$userRead->method('findAuthzSnapshot')->willReturn(null);
|
|
|
|
|
|
|
|
|
|
$service = $this->newService(userReadRepository: $userRead);
|
|
|
|
|
$result = $service->refreshSessionAuthState(5);
|
|
|
|
|
|
|
|
|
|
$this->assertFalse($result['ok']);
|
|
|
|
|
$this->assertTrue($result['logout_required']);
|
|
|
|
|
$this->assertSame('user_not_found', $result['reason']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testRefreshSessionRequiresLogoutWhenUserIsInactive(): void
|
|
|
|
|
{
|
|
|
|
|
$userRead = $this->createMock(UserReadRepositoryInterface::class);
|
|
|
|
|
$userRead->method('findAuthzSnapshot')->willReturn(['active' => 0, 'authz_version' => 1]);
|
|
|
|
|
|
|
|
|
|
$service = $this->newService(userReadRepository: $userRead);
|
|
|
|
|
$result = $service->refreshSessionAuthState(5);
|
|
|
|
|
|
|
|
|
|
$this->assertFalse($result['ok']);
|
|
|
|
|
$this->assertTrue($result['logout_required']);
|
|
|
|
|
$this->assertSame('inactive', $result['reason']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testRefreshSessionReturnsOkWhenVersionMatchesAndTenantActive(): void
|
|
|
|
|
{
|
|
|
|
|
$session = $this->createMock(SessionStoreInterface::class);
|
|
|
|
|
$session->method('get')->willReturnMap([
|
|
|
|
|
['user', [], ['id' => 5, 'authz_version' => 2]],
|
|
|
|
|
['current_tenant', [], ['id' => 10]],
|
|
|
|
|
['no_active_tenant', false, false],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$userRead = $this->createMock(UserReadRepositoryInterface::class);
|
|
|
|
|
$userRead->method('findAuthzSnapshot')->willReturn(['active' => 1, 'authz_version' => 2]);
|
|
|
|
|
|
|
|
|
|
$service = $this->newService(userReadRepository: $userRead, sessionStore: $session);
|
|
|
|
|
$result = $service->refreshSessionAuthState(5);
|
|
|
|
|
|
|
|
|
|
$this->assertTrue($result['ok']);
|
|
|
|
|
$this->assertFalse($result['logout_required']);
|
|
|
|
|
$this->assertFalse($result['refreshed']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testRefreshSessionRefreshesWhenVersionMismatch(): void
|
|
|
|
|
{
|
|
|
|
|
$session = $this->createMock(SessionStoreInterface::class);
|
|
|
|
|
$session->method('get')->willReturnCallback(function (string $key, mixed $default) {
|
|
|
|
|
if ($key === 'user') {
|
|
|
|
|
return ['id' => 5, 'authz_version' => 1];
|
|
|
|
|
}
|
|
|
|
|
if ($key === 'no_active_tenant') {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return $default;
|
|
|
|
|
});
|
|
|
|
|
$session->expects($this->atLeastOnce())->method('set');
|
|
|
|
|
|
|
|
|
|
$userRead = $this->createMock(UserReadRepositoryInterface::class);
|
|
|
|
|
$userRead->method('findAuthzSnapshot')->willReturn(['active' => 1, 'authz_version' => 3]);
|
|
|
|
|
$userRead->method('find')->willReturn(['id' => 5, 'active' => 1, 'authz_version' => 3]);
|
|
|
|
|
|
2026-03-13 11:31:33 +01:00
|
|
|
$permService = $this->createMock(PermissionService::class);
|
|
|
|
|
$permService->expects($this->once())->method('getUserPermissions')->with(5, true);
|
test(auth): add 33 unit tests for AuthService and RememberMeService
AuthServiceTest (17 tests): canLoginToTenant, refreshSessionAuthState,
loginUserById, login email-not-verified branch.
RememberMeServiceTest (16 tests): rememberUser, autoLoginFromCookie
(all 11 branches), forgetCurrentUser, forgetAllForUser,
expireAllTokensByAdmin.
Also fixes: 3 PHPStan errors in FrontendTelemetryIngestService,
8 CS-Fixer violations in out-of-scope files (ordered_imports,
braces_position).
All quality gates green: QG-001 (429 tests/0 failures),
QG-002 (0 errors), QG-003 (11 arch tests pass), QG-006 (0 violations).
Task: AUTH-LOGIN-REVIEW-001
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:43:31 +01:00
|
|
|
|
|
|
|
|
$tenantCtxService = $this->createMock(AuthSessionTenantContextService::class);
|
|
|
|
|
$tenantCtxService->expects($this->once())->method('hydrateForUser')->with(5);
|
|
|
|
|
|
|
|
|
|
$service = $this->newService(
|
|
|
|
|
userReadRepository: $userRead,
|
2026-03-13 11:31:33 +01:00
|
|
|
permissionService: $permService,
|
test(auth): add 33 unit tests for AuthService and RememberMeService
AuthServiceTest (17 tests): canLoginToTenant, refreshSessionAuthState,
loginUserById, login email-not-verified branch.
RememberMeServiceTest (16 tests): rememberUser, autoLoginFromCookie
(all 11 branches), forgetCurrentUser, forgetAllForUser,
expireAllTokensByAdmin.
Also fixes: 3 PHPStan errors in FrontendTelemetryIngestService,
8 CS-Fixer violations in out-of-scope files (ordered_imports,
braces_position).
All quality gates green: QG-001 (429 tests/0 failures),
QG-002 (0 errors), QG-003 (11 arch tests pass), QG-006 (0 violations).
Task: AUTH-LOGIN-REVIEW-001
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:43:31 +01:00
|
|
|
authSessionTenantContextService: $tenantCtxService,
|
|
|
|
|
sessionStore: $session
|
|
|
|
|
);
|
|
|
|
|
$result = $service->refreshSessionAuthState(5);
|
|
|
|
|
|
|
|
|
|
$this->assertTrue($result['ok']);
|
|
|
|
|
$this->assertFalse($result['logout_required']);
|
|
|
|
|
$this->assertTrue($result['refreshed']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testRefreshSessionRequiresLogoutWhenNoActiveTenantAfterRefresh(): void
|
|
|
|
|
{
|
|
|
|
|
$session = $this->createMock(SessionStoreInterface::class);
|
|
|
|
|
$session->method('get')->willReturnCallback(function (string $key, mixed $default) {
|
|
|
|
|
if ($key === 'user') {
|
|
|
|
|
return ['id' => 5, 'authz_version' => 1];
|
|
|
|
|
}
|
|
|
|
|
if ($key === 'no_active_tenant') {
|
|
|
|
|
return true; // no active tenant
|
|
|
|
|
}
|
|
|
|
|
return $default;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
$userRead = $this->createMock(UserReadRepositoryInterface::class);
|
|
|
|
|
$userRead->method('findAuthzSnapshot')->willReturn(['active' => 1, 'authz_version' => 2]);
|
|
|
|
|
$userRead->method('find')->willReturn(['id' => 5, 'active' => 1, 'authz_version' => 2]);
|
|
|
|
|
|
|
|
|
|
$service = $this->newService(userReadRepository: $userRead, sessionStore: $session);
|
|
|
|
|
$result = $service->refreshSessionAuthState(5);
|
|
|
|
|
|
|
|
|
|
$this->assertFalse($result['ok']);
|
|
|
|
|
$this->assertTrue($result['logout_required']);
|
|
|
|
|
$this->assertSame('no_active_tenant', $result['reason']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------
|
|
|
|
|
// loginUserById
|
|
|
|
|
// ---------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
public function testLoginUserByIdReturnsErrorForZeroId(): void
|
|
|
|
|
{
|
|
|
|
|
$service = $this->newService();
|
|
|
|
|
$result = $service->loginUserById(0);
|
|
|
|
|
|
|
|
|
|
$this->assertFalse($result['ok']);
|
|
|
|
|
$this->assertSame('user_not_found', $result['error']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testLoginUserByIdReturnsErrorWhenUserNotFound(): void
|
|
|
|
|
{
|
|
|
|
|
$userRead = $this->createMock(UserReadRepositoryInterface::class);
|
|
|
|
|
$userRead->method('find')->willReturn(null);
|
|
|
|
|
|
|
|
|
|
$service = $this->newService(userReadRepository: $userRead);
|
|
|
|
|
$result = $service->loginUserById(5);
|
|
|
|
|
|
|
|
|
|
$this->assertFalse($result['ok']);
|
|
|
|
|
$this->assertSame('user_not_found', $result['error']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testLoginUserByIdReturnsErrorWhenUserInactive(): void
|
|
|
|
|
{
|
|
|
|
|
$userRead = $this->createMock(UserReadRepositoryInterface::class);
|
|
|
|
|
$userRead->method('find')->willReturn(['id' => 5, 'active' => 0]);
|
|
|
|
|
|
|
|
|
|
$service = $this->newService(userReadRepository: $userRead);
|
|
|
|
|
$result = $service->loginUserById(5);
|
|
|
|
|
|
|
|
|
|
$this->assertFalse($result['ok']);
|
|
|
|
|
$this->assertSame('user_inactive', $result['error']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testLoginUserByIdSucceedsWithActiveTenant(): void
|
|
|
|
|
{
|
|
|
|
|
$_SESSION = [];
|
|
|
|
|
|
|
|
|
|
$userRead = $this->createMock(UserReadRepositoryInterface::class);
|
|
|
|
|
$userRead->method('find')->willReturn(['id' => 5, 'active' => 1]);
|
|
|
|
|
|
|
|
|
|
$userWrite = $this->createMock(UserWriteRepositoryInterface::class);
|
|
|
|
|
$userWrite->expects($this->once())->method('updateLastLogin')->with(5, 'microsoft');
|
|
|
|
|
|
|
|
|
|
$session = $this->createMock(SessionStoreInterface::class);
|
|
|
|
|
$session->expects($this->atLeastOnce())->method('set');
|
|
|
|
|
$session->method('get')->willReturnMap([
|
|
|
|
|
['user', [], ['id' => 5, 'active' => 1]],
|
|
|
|
|
['current_tenant', [], ['id' => 10]],
|
|
|
|
|
['no_active_tenant', false, false],
|
|
|
|
|
]);
|
|
|
|
|
|
2026-03-13 11:31:33 +01:00
|
|
|
$permService = $this->createMock(PermissionService::class);
|
|
|
|
|
$permService->expects($this->once())->method('getUserPermissions')->with(5, true);
|
test(auth): add 33 unit tests for AuthService and RememberMeService
AuthServiceTest (17 tests): canLoginToTenant, refreshSessionAuthState,
loginUserById, login email-not-verified branch.
RememberMeServiceTest (16 tests): rememberUser, autoLoginFromCookie
(all 11 branches), forgetCurrentUser, forgetAllForUser,
expireAllTokensByAdmin.
Also fixes: 3 PHPStan errors in FrontendTelemetryIngestService,
8 CS-Fixer violations in out-of-scope files (ordered_imports,
braces_position).
All quality gates green: QG-001 (429 tests/0 failures),
QG-002 (0 errors), QG-003 (11 arch tests pass), QG-006 (0 violations).
Task: AUTH-LOGIN-REVIEW-001
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:43:31 +01:00
|
|
|
|
|
|
|
|
$tenantCtxService = $this->createMock(AuthSessionTenantContextService::class);
|
|
|
|
|
$tenantCtxService->expects($this->once())->method('hydrateForUser')->with(5);
|
|
|
|
|
|
refactor(audit): extract audit domain into self-contained module
Move the entire audit subsystem (system audit, API audit, import audit,
user lifecycle audit, frontend telemetry) from core into modules/audit/.
Core decoupling via interface-based injection:
- AuditRecorderInterface replaces SystemAuditService in 10+ core services
- UserLifecycleAuditInterface / ImportAuditInterface for specialized flows
- NullAuditRecorder fallback when audit module is disabled
- ApiBootstrap/ApiResponse use null-safe callable resolvers
Module structure (modules/audit/):
- Manifest with routes, permissions, scheduler jobs, authorization policy
- 9 services, 8 repositories, 6 domain enums, 4 job handlers
- 33 page files, 4 JS files, 8 test files, migration scripts, i18n
Core cleanup:
- OperationsAuthorizationPolicy, UiCapabilityMap, PermissionService
surgically cleaned of audit-specific constants
- Sidebar template cleared of hardcoded audit navigation
- AuditModuleIsolationContractTest ensures no future core→module coupling
All quality gates pass: 1346 tests (19,276 assertions), PHPStan level 5
clean, architecture contracts verified.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:12:49 +01:00
|
|
|
$audit = $this->createMock(AuditRecorderInterface::class);
|
test(auth): add 33 unit tests for AuthService and RememberMeService
AuthServiceTest (17 tests): canLoginToTenant, refreshSessionAuthState,
loginUserById, login email-not-verified branch.
RememberMeServiceTest (16 tests): rememberUser, autoLoginFromCookie
(all 11 branches), forgetCurrentUser, forgetAllForUser,
expireAllTokensByAdmin.
Also fixes: 3 PHPStan errors in FrontendTelemetryIngestService,
8 CS-Fixer violations in out-of-scope files (ordered_imports,
braces_position).
All quality gates green: QG-001 (429 tests/0 failures),
QG-002 (0 errors), QG-003 (11 arch tests pass), QG-006 (0 violations).
Task: AUTH-LOGIN-REVIEW-001
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:43:31 +01:00
|
|
|
$audit->expects($this->once())->method('record')
|
|
|
|
|
->with('auth.login.success', 'success', $this->anything());
|
|
|
|
|
|
|
|
|
|
$service = $this->newService(
|
|
|
|
|
userReadRepository: $userRead,
|
|
|
|
|
userWriteRepository: $userWrite,
|
2026-03-13 11:31:33 +01:00
|
|
|
permissionService: $permService,
|
test(auth): add 33 unit tests for AuthService and RememberMeService
AuthServiceTest (17 tests): canLoginToTenant, refreshSessionAuthState,
loginUserById, login email-not-verified branch.
RememberMeServiceTest (16 tests): rememberUser, autoLoginFromCookie
(all 11 branches), forgetCurrentUser, forgetAllForUser,
expireAllTokensByAdmin.
Also fixes: 3 PHPStan errors in FrontendTelemetryIngestService,
8 CS-Fixer violations in out-of-scope files (ordered_imports,
braces_position).
All quality gates green: QG-001 (429 tests/0 failures),
QG-002 (0 errors), QG-003 (11 arch tests pass), QG-006 (0 violations).
Task: AUTH-LOGIN-REVIEW-001
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:43:31 +01:00
|
|
|
authSessionTenantContextService: $tenantCtxService,
|
|
|
|
|
systemAuditService: $audit,
|
|
|
|
|
sessionStore: $session
|
|
|
|
|
);
|
|
|
|
|
$result = $service->loginUserById(5, null, 'microsoft');
|
|
|
|
|
|
|
|
|
|
$this->assertTrue($result['ok']);
|
|
|
|
|
$this->assertSame(5, (int) ($result['user']['id'] ?? 0));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testLoginUserByIdFailsWhenNoActiveTenant(): void
|
|
|
|
|
{
|
|
|
|
|
$_SESSION = [];
|
|
|
|
|
|
|
|
|
|
$userRead = $this->createMock(UserReadRepositoryInterface::class);
|
|
|
|
|
$userRead->method('find')->willReturn(['id' => 5, 'active' => 1]);
|
|
|
|
|
|
|
|
|
|
$session = $this->createMock(SessionStoreInterface::class);
|
|
|
|
|
$session->method('get')->willReturnCallback(function (string $key, mixed $default) {
|
|
|
|
|
if ($key === 'user') {
|
|
|
|
|
return ['id' => 5, 'active' => 1];
|
|
|
|
|
}
|
|
|
|
|
if ($key === 'no_active_tenant') {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return $default;
|
|
|
|
|
});
|
|
|
|
|
|
refactor(audit): extract audit domain into self-contained module
Move the entire audit subsystem (system audit, API audit, import audit,
user lifecycle audit, frontend telemetry) from core into modules/audit/.
Core decoupling via interface-based injection:
- AuditRecorderInterface replaces SystemAuditService in 10+ core services
- UserLifecycleAuditInterface / ImportAuditInterface for specialized flows
- NullAuditRecorder fallback when audit module is disabled
- ApiBootstrap/ApiResponse use null-safe callable resolvers
Module structure (modules/audit/):
- Manifest with routes, permissions, scheduler jobs, authorization policy
- 9 services, 8 repositories, 6 domain enums, 4 job handlers
- 33 page files, 4 JS files, 8 test files, migration scripts, i18n
Core cleanup:
- OperationsAuthorizationPolicy, UiCapabilityMap, PermissionService
surgically cleaned of audit-specific constants
- Sidebar template cleared of hardcoded audit navigation
- AuditModuleIsolationContractTest ensures no future core→module coupling
All quality gates pass: 1346 tests (19,276 assertions), PHPStan level 5
clean, architecture contracts verified.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:12:49 +01:00
|
|
|
$audit = $this->createMock(AuditRecorderInterface::class);
|
test(auth): add 33 unit tests for AuthService and RememberMeService
AuthServiceTest (17 tests): canLoginToTenant, refreshSessionAuthState,
loginUserById, login email-not-verified branch.
RememberMeServiceTest (16 tests): rememberUser, autoLoginFromCookie
(all 11 branches), forgetCurrentUser, forgetAllForUser,
expireAllTokensByAdmin.
Also fixes: 3 PHPStan errors in FrontendTelemetryIngestService,
8 CS-Fixer violations in out-of-scope files (ordered_imports,
braces_position).
All quality gates green: QG-001 (429 tests/0 failures),
QG-002 (0 errors), QG-003 (11 arch tests pass), QG-006 (0 violations).
Task: AUTH-LOGIN-REVIEW-001
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:43:31 +01:00
|
|
|
$audit->expects($this->once())->method('record')
|
|
|
|
|
->with('auth.login.failed', 'failed', $this->anything());
|
|
|
|
|
|
|
|
|
|
$service = $this->newService(
|
|
|
|
|
userReadRepository: $userRead,
|
|
|
|
|
systemAuditService: $audit,
|
|
|
|
|
sessionStore: $session
|
|
|
|
|
);
|
|
|
|
|
$result = $service->loginUserById(5);
|
|
|
|
|
|
|
|
|
|
$this->assertFalse($result['ok']);
|
|
|
|
|
$this->assertSame('login_no_active_tenant', $result['error']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testLoginUserByIdSetsPreferredTenantWhenProvided(): void
|
|
|
|
|
{
|
|
|
|
|
$_SESSION = [];
|
|
|
|
|
|
|
|
|
|
$userRead = $this->createMock(UserReadRepositoryInterface::class);
|
|
|
|
|
$userRead->method('find')->willReturn(['id' => 5, 'active' => 1]);
|
|
|
|
|
|
|
|
|
|
$tenantCtx = $this->createMock(UserTenantContextService::class);
|
|
|
|
|
$tenantCtx->expects($this->once())
|
|
|
|
|
->method('setCurrentTenant')
|
|
|
|
|
->with(5, 42)
|
|
|
|
|
->willReturn(['ok' => true]);
|
|
|
|
|
|
|
|
|
|
$session = $this->createMock(SessionStoreInterface::class);
|
|
|
|
|
$session->method('get')->willReturnMap([
|
|
|
|
|
['user', [], ['id' => 5, 'active' => 1]],
|
|
|
|
|
['current_tenant', [], ['id' => 42]],
|
|
|
|
|
['no_active_tenant', false, false],
|
|
|
|
|
]);
|
|
|
|
|
$session->expects($this->atLeastOnce())->method('set');
|
|
|
|
|
|
|
|
|
|
$service = $this->newService(
|
|
|
|
|
userReadRepository: $userRead,
|
|
|
|
|
userTenantContextService: $tenantCtx,
|
|
|
|
|
sessionStore: $session
|
|
|
|
|
);
|
|
|
|
|
$result = $service->loginUserById(5, 42, 'local');
|
|
|
|
|
|
|
|
|
|
$this->assertTrue($result['ok']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------
|
|
|
|
|
// login — email not verified branch
|
|
|
|
|
// ---------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
public function testLoginReturnsNeedsVerificationWhenEmailNotVerified(): void
|
|
|
|
|
{
|
|
|
|
|
$userRead = $this->createMock(UserReadRepositoryInterface::class);
|
|
|
|
|
$userRead->method('findByEmail')->willReturn([
|
|
|
|
|
'id' => 5,
|
|
|
|
|
'email' => 'test@example.com',
|
|
|
|
|
'email_verified_at' => null,
|
|
|
|
|
]);
|
|
|
|
|
|
refactor(audit): extract audit domain into self-contained module
Move the entire audit subsystem (system audit, API audit, import audit,
user lifecycle audit, frontend telemetry) from core into modules/audit/.
Core decoupling via interface-based injection:
- AuditRecorderInterface replaces SystemAuditService in 10+ core services
- UserLifecycleAuditInterface / ImportAuditInterface for specialized flows
- NullAuditRecorder fallback when audit module is disabled
- ApiBootstrap/ApiResponse use null-safe callable resolvers
Module structure (modules/audit/):
- Manifest with routes, permissions, scheduler jobs, authorization policy
- 9 services, 8 repositories, 6 domain enums, 4 job handlers
- 33 page files, 4 JS files, 8 test files, migration scripts, i18n
Core cleanup:
- OperationsAuthorizationPolicy, UiCapabilityMap, PermissionService
surgically cleaned of audit-specific constants
- Sidebar template cleared of hardcoded audit navigation
- AuditModuleIsolationContractTest ensures no future core→module coupling
All quality gates pass: 1346 tests (19,276 assertions), PHPStan level 5
clean, architecture contracts verified.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:12:49 +01:00
|
|
|
$audit = $this->createMock(AuditRecorderInterface::class);
|
test(auth): add 33 unit tests for AuthService and RememberMeService
AuthServiceTest (17 tests): canLoginToTenant, refreshSessionAuthState,
loginUserById, login email-not-verified branch.
RememberMeServiceTest (16 tests): rememberUser, autoLoginFromCookie
(all 11 branches), forgetCurrentUser, forgetAllForUser,
expireAllTokensByAdmin.
Also fixes: 3 PHPStan errors in FrontendTelemetryIngestService,
8 CS-Fixer violations in out-of-scope files (ordered_imports,
braces_position).
All quality gates green: QG-001 (429 tests/0 failures),
QG-002 (0 errors), QG-003 (11 arch tests pass), QG-006 (0 violations).
Task: AUTH-LOGIN-REVIEW-001
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:43:31 +01:00
|
|
|
$audit->expects($this->once())->method('record')
|
|
|
|
|
->with('auth.login.failed', 'failed', $this->callback(function (array $ctx): bool {
|
|
|
|
|
return ($ctx['error_code'] ?? '') === 'email_not_verified';
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
$service = $this->newService(userReadRepository: $userRead, systemAuditService: $audit);
|
|
|
|
|
$result = $service->login('test@example.com', 'password');
|
|
|
|
|
|
|
|
|
|
$this->assertFalse($result['ok']);
|
|
|
|
|
$this->assertTrue($result['needs_verification'] ?? false);
|
|
|
|
|
$this->assertSame('login_not_verified', $result['flash_key']);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 20:47:21 +01:00
|
|
|
// ---------------------------------------------------------------
|
|
|
|
|
// logout behavior
|
|
|
|
|
// ---------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
public function testLogoutDueToSessionTimeoutKeepsRememberTokenAndClearsSessionTimers(): void
|
|
|
|
|
{
|
|
|
|
|
$_SESSION = [];
|
|
|
|
|
if (session_status() !== PHP_SESSION_ACTIVE) {
|
|
|
|
|
session_start();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$removedKeys = [];
|
|
|
|
|
$session = $this->createMock(SessionStoreInterface::class);
|
|
|
|
|
$session->method('get')->willReturnMap([
|
|
|
|
|
['user', [], ['id' => 5]],
|
|
|
|
|
['current_tenant', [], ['id' => 9]],
|
|
|
|
|
]);
|
|
|
|
|
$session->method('remove')->willReturnCallback(function (string $key) use (&$removedKeys): void {
|
|
|
|
|
$removedKeys[] = $key;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
$rememberMeService = $this->createMock(RememberMeService::class);
|
|
|
|
|
$rememberMeService->expects($this->never())->method('forgetCurrentUser');
|
|
|
|
|
|
refactor(audit): extract audit domain into self-contained module
Move the entire audit subsystem (system audit, API audit, import audit,
user lifecycle audit, frontend telemetry) from core into modules/audit/.
Core decoupling via interface-based injection:
- AuditRecorderInterface replaces SystemAuditService in 10+ core services
- UserLifecycleAuditInterface / ImportAuditInterface for specialized flows
- NullAuditRecorder fallback when audit module is disabled
- ApiBootstrap/ApiResponse use null-safe callable resolvers
Module structure (modules/audit/):
- Manifest with routes, permissions, scheduler jobs, authorization policy
- 9 services, 8 repositories, 6 domain enums, 4 job handlers
- 33 page files, 4 JS files, 8 test files, migration scripts, i18n
Core cleanup:
- OperationsAuthorizationPolicy, UiCapabilityMap, PermissionService
surgically cleaned of audit-specific constants
- Sidebar template cleared of hardcoded audit navigation
- AuditModuleIsolationContractTest ensures no future core→module coupling
All quality gates pass: 1346 tests (19,276 assertions), PHPStan level 5
clean, architecture contracts verified.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:12:49 +01:00
|
|
|
$audit = $this->createMock(AuditRecorderInterface::class);
|
2026-03-09 20:47:21 +01:00
|
|
|
$audit->expects($this->once())->method('record')
|
|
|
|
|
->with('auth.session.timeout', 'success', $this->callback(function (array $context): bool {
|
|
|
|
|
return (int) ($context['actor_user_id'] ?? 0) === 5
|
|
|
|
|
&& (int) ($context['actor_tenant_id'] ?? 0) === 9;
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
$service = $this->newService(
|
|
|
|
|
rememberMeService: $rememberMeService,
|
|
|
|
|
systemAuditService: $audit,
|
|
|
|
|
sessionStore: $session
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$service->logoutDueToSessionTimeout();
|
|
|
|
|
sort($removedKeys);
|
|
|
|
|
$this->assertSame(['session_last_activity', 'session_started_at'], $removedKeys);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testLogoutStillForgetsRememberTokenOnManualLogout(): void
|
|
|
|
|
{
|
|
|
|
|
$_SESSION = [];
|
|
|
|
|
if (session_status() !== PHP_SESSION_ACTIVE) {
|
|
|
|
|
session_start();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$rememberMeService = $this->createMock(RememberMeService::class);
|
|
|
|
|
$rememberMeService->expects($this->once())->method('forgetCurrentUser');
|
|
|
|
|
|
|
|
|
|
$session = $this->createMock(SessionStoreInterface::class);
|
|
|
|
|
$session->method('get')->willReturnMap([
|
|
|
|
|
['user', [], ['id' => 5]],
|
|
|
|
|
['current_tenant', [], ['id' => 11]],
|
|
|
|
|
]);
|
|
|
|
|
|
refactor(audit): extract audit domain into self-contained module
Move the entire audit subsystem (system audit, API audit, import audit,
user lifecycle audit, frontend telemetry) from core into modules/audit/.
Core decoupling via interface-based injection:
- AuditRecorderInterface replaces SystemAuditService in 10+ core services
- UserLifecycleAuditInterface / ImportAuditInterface for specialized flows
- NullAuditRecorder fallback when audit module is disabled
- ApiBootstrap/ApiResponse use null-safe callable resolvers
Module structure (modules/audit/):
- Manifest with routes, permissions, scheduler jobs, authorization policy
- 9 services, 8 repositories, 6 domain enums, 4 job handlers
- 33 page files, 4 JS files, 8 test files, migration scripts, i18n
Core cleanup:
- OperationsAuthorizationPolicy, UiCapabilityMap, PermissionService
surgically cleaned of audit-specific constants
- Sidebar template cleared of hardcoded audit navigation
- AuditModuleIsolationContractTest ensures no future core→module coupling
All quality gates pass: 1346 tests (19,276 assertions), PHPStan level 5
clean, architecture contracts verified.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:12:49 +01:00
|
|
|
$audit = $this->createMock(AuditRecorderInterface::class);
|
2026-03-09 20:47:21 +01:00
|
|
|
$audit->expects($this->once())->method('record')
|
|
|
|
|
->with('auth.logout', 'success', $this->callback(function (array $context): bool {
|
|
|
|
|
return (int) ($context['actor_user_id'] ?? 0) === 5
|
|
|
|
|
&& (int) ($context['actor_tenant_id'] ?? 0) === 11;
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
$service = $this->newService(
|
|
|
|
|
rememberMeService: $rememberMeService,
|
|
|
|
|
systemAuditService: $audit,
|
|
|
|
|
sessionStore: $session
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$service->logout();
|
|
|
|
|
}
|
|
|
|
|
|
test(auth): add 33 unit tests for AuthService and RememberMeService
AuthServiceTest (17 tests): canLoginToTenant, refreshSessionAuthState,
loginUserById, login email-not-verified branch.
RememberMeServiceTest (16 tests): rememberUser, autoLoginFromCookie
(all 11 branches), forgetCurrentUser, forgetAllForUser,
expireAllTokensByAdmin.
Also fixes: 3 PHPStan errors in FrontendTelemetryIngestService,
8 CS-Fixer violations in out-of-scope files (ordered_imports,
braces_position).
All quality gates green: QG-001 (429 tests/0 failures),
QG-002 (0 errors), QG-003 (11 arch tests pass), QG-006 (0 violations).
Task: AUTH-LOGIN-REVIEW-001
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:43:31 +01:00
|
|
|
// ---------------------------------------------------------------
|
|
|
|
|
// Helper
|
|
|
|
|
// ---------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
private function newService(
|
|
|
|
|
?UserReadRepositoryInterface $userReadRepository = null,
|
|
|
|
|
?UserWriteRepositoryInterface $userWriteRepository = null,
|
|
|
|
|
?UserAccountService $userAccountService = null,
|
|
|
|
|
?UserTenantContextService $userTenantContextService = null,
|
|
|
|
|
?RememberMeService $rememberMeService = null,
|
|
|
|
|
?EmailVerificationService $emailVerificationService = null,
|
2026-03-13 11:31:33 +01:00
|
|
|
?PermissionService $permissionService = null,
|
|
|
|
|
?TenantSsoService $tenantSsoService = null,
|
test(auth): add 33 unit tests for AuthService and RememberMeService
AuthServiceTest (17 tests): canLoginToTenant, refreshSessionAuthState,
loginUserById, login email-not-verified branch.
RememberMeServiceTest (16 tests): rememberUser, autoLoginFromCookie
(all 11 branches), forgetCurrentUser, forgetAllForUser,
expireAllTokensByAdmin.
Also fixes: 3 PHPStan errors in FrontendTelemetryIngestService,
8 CS-Fixer violations in out-of-scope files (ordered_imports,
braces_position).
All quality gates green: QG-001 (429 tests/0 failures),
QG-002 (0 errors), QG-003 (11 arch tests pass), QG-006 (0 violations).
Task: AUTH-LOGIN-REVIEW-001
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:43:31 +01:00
|
|
|
?AuthSessionTenantContextService $authSessionTenantContextService = null,
|
refactor(audit): extract audit domain into self-contained module
Move the entire audit subsystem (system audit, API audit, import audit,
user lifecycle audit, frontend telemetry) from core into modules/audit/.
Core decoupling via interface-based injection:
- AuditRecorderInterface replaces SystemAuditService in 10+ core services
- UserLifecycleAuditInterface / ImportAuditInterface for specialized flows
- NullAuditRecorder fallback when audit module is disabled
- ApiBootstrap/ApiResponse use null-safe callable resolvers
Module structure (modules/audit/):
- Manifest with routes, permissions, scheduler jobs, authorization policy
- 9 services, 8 repositories, 6 domain enums, 4 job handlers
- 33 page files, 4 JS files, 8 test files, migration scripts, i18n
Core cleanup:
- OperationsAuthorizationPolicy, UiCapabilityMap, PermissionService
surgically cleaned of audit-specific constants
- Sidebar template cleared of hardcoded audit navigation
- AuditModuleIsolationContractTest ensures no future core→module coupling
All quality gates pass: 1346 tests (19,276 assertions), PHPStan level 5
clean, architecture contracts verified.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:12:49 +01:00
|
|
|
?AuditRecorderInterface $systemAuditService = null,
|
test(auth): add 33 unit tests for AuthService and RememberMeService
AuthServiceTest (17 tests): canLoginToTenant, refreshSessionAuthState,
loginUserById, login email-not-verified branch.
RememberMeServiceTest (16 tests): rememberUser, autoLoginFromCookie
(all 11 branches), forgetCurrentUser, forgetAllForUser,
expireAllTokensByAdmin.
Also fixes: 3 PHPStan errors in FrontendTelemetryIngestService,
8 CS-Fixer violations in out-of-scope files (ordered_imports,
braces_position).
All quality gates green: QG-001 (429 tests/0 failures),
QG-002 (0 errors), QG-003 (11 arch tests pass), QG-006 (0 violations).
Task: AUTH-LOGIN-REVIEW-001
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:43:31 +01:00
|
|
|
?SessionStoreInterface $sessionStore = null
|
|
|
|
|
): AuthService {
|
|
|
|
|
return new AuthService(
|
|
|
|
|
$userReadRepository ?? $this->createMock(UserReadRepositoryInterface::class),
|
|
|
|
|
$userWriteRepository ?? $this->createMock(UserWriteRepositoryInterface::class),
|
|
|
|
|
$userAccountService ?? $this->createMock(UserAccountService::class),
|
|
|
|
|
$userTenantContextService ?? $this->createMock(UserTenantContextService::class),
|
|
|
|
|
$rememberMeService ?? $this->createMock(RememberMeService::class),
|
|
|
|
|
$emailVerificationService ?? $this->createMock(EmailVerificationService::class),
|
2026-03-13 11:31:33 +01:00
|
|
|
$permissionService ?? $this->createMock(PermissionService::class),
|
|
|
|
|
$tenantSsoService ?? $this->createMock(TenantSsoService::class),
|
test(auth): add 33 unit tests for AuthService and RememberMeService
AuthServiceTest (17 tests): canLoginToTenant, refreshSessionAuthState,
loginUserById, login email-not-verified branch.
RememberMeServiceTest (16 tests): rememberUser, autoLoginFromCookie
(all 11 branches), forgetCurrentUser, forgetAllForUser,
expireAllTokensByAdmin.
Also fixes: 3 PHPStan errors in FrontendTelemetryIngestService,
8 CS-Fixer violations in out-of-scope files (ordered_imports,
braces_position).
All quality gates green: QG-001 (429 tests/0 failures),
QG-002 (0 errors), QG-003 (11 arch tests pass), QG-006 (0 violations).
Task: AUTH-LOGIN-REVIEW-001
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:43:31 +01:00
|
|
|
$authSessionTenantContextService ?? $this->createMock(AuthSessionTenantContextService::class),
|
refactor(audit): extract audit domain into self-contained module
Move the entire audit subsystem (system audit, API audit, import audit,
user lifecycle audit, frontend telemetry) from core into modules/audit/.
Core decoupling via interface-based injection:
- AuditRecorderInterface replaces SystemAuditService in 10+ core services
- UserLifecycleAuditInterface / ImportAuditInterface for specialized flows
- NullAuditRecorder fallback when audit module is disabled
- ApiBootstrap/ApiResponse use null-safe callable resolvers
Module structure (modules/audit/):
- Manifest with routes, permissions, scheduler jobs, authorization policy
- 9 services, 8 repositories, 6 domain enums, 4 job handlers
- 33 page files, 4 JS files, 8 test files, migration scripts, i18n
Core cleanup:
- OperationsAuthorizationPolicy, UiCapabilityMap, PermissionService
surgically cleaned of audit-specific constants
- Sidebar template cleared of hardcoded audit navigation
- AuditModuleIsolationContractTest ensures no future core→module coupling
All quality gates pass: 1346 tests (19,276 assertions), PHPStan level 5
clean, architecture contracts verified.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:12:49 +01:00
|
|
|
$systemAuditService ?? $this->createMock(AuditRecorderInterface::class),
|
test(auth): add 33 unit tests for AuthService and RememberMeService
AuthServiceTest (17 tests): canLoginToTenant, refreshSessionAuthState,
loginUserById, login email-not-verified branch.
RememberMeServiceTest (16 tests): rememberUser, autoLoginFromCookie
(all 11 branches), forgetCurrentUser, forgetAllForUser,
expireAllTokensByAdmin.
Also fixes: 3 PHPStan errors in FrontendTelemetryIngestService,
8 CS-Fixer violations in out-of-scope files (ordered_imports,
braces_position).
All quality gates green: QG-001 (429 tests/0 failures),
QG-002 (0 errors), QG-003 (11 arch tests pass), QG-006 (0 violations).
Task: AUTH-LOGIN-REVIEW-001
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:43:31 +01:00
|
|
|
$sessionStore ?? $this->createMock(SessionStoreInterface::class)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|