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>
This commit is contained in:
115
tests/Architecture/AuditModuleIsolationContractTest.php
Normal file
115
tests/Architecture/AuditModuleIsolationContractTest.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Architecture;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class AuditModuleIsolationContractTest extends TestCase
|
||||
{
|
||||
public function testCoreServiceAuditDirectoryContainsOnlyInterfaces(): void
|
||||
{
|
||||
$dir = dirname(__DIR__, 2) . '/lib/Service/Audit';
|
||||
self::assertDirectoryExists($dir);
|
||||
|
||||
$files = array_values(array_filter(
|
||||
scandir($dir) ?: [],
|
||||
static fn (string $f): bool => str_ends_with($f, '.php')
|
||||
));
|
||||
|
||||
$allowed = [
|
||||
'AuditRecorderInterface.php',
|
||||
'ImportAuditInterface.php',
|
||||
'NullAuditRecorder.php',
|
||||
'NullImportAudit.php',
|
||||
'NullUserLifecycleAudit.php',
|
||||
'UserLifecycleAuditInterface.php',
|
||||
];
|
||||
|
||||
sort($files);
|
||||
sort($allowed);
|
||||
|
||||
self::assertSame(
|
||||
$allowed,
|
||||
$files,
|
||||
'lib/Service/Audit/ should contain only interfaces and null implementations, found: ' . implode(', ', $files)
|
||||
);
|
||||
}
|
||||
|
||||
public function testCoreRepositoryAuditDirectoryDoesNotExist(): void
|
||||
{
|
||||
$root = dirname(__DIR__, 2);
|
||||
self::assertDirectoryDoesNotExist(
|
||||
$root . '/lib/Repository/Audit',
|
||||
'Audit repository classes should live in modules/audit/lib, not in Core lib/Repository/Audit'
|
||||
);
|
||||
}
|
||||
|
||||
public function testCoreDomainTaxonomyHasNoAuditEnums(): void
|
||||
{
|
||||
$dir = dirname(__DIR__, 2) . '/lib/Domain/Taxonomy';
|
||||
if (!is_dir($dir)) {
|
||||
self::addToAssertionCount(1);
|
||||
return;
|
||||
}
|
||||
|
||||
$entries = scandir($dir) ?: [];
|
||||
$auditFiles = array_filter($entries, static fn (string $f): bool => str_contains($f, 'Audit') || str_contains($f, 'Lifecycle'));
|
||||
self::assertSame(
|
||||
[],
|
||||
array_values($auditFiles),
|
||||
'Core lib/Domain/Taxonomy/ still contains audit enum files: ' . implode(', ', $auditFiles)
|
||||
);
|
||||
}
|
||||
|
||||
public function testCoreAuditPagesRemoved(): void
|
||||
{
|
||||
$root = dirname(__DIR__, 2);
|
||||
self::assertDirectoryDoesNotExist($root . '/pages/admin/api-audit');
|
||||
self::assertDirectoryDoesNotExist($root . '/pages/admin/system-audit');
|
||||
self::assertDirectoryDoesNotExist($root . '/pages/admin/import-audit');
|
||||
self::assertDirectoryDoesNotExist($root . '/pages/admin/user-lifecycle-audit');
|
||||
self::assertDirectoryDoesNotExist($root . '/pages/admin/frontend-telemetry');
|
||||
}
|
||||
|
||||
public function testCoreJsHasNoAuditPageScripts(): void
|
||||
{
|
||||
$root = dirname(__DIR__, 2);
|
||||
self::assertFileDoesNotExist($root . '/web/js/pages/admin-api-audit-index.js');
|
||||
self::assertFileDoesNotExist($root . '/web/js/pages/admin-system-audit-index.js');
|
||||
self::assertFileDoesNotExist($root . '/web/js/pages/admin-import-audit-index.js');
|
||||
self::assertFileDoesNotExist($root . '/web/js/pages/admin-user-lifecycle-audit-index.js');
|
||||
}
|
||||
|
||||
public function testCoreHttpHasNoConcreteAuditReporter(): void
|
||||
{
|
||||
$root = dirname(__DIR__, 2);
|
||||
self::assertFileDoesNotExist(
|
||||
$root . '/lib/Http/ApiSystemAuditReporter.php',
|
||||
'ApiSystemAuditReporter should live in modules/audit/lib, not in Core lib/Http'
|
||||
);
|
||||
}
|
||||
|
||||
public function testCoreSidebarHasNoHardcodedAuditNavigation(): void
|
||||
{
|
||||
$file = dirname(__DIR__, 2) . '/templates/partials/app-main-aside.phtml';
|
||||
$content = file_get_contents($file);
|
||||
self::assertIsString($content);
|
||||
self::assertStringNotContainsString('can_view_api_audit', $content);
|
||||
self::assertStringNotContainsString('can_view_system_audit', $content);
|
||||
self::assertStringNotContainsString('can_view_imports_audit', $content);
|
||||
self::assertStringNotContainsString('can_view_user_lifecycle_audit', $content);
|
||||
self::assertStringNotContainsString('admin/api-audit', $content);
|
||||
self::assertStringNotContainsString('admin/system-audit', $content);
|
||||
self::assertStringNotContainsString('admin/import-audit', $content);
|
||||
self::assertStringNotContainsString('admin/user-lifecycle-audit', $content);
|
||||
}
|
||||
|
||||
public function testCoreSchedulerHandlerRemoved(): void
|
||||
{
|
||||
$root = dirname(__DIR__, 2);
|
||||
self::assertFileDoesNotExist(
|
||||
$root . '/lib/Service/Scheduler/Handler/SystemAuditPurgeJobHandler.php',
|
||||
'SystemAuditPurgeJobHandler should live in modules/audit, not in Core'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -58,12 +58,7 @@ class AuthzUiActionContractTest extends TestCase
|
||||
'pages/admin/users/index().php' => 'UiCapabilityMap::PAGE_USERS_INDEX',
|
||||
'pages/admin/users/create().php' => 'UiCapabilityMap::PAGE_USERS_CREATE',
|
||||
'pages/admin/stats/index().php' => 'UiCapabilityMap::PAGE_STATS_INDEX',
|
||||
'pages/admin/api-audit/index().php' => 'UiCapabilityMap::PAGE_AUDIT_PURGE',
|
||||
'pages/admin/system-audit/index().php' => 'UiCapabilityMap::PAGE_SYSTEM_AUDIT',
|
||||
'pages/admin/import-audit/index().php' => 'UiCapabilityMap::PAGE_AUDIT_PURGE',
|
||||
'pages/admin/scheduled-jobs/index().php' => 'UiCapabilityMap::PAGE_AUDIT_PURGE',
|
||||
'pages/admin/user-lifecycle-audit/index().php' => 'UiCapabilityMap::PAGE_AUDIT_PURGE',
|
||||
'pages/admin/user-lifecycle-audit/view($id).php' => 'UiCapabilityMap::PAGE_USER_LIFECYCLE_VIEW',
|
||||
'pages/admin/scheduled-jobs/index().php' => 'UiCapabilityMap::PAGE_JOBS_PURGE',
|
||||
];
|
||||
|
||||
foreach ($actions as $action => $mapConstant) {
|
||||
@@ -72,4 +67,20 @@ class AuthzUiActionContractTest extends TestCase
|
||||
$this->assertStringContainsString($mapConstant, $content, $action);
|
||||
}
|
||||
}
|
||||
|
||||
public function testModuleAuditActionsUseInlinePageCapabilities(): void
|
||||
{
|
||||
$actions = [
|
||||
'modules/audit/pages/admin/api-audit/index().php',
|
||||
'modules/audit/pages/admin/system-audit/index().php',
|
||||
'modules/audit/pages/admin/import-audit/index().php',
|
||||
'modules/audit/pages/admin/user-lifecycle-audit/index().php',
|
||||
'modules/audit/pages/admin/user-lifecycle-audit/view($id).php',
|
||||
];
|
||||
|
||||
foreach ($actions as $action) {
|
||||
$content = $this->readProjectFile($action);
|
||||
$this->assertStringContainsString('->pageCapabilities(', $content, $action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,34 +36,34 @@ trait AuthzUiContractSupport
|
||||
'UiCapabilityMap::PAGE_STATS_INDEX',
|
||||
],
|
||||
'api audit index' => [
|
||||
'pages/admin/api-audit/index(default).phtml',
|
||||
'pages/admin/api-audit/index().php',
|
||||
'UiCapabilityMap::PAGE_AUDIT_PURGE',
|
||||
'modules/audit/pages/admin/api-audit/index(default).phtml',
|
||||
'modules/audit/pages/admin/api-audit/index().php',
|
||||
null,
|
||||
],
|
||||
'import audit index' => [
|
||||
'pages/admin/import-audit/index(default).phtml',
|
||||
'pages/admin/import-audit/index().php',
|
||||
'UiCapabilityMap::PAGE_AUDIT_PURGE',
|
||||
'modules/audit/pages/admin/import-audit/index(default).phtml',
|
||||
'modules/audit/pages/admin/import-audit/index().php',
|
||||
null,
|
||||
],
|
||||
'scheduled jobs index' => [
|
||||
'pages/admin/scheduled-jobs/index(default).phtml',
|
||||
'pages/admin/scheduled-jobs/index().php',
|
||||
'UiCapabilityMap::PAGE_AUDIT_PURGE',
|
||||
'UiCapabilityMap::PAGE_JOBS_PURGE',
|
||||
],
|
||||
'system audit index' => [
|
||||
'pages/admin/system-audit/index(default).phtml',
|
||||
'pages/admin/system-audit/index().php',
|
||||
'UiCapabilityMap::PAGE_SYSTEM_AUDIT',
|
||||
'modules/audit/pages/admin/system-audit/index(default).phtml',
|
||||
'modules/audit/pages/admin/system-audit/index().php',
|
||||
null,
|
||||
],
|
||||
'user lifecycle index' => [
|
||||
'pages/admin/user-lifecycle-audit/index(default).phtml',
|
||||
'pages/admin/user-lifecycle-audit/index().php',
|
||||
'UiCapabilityMap::PAGE_AUDIT_PURGE',
|
||||
'modules/audit/pages/admin/user-lifecycle-audit/index(default).phtml',
|
||||
'modules/audit/pages/admin/user-lifecycle-audit/index().php',
|
||||
null,
|
||||
],
|
||||
'user lifecycle view' => [
|
||||
'pages/admin/user-lifecycle-audit/view(default).phtml',
|
||||
'pages/admin/user-lifecycle-audit/view($id).php',
|
||||
'UiCapabilityMap::PAGE_USER_LIFECYCLE_VIEW',
|
||||
'modules/audit/pages/admin/user-lifecycle-audit/view(default).phtml',
|
||||
'modules/audit/pages/admin/user-lifecycle-audit/view($id).php',
|
||||
null,
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -85,6 +85,8 @@ trait AuthzUiContractSupport
|
||||
private function extractViewAuthPageKeys(string $content): array
|
||||
{
|
||||
$keys = [];
|
||||
|
||||
// Match inline array: $viewAuth['page'] = ['key' => ...];
|
||||
preg_match_all('/\\$viewAuth\\[\'page\'\\]\\s*=\\s*\\[(.*?)\\];/s', $content, $blocks);
|
||||
foreach ($blocks[1] as $block) {
|
||||
preg_match_all('/[\'"]([a-z_]+)[\'"]\\s*=>/', $block, $matches);
|
||||
@@ -93,6 +95,14 @@ trait AuthzUiContractSupport
|
||||
}
|
||||
}
|
||||
|
||||
// Match pageCapabilities() inline array: ->pageCapabilities(..., ['key' => ...])
|
||||
if (preg_match('/->pageCapabilities\\([^,]+,\\s*\\[(.*?)\\]\\s*\\)/s', $content, $pcMatch)) {
|
||||
preg_match_all('/[\'"]([a-z_]+)[\'"]\\s*=>/', $pcMatch[1], $matches);
|
||||
foreach ($matches[1] as $key) {
|
||||
$keys[] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
$keys = array_values(array_unique($keys));
|
||||
sort($keys);
|
||||
return $keys;
|
||||
|
||||
@@ -40,10 +40,12 @@ class AuthzUiLayoutContractTest extends TestCase
|
||||
$this->assertStringContainsString('UiCapabilityMap::LAYOUT', $content);
|
||||
}
|
||||
|
||||
public function testStatsPageCapabilityMapIncludesAuditCapabilities(): void
|
||||
public function testCoreCapabilityMapDoesNotHardcodeAuditCapabilities(): void
|
||||
{
|
||||
$content = $this->readProjectFile('lib/Service/Access/UiCapabilityMap.php');
|
||||
$this->assertStringContainsString("'can_view_system_audit' => OperationsAuthorizationPolicy::ABILITY_ADMIN_SYSTEM_AUDIT_VIEW", $content);
|
||||
$this->assertStringContainsString("'can_view_user_lifecycle_audit' => OperationsAuthorizationPolicy::ABILITY_ADMIN_USER_LIFECYCLE_AUDIT_VIEW", $content);
|
||||
$this->assertStringNotContainsString('can_view_system_audit', $content);
|
||||
$this->assertStringNotContainsString('can_view_api_audit', $content);
|
||||
$this->assertStringNotContainsString('can_view_user_lifecycle_audit', $content);
|
||||
$this->assertStringNotContainsString('can_view_imports_audit', $content);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,13 +74,13 @@ class AuthzUiTemplateContractTest extends TestCase
|
||||
$templates = [
|
||||
'templates/partials/app-main-aside.phtml',
|
||||
'templates/partials/app-main-aside-icon-bar.phtml',
|
||||
'pages/admin/api-audit/index(default).phtml',
|
||||
'pages/admin/system-audit/index(default).phtml',
|
||||
'pages/admin/import-audit/index(default).phtml',
|
||||
'modules/audit/pages/admin/api-audit/index(default).phtml',
|
||||
'modules/audit/pages/admin/system-audit/index(default).phtml',
|
||||
'modules/audit/pages/admin/import-audit/index(default).phtml',
|
||||
'pages/admin/scheduled-jobs/index(default).phtml',
|
||||
'pages/admin/stats/index(default).phtml',
|
||||
'pages/admin/user-lifecycle-audit/index(default).phtml',
|
||||
'pages/admin/user-lifecycle-audit/view(default).phtml',
|
||||
'modules/audit/pages/admin/user-lifecycle-audit/index(default).phtml',
|
||||
'modules/audit/pages/admin/user-lifecycle-audit/view(default).phtml',
|
||||
'pages/admin/users/index(default).phtml',
|
||||
'pages/admin/users/create(default).phtml',
|
||||
'pages/admin/tenants/edit(default).phtml',
|
||||
|
||||
@@ -17,7 +17,7 @@ class ContainerFactoryContractTest extends TestCase
|
||||
return [
|
||||
[\MintyPHP\Service\Access\AccessServicesFactory::class],
|
||||
[\MintyPHP\Module\AddressBook\Service\AddressBookServicesFactory::class],
|
||||
[\MintyPHP\Service\Audit\AuditServicesFactory::class],
|
||||
[\MintyPHP\Module\Audit\Service\AuditServicesFactory::class],
|
||||
[\MintyPHP\Service\Auth\AuthServicesFactory::class],
|
||||
[\MintyPHP\Service\Branding\BrandingServicesFactory::class],
|
||||
[\MintyPHP\Service\Directory\DirectoryServicesFactory::class],
|
||||
|
||||
@@ -11,7 +11,7 @@ class FrontendTelemetryContractTest extends TestCase
|
||||
public function testEventTypeMappingsStayInSyncAcrossFrontendAndBackend(): void
|
||||
{
|
||||
$js = $this->readProjectFile('web/js/core/app-telemetry.js');
|
||||
$ingest = $this->readProjectFile('lib/Service/Audit/FrontendTelemetryIngestService.php');
|
||||
$ingest = $this->readProjectFile('modules/audit/lib/Module/Audit/Service/FrontendTelemetryIngestService.php');
|
||||
$settings = $this->readProjectFile('lib/Service/Settings/SettingsFrontendTelemetryGateway.php');
|
||||
|
||||
$jsMap = $this->parseJsEventKeyToTypeMap($js);
|
||||
@@ -35,7 +35,7 @@ class FrontendTelemetryContractTest extends TestCase
|
||||
public function testAllowedMetaKeysStayInSyncAcrossFrontendAndBackend(): void
|
||||
{
|
||||
$js = $this->readProjectFile('web/js/core/app-telemetry.js');
|
||||
$ingest = $this->readProjectFile('lib/Service/Audit/FrontendTelemetryIngestService.php');
|
||||
$ingest = $this->readProjectFile('modules/audit/lib/Module/Audit/Service/FrontendTelemetryIngestService.php');
|
||||
|
||||
$jsKeys = $this->parseJsStringSet($js, 'ALLOWED_META_KEYS');
|
||||
$phpKeys = $this->parsePhpStringArrayConstant($ingest, 'ALLOWED_META_KEYS');
|
||||
@@ -48,7 +48,7 @@ class FrontendTelemetryContractTest extends TestCase
|
||||
public function testLengthLimitsStayInSyncAcrossFrontendAndBackend(): void
|
||||
{
|
||||
$js = $this->readProjectFile('web/js/core/app-telemetry.js');
|
||||
$ingest = $this->readProjectFile('lib/Service/Audit/FrontendTelemetryIngestService.php');
|
||||
$ingest = $this->readProjectFile('modules/audit/lib/Module/Audit/Service/FrontendTelemetryIngestService.php');
|
||||
|
||||
$this->assertSame(
|
||||
$this->parseJsIntConstant($js, 'MAX_MESSAGE_LENGTH'),
|
||||
|
||||
@@ -53,10 +53,10 @@ trait ListContractFiles
|
||||
'pages/admin/mail-log/data().php',
|
||||
'pages/admin/scheduled-jobs/data().php',
|
||||
'pages/admin/scheduled-jobs/runs-data($id).php',
|
||||
'pages/admin/api-audit/data().php',
|
||||
'pages/admin/import-audit/data().php',
|
||||
'pages/admin/user-lifecycle-audit/data().php',
|
||||
'pages/admin/system-audit/data().php',
|
||||
'modules/audit/pages/admin/api-audit/data().php',
|
||||
'modules/audit/pages/admin/import-audit/data().php',
|
||||
'modules/audit/pages/admin/user-lifecycle-audit/data().php',
|
||||
'modules/audit/pages/admin/system-audit/data().php',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -76,10 +76,10 @@ trait ListContractFiles
|
||||
'pages/admin/mail-log/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'",
|
||||
'pages/admin/scheduled-jobs/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'",
|
||||
'pages/admin/scheduled-jobs/runs-data($id).php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/runs-filter-schema.php'",
|
||||
'pages/admin/api-audit/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'",
|
||||
'pages/admin/import-audit/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'",
|
||||
'pages/admin/user-lifecycle-audit/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'",
|
||||
'pages/admin/system-audit/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'",
|
||||
'modules/audit/pages/admin/api-audit/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'",
|
||||
'modules/audit/pages/admin/import-audit/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'",
|
||||
'modules/audit/pages/admin/user-lifecycle-audit/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'",
|
||||
'modules/audit/pages/admin/system-audit/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'",
|
||||
];
|
||||
}
|
||||
|
||||
@@ -98,10 +98,10 @@ trait ListContractFiles
|
||||
'pages/admin/permissions/index(default).phtml',
|
||||
'pages/admin/mail-log/index(default).phtml',
|
||||
'pages/admin/scheduled-jobs/index(default).phtml',
|
||||
'pages/admin/api-audit/index(default).phtml',
|
||||
'pages/admin/import-audit/index(default).phtml',
|
||||
'pages/admin/user-lifecycle-audit/index(default).phtml',
|
||||
'pages/admin/system-audit/index(default).phtml',
|
||||
'modules/audit/pages/admin/api-audit/index(default).phtml',
|
||||
'modules/audit/pages/admin/import-audit/index(default).phtml',
|
||||
'modules/audit/pages/admin/user-lifecycle-audit/index(default).phtml',
|
||||
'modules/audit/pages/admin/system-audit/index(default).phtml',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -120,10 +120,10 @@ trait ListContractFiles
|
||||
'pages/admin/permissions/index().php',
|
||||
'pages/admin/mail-log/index().php',
|
||||
'pages/admin/scheduled-jobs/index().php',
|
||||
'pages/admin/api-audit/index().php',
|
||||
'pages/admin/import-audit/index().php',
|
||||
'pages/admin/user-lifecycle-audit/index().php',
|
||||
'pages/admin/system-audit/index().php',
|
||||
'modules/audit/pages/admin/api-audit/index().php',
|
||||
'modules/audit/pages/admin/import-audit/index().php',
|
||||
'modules/audit/pages/admin/user-lifecycle-audit/index().php',
|
||||
'modules/audit/pages/admin/system-audit/index().php',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -141,10 +141,10 @@ trait ListContractFiles
|
||||
'pages/admin/permissions/index(default).phtml',
|
||||
'pages/admin/mail-log/index(default).phtml',
|
||||
'pages/admin/scheduled-jobs/index(default).phtml',
|
||||
'pages/admin/api-audit/index(default).phtml',
|
||||
'pages/admin/import-audit/index(default).phtml',
|
||||
'pages/admin/user-lifecycle-audit/index(default).phtml',
|
||||
'pages/admin/system-audit/index(default).phtml',
|
||||
'modules/audit/pages/admin/api-audit/index(default).phtml',
|
||||
'modules/audit/pages/admin/import-audit/index(default).phtml',
|
||||
'modules/audit/pages/admin/user-lifecycle-audit/index(default).phtml',
|
||||
'modules/audit/pages/admin/system-audit/index(default).phtml',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -46,10 +46,10 @@ class ListDataEndpointContractTest extends TestCase
|
||||
public function testAuditRepositoriesDoNotUseLegacySingleIdAliasFallbacks(): void
|
||||
{
|
||||
$files = [
|
||||
'lib/Repository/Audit/ApiAuditLogRepository.php',
|
||||
'lib/Repository/Audit/ImportAuditRunRepository.php',
|
||||
'lib/Repository/Audit/UserLifecycleAuditRepository.php',
|
||||
'lib/Repository/Audit/SystemAuditLogRepository.php',
|
||||
'modules/audit/lib/Module/Audit/Repository/ApiAuditLogRepository.php',
|
||||
'modules/audit/lib/Module/Audit/Repository/ImportAuditRunRepository.php',
|
||||
'modules/audit/lib/Module/Audit/Repository/UserLifecycleAuditRepository.php',
|
||||
'modules/audit/lib/Module/Audit/Repository/SystemAuditLogRepository.php',
|
||||
];
|
||||
|
||||
foreach ($files as $file) {
|
||||
|
||||
@@ -17,10 +17,10 @@ final class ListTitlebarContractFiles
|
||||
'pages/admin/permissions/index(default).phtml',
|
||||
'pages/admin/mail-log/index(default).phtml',
|
||||
'pages/admin/scheduled-jobs/index(default).phtml',
|
||||
'pages/admin/api-audit/index(default).phtml',
|
||||
'pages/admin/import-audit/index(default).phtml',
|
||||
'pages/admin/user-lifecycle-audit/index(default).phtml',
|
||||
'pages/admin/system-audit/index(default).phtml',
|
||||
'modules/audit/pages/admin/api-audit/index(default).phtml',
|
||||
'modules/audit/pages/admin/import-audit/index(default).phtml',
|
||||
'modules/audit/pages/admin/user-lifecycle-audit/index(default).phtml',
|
||||
'modules/audit/pages/admin/system-audit/index(default).phtml',
|
||||
'modules/addressbook/pages/address-book/index(default).phtml',
|
||||
'pages/search/index(default).phtml',
|
||||
'pages/help/hotkeys(default).phtml',
|
||||
|
||||
@@ -26,10 +26,10 @@ class ListUiSharedPartialsContractTest extends TestCase
|
||||
{
|
||||
return [
|
||||
'pages/admin/scheduled-jobs/index(default).phtml',
|
||||
'pages/admin/api-audit/index(default).phtml',
|
||||
'pages/admin/import-audit/index(default).phtml',
|
||||
'pages/admin/user-lifecycle-audit/index(default).phtml',
|
||||
'pages/admin/system-audit/index(default).phtml',
|
||||
'modules/audit/pages/admin/api-audit/index(default).phtml',
|
||||
'modules/audit/pages/admin/import-audit/index(default).phtml',
|
||||
'modules/audit/pages/admin/user-lifecycle-audit/index(default).phtml',
|
||||
'modules/audit/pages/admin/system-audit/index(default).phtml',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ final class SecurityLoggingRedactionContractTest extends TestCase
|
||||
|
||||
public function testSystemAuditRedactionCoversCredentialsTokensAndEmailFields(): void
|
||||
{
|
||||
$content = $this->readProjectFile('lib/Service/Audit/SystemAuditRedactionService.php');
|
||||
$content = $this->readProjectFile('modules/audit/lib/Module/Audit/Service/SystemAuditRedactionService.php');
|
||||
|
||||
foreach ([
|
||||
"'password'",
|
||||
@@ -37,7 +37,7 @@ final class SecurityLoggingRedactionContractTest extends TestCase
|
||||
|
||||
public function testApiAuditRedactionCoversCredentialsAndAuthorizationFields(): void
|
||||
{
|
||||
$content = $this->readProjectFile('lib/Service/Audit/ApiAuditService.php');
|
||||
$content = $this->readProjectFile('modules/audit/lib/Module/Audit/Service/ApiAuditService.php');
|
||||
|
||||
foreach ([
|
||||
"'token'",
|
||||
|
||||
@@ -12,17 +12,18 @@ final class SecurityLoggingRuntimeContractTest extends TestCase
|
||||
{
|
||||
$violations = array_merge(
|
||||
$this->findPatternMatchesInPhpFiles('lib', '/\berror_log\s*\(/'),
|
||||
$this->findPatternMatchesInPhpFiles('pages', '/\berror_log\s*\(/')
|
||||
$this->findPatternMatchesInPhpFiles('pages', '/\berror_log\s*\(/'),
|
||||
$this->findPatternMatchesInPhpFiles('modules', '/\berror_log\s*\(/')
|
||||
);
|
||||
sort($violations);
|
||||
|
||||
self::assertSame(
|
||||
['pages/admin/frontend-telemetry/ingest().php'],
|
||||
['modules/audit/pages/admin/frontend-telemetry/ingest().php'],
|
||||
$violations,
|
||||
"Unexpected error_log() usage outside the telemetry ingest fallback:\n" . implode("\n", $violations)
|
||||
);
|
||||
|
||||
$content = $this->readProjectFile('pages/admin/frontend-telemetry/ingest().php');
|
||||
$content = $this->readProjectFile('modules/audit/pages/admin/frontend-telemetry/ingest().php');
|
||||
self::assertStringContainsString(
|
||||
"error_log('[frontend-telemetry] ingest record_failed request_id=' . RequestContext::id());",
|
||||
$content
|
||||
|
||||
@@ -10,7 +10,7 @@ final class SecurityLoggingTelemetryContractTest extends TestCase
|
||||
|
||||
public function testFrontendTelemetrySanitizesSecretsAndPiiMarkers(): void
|
||||
{
|
||||
$content = $this->readProjectFile('lib/Service/Audit/FrontendTelemetryIngestService.php');
|
||||
$content = $this->readProjectFile('modules/audit/lib/Module/Audit/Service/FrontendTelemetryIngestService.php');
|
||||
|
||||
self::assertStringContainsString('[REDACTED]', $content);
|
||||
self::assertStringContainsString('[REDACTED_EMAIL]', $content);
|
||||
|
||||
@@ -10,12 +10,12 @@ final class StatusTaxonomyContractFiles
|
||||
public static function taxonomyEnumFiles(): array
|
||||
{
|
||||
return [
|
||||
'lib/Domain/Taxonomy/SystemAuditOutcome.php',
|
||||
'lib/Domain/Taxonomy/SystemAuditChannel.php',
|
||||
'lib/Domain/Taxonomy/UserLifecycleAction.php',
|
||||
'lib/Domain/Taxonomy/UserLifecycleTriggerType.php',
|
||||
'lib/Domain/Taxonomy/UserLifecycleStatus.php',
|
||||
'lib/Domain/Taxonomy/ImportAuditStatus.php',
|
||||
'modules/audit/lib/Module/Audit/Domain/SystemAuditOutcome.php',
|
||||
'modules/audit/lib/Module/Audit/Domain/SystemAuditChannel.php',
|
||||
'modules/audit/lib/Module/Audit/Domain/UserLifecycleAction.php',
|
||||
'modules/audit/lib/Module/Audit/Domain/UserLifecycleTriggerType.php',
|
||||
'modules/audit/lib/Module/Audit/Domain/UserLifecycleStatus.php',
|
||||
'modules/audit/lib/Module/Audit/Domain/ImportAuditStatus.php',
|
||||
'lib/Domain/Taxonomy/ScheduledJobStatus.php',
|
||||
'lib/Domain/Taxonomy/ScheduledJobRunStatus.php',
|
||||
'lib/Domain/Taxonomy/ScheduledJobTriggerType.php',
|
||||
@@ -31,16 +31,16 @@ final class StatusTaxonomyContractFiles
|
||||
public static function taxonomySchemaExpectations(): array
|
||||
{
|
||||
return [
|
||||
'pages/admin/system-audit/filter-schema.php' => [
|
||||
'modules/audit/pages/admin/system-audit/filter-schema.php' => [
|
||||
'SystemAuditOutcome::values()',
|
||||
'SystemAuditChannel::values()',
|
||||
],
|
||||
'pages/admin/user-lifecycle-audit/filter-schema.php' => [
|
||||
'modules/audit/pages/admin/user-lifecycle-audit/filter-schema.php' => [
|
||||
'gridEnumSanitizer(UserLifecycleAction::class)',
|
||||
'gridEnumSanitizer(UserLifecycleStatus::class)',
|
||||
'gridEnumSanitizer(UserLifecycleTriggerType::class)',
|
||||
],
|
||||
'pages/admin/import-audit/filter-schema.php' => [
|
||||
'modules/audit/pages/admin/import-audit/filter-schema.php' => [
|
||||
'ImportAuditStatus::values()',
|
||||
],
|
||||
'pages/admin/scheduled-jobs/filter-schema.php' => [
|
||||
@@ -62,9 +62,9 @@ final class StatusTaxonomyContractFiles
|
||||
public static function taxonomyDataEndpointFiles(): array
|
||||
{
|
||||
return [
|
||||
'pages/admin/system-audit/data().php',
|
||||
'pages/admin/user-lifecycle-audit/data().php',
|
||||
'pages/admin/import-audit/data().php',
|
||||
'modules/audit/pages/admin/system-audit/data().php',
|
||||
'modules/audit/pages/admin/user-lifecycle-audit/data().php',
|
||||
'modules/audit/pages/admin/import-audit/data().php',
|
||||
'pages/admin/scheduled-jobs/data().php',
|
||||
'pages/admin/scheduled-jobs/runs-data($id).php',
|
||||
'pages/admin/mail-log/data().php',
|
||||
@@ -78,13 +78,13 @@ final class StatusTaxonomyContractFiles
|
||||
public static function taxonomyLiteralGuardFiles(): array
|
||||
{
|
||||
return [
|
||||
'lib/Service/Audit/SystemAuditService.php',
|
||||
'lib/Repository/Audit/SystemAuditLogRepository.php',
|
||||
'lib/Http/ApiSystemAuditReporter.php',
|
||||
'lib/Service/Audit/UserLifecycleAuditService.php',
|
||||
'lib/Repository/Audit/UserLifecycleAuditRepository.php',
|
||||
'lib/Service/Audit/ImportAuditService.php',
|
||||
'lib/Repository/Audit/ImportAuditRunRepository.php',
|
||||
'modules/audit/lib/Module/Audit/Service/SystemAuditService.php',
|
||||
'modules/audit/lib/Module/Audit/Repository/SystemAuditLogRepository.php',
|
||||
'modules/audit/lib/Module/Audit/Http/ApiSystemAuditReporter.php',
|
||||
'modules/audit/lib/Module/Audit/Service/UserLifecycleAuditService.php',
|
||||
'modules/audit/lib/Module/Audit/Repository/UserLifecycleAuditRepository.php',
|
||||
'modules/audit/lib/Module/Audit/Service/ImportAuditService.php',
|
||||
'modules/audit/lib/Module/Audit/Repository/ImportAuditRunRepository.php',
|
||||
'lib/Service/Scheduler/SchedulerRunService.php',
|
||||
'lib/Repository/Scheduler/ScheduledJobRepository.php',
|
||||
'lib/Repository/Scheduler/ScheduledJobRunRepository.php',
|
||||
@@ -94,16 +94,16 @@ final class StatusTaxonomyContractFiles
|
||||
'lib/Service/Tenant/TenantService.php',
|
||||
'lib/Repository/Tenant/TenantRepository.php',
|
||||
'lib/Repository/Stats/AdminStatsRepository.php',
|
||||
'pages/admin/system-audit/data().php',
|
||||
'pages/admin/user-lifecycle-audit/data().php',
|
||||
'pages/admin/import-audit/data().php',
|
||||
'modules/audit/pages/admin/system-audit/data().php',
|
||||
'modules/audit/pages/admin/user-lifecycle-audit/data().php',
|
||||
'modules/audit/pages/admin/import-audit/data().php',
|
||||
'pages/admin/scheduled-jobs/data().php',
|
||||
'pages/admin/scheduled-jobs/runs-data($id).php',
|
||||
'pages/admin/mail-log/data().php',
|
||||
'pages/admin/tenants/data().php',
|
||||
'pages/admin/system-audit/index().php',
|
||||
'pages/admin/user-lifecycle-audit/index().php',
|
||||
'pages/admin/import-audit/index().php',
|
||||
'modules/audit/pages/admin/system-audit/index().php',
|
||||
'modules/audit/pages/admin/user-lifecycle-audit/index().php',
|
||||
'modules/audit/pages/admin/import-audit/index().php',
|
||||
'pages/admin/scheduled-jobs/index().php',
|
||||
'pages/admin/mail-log/index().php',
|
||||
];
|
||||
|
||||
@@ -10,11 +10,11 @@ final class StatusTaxonomyUiContractTest extends TestCase
|
||||
|
||||
public function testImportAuditIndexTemplateDoesNotUseLegacyStatusMapper(): void
|
||||
{
|
||||
$content = $this->readProjectFile('pages/admin/import-audit/index(default).phtml');
|
||||
$moduleContent = $this->readProjectFile('web/js/pages/admin-import-audit-index.js');
|
||||
$content = $this->readProjectFile('modules/audit/pages/admin/import-audit/index(default).phtml');
|
||||
$moduleContent = $this->readProjectFile('modules/audit/web/js/pages/admin-import-audit-index.js');
|
||||
$this->assertStringNotContainsString('const statusLabel = (value) =>', $moduleContent);
|
||||
$this->assertStringNotContainsString("if (key === 'running')", $moduleContent);
|
||||
$this->assertStringContainsString('cell.label || \'-\'', $moduleContent);
|
||||
$this->assertStringContainsString("assetVersion('js/pages/admin-import-audit-index.js')", $content);
|
||||
$this->assertStringContainsString("assetVersion('modules/audit/js/pages/admin-import-audit-index.js')", $content);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
|
||||
namespace MintyPHP\Tests\Domain\Taxonomy;
|
||||
|
||||
use MintyPHP\Domain\Taxonomy\ImportAuditStatus;
|
||||
use MintyPHP\Domain\Taxonomy\MailLogStatus;
|
||||
use MintyPHP\Domain\Taxonomy\ScheduledJobRunStatus;
|
||||
use MintyPHP\Domain\Taxonomy\ScheduledJobStatus;
|
||||
use MintyPHP\Domain\Taxonomy\ScheduledJobTriggerType;
|
||||
use MintyPHP\Domain\Taxonomy\SchedulerRuntimeResult;
|
||||
use MintyPHP\Domain\Taxonomy\SystemAuditChannel;
|
||||
use MintyPHP\Domain\Taxonomy\SystemAuditOutcome;
|
||||
use MintyPHP\Domain\Taxonomy\TenantStatus;
|
||||
use MintyPHP\Domain\Taxonomy\UserLifecycleAction;
|
||||
use MintyPHP\Domain\Taxonomy\UserLifecycleStatus;
|
||||
use MintyPHP\Domain\Taxonomy\UserLifecycleTriggerType;
|
||||
use MintyPHP\Module\Audit\Domain\ImportAuditStatus;
|
||||
use MintyPHP\Module\Audit\Domain\SystemAuditChannel;
|
||||
use MintyPHP\Module\Audit\Domain\SystemAuditOutcome;
|
||||
use MintyPHP\Module\Audit\Domain\UserLifecycleAction;
|
||||
use MintyPHP\Module\Audit\Domain\UserLifecycleStatus;
|
||||
use MintyPHP\Module\Audit\Domain\UserLifecycleTriggerType;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Http;
|
||||
|
||||
use MintyPHP\Http\ApiAuth;
|
||||
use MintyPHP\Http\ApiSystemAuditReporter;
|
||||
use MintyPHP\Http\RequestContext;
|
||||
use MintyPHP\Service\Audit\SystemAuditService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ApiSystemAuditReporterTest extends TestCase
|
||||
{
|
||||
private array $serverBackup = [];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->serverBackup = $_SERVER;
|
||||
$_SERVER = [];
|
||||
RequestContext::resetForTests();
|
||||
ApiAuth::authenticate();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$_SERVER = $this->serverBackup;
|
||||
RequestContext::resetForTests();
|
||||
ApiAuth::authenticate();
|
||||
}
|
||||
|
||||
public function testItRecordsMutatingRequest(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'POST';
|
||||
$_SERVER['REQUEST_URI'] = '/api/v1/users/123';
|
||||
|
||||
$audit = $this->createMock(SystemAuditService::class);
|
||||
$audit->expects($this->once())
|
||||
->method('record')
|
||||
->with(
|
||||
'api.request',
|
||||
'success',
|
||||
$this->callback(static function (array $context): bool {
|
||||
$metadata = is_array($context['metadata'] ?? null) ? $context['metadata'] : [];
|
||||
return ($metadata['endpoint_key'] ?? '') === '/api/v1/users/{id}'
|
||||
&& ($metadata['status_code'] ?? null) === 201
|
||||
&& ($metadata['status_class'] ?? '') === '2xx'
|
||||
&& ($metadata['auth_mode'] ?? '') === 'public'
|
||||
&& ($metadata['write_method'] ?? null) === true
|
||||
&& ($metadata['security_endpoint'] ?? null) === false;
|
||||
})
|
||||
);
|
||||
|
||||
$reporter = new ApiSystemAuditReporter($audit);
|
||||
$reporter->start();
|
||||
$reporter->finish(201);
|
||||
}
|
||||
|
||||
public function testItSkipsNonSecurityGetSuccess(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
$_SERVER['REQUEST_URI'] = '/api/v1/users';
|
||||
|
||||
$audit = $this->createMock(SystemAuditService::class);
|
||||
$audit->expects($this->never())->method('record');
|
||||
|
||||
$reporter = new ApiSystemAuditReporter($audit);
|
||||
$reporter->start();
|
||||
$reporter->finish(200);
|
||||
}
|
||||
|
||||
public function testItRecordsDeniedSecurityGetFailure(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
$_SERVER['REQUEST_URI'] = '/api/v1/me/tokens';
|
||||
|
||||
$audit = $this->createMock(SystemAuditService::class);
|
||||
$audit->expects($this->once())
|
||||
->method('record')
|
||||
->with(
|
||||
'api.request',
|
||||
'denied',
|
||||
$this->callback(static function (array $context): bool {
|
||||
$metadata = is_array($context['metadata'] ?? null) ? $context['metadata'] : [];
|
||||
return ($metadata['endpoint_key'] ?? '') === '/api/v1/me/tokens'
|
||||
&& ($metadata['status_code'] ?? null) === 401
|
||||
&& ($metadata['status_class'] ?? '') === '4xx'
|
||||
&& ($metadata['security_endpoint'] ?? null) === true;
|
||||
})
|
||||
);
|
||||
|
||||
$reporter = new ApiSystemAuditReporter($audit);
|
||||
$reporter->start();
|
||||
$reporter->finish(401, 'unauthorized');
|
||||
}
|
||||
|
||||
public function testItRecordsFailedServerError(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
$_SERVER['REQUEST_URI'] = '/api/v1/users';
|
||||
|
||||
$audit = $this->createMock(SystemAuditService::class);
|
||||
$audit->expects($this->once())
|
||||
->method('record')
|
||||
->with(
|
||||
'api.request',
|
||||
'failed',
|
||||
$this->callback(static function (array $context): bool {
|
||||
$metadata = is_array($context['metadata'] ?? null) ? $context['metadata'] : [];
|
||||
return ($metadata['status_code'] ?? null) === 500
|
||||
&& ($metadata['status_class'] ?? '') === '5xx'
|
||||
&& ($metadata['write_method'] ?? null) === false;
|
||||
})
|
||||
);
|
||||
|
||||
$reporter = new ApiSystemAuditReporter($audit);
|
||||
$reporter->start();
|
||||
$reporter->finish(500, 'unexpected_error');
|
||||
}
|
||||
|
||||
public function testFinishIsIdempotent(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'POST';
|
||||
$_SERVER['REQUEST_URI'] = '/api/v1/tenants';
|
||||
|
||||
$audit = $this->createMock(SystemAuditService::class);
|
||||
$audit->expects($this->once())->method('record');
|
||||
|
||||
$reporter = new ApiSystemAuditReporter($audit);
|
||||
$reporter->start();
|
||||
$reporter->finish(201);
|
||||
$reporter->finish(201);
|
||||
}
|
||||
}
|
||||
@@ -110,20 +110,6 @@ class OperationsAuthorizationPolicyTest extends TestCase
|
||||
$this->assertForbiddenDecision($decision);
|
||||
}
|
||||
|
||||
public function testSystemAuditPurgeAllowedWithPermission(): void
|
||||
{
|
||||
$permissionService = $this->permissionGatewayAllowing([
|
||||
3 => [PermissionService::SYSTEM_AUDIT_PURGE],
|
||||
]);
|
||||
|
||||
$policy = new OperationsAuthorizationPolicy($permissionService);
|
||||
$decision = $policy->authorize(OperationsAuthorizationPolicy::ABILITY_ADMIN_SYSTEM_AUDIT_PURGE, [
|
||||
'actor_user_id' => 3,
|
||||
]);
|
||||
|
||||
$this->assertTrue($decision->isAllowed());
|
||||
}
|
||||
|
||||
// --- authorize: authorizeUsersCreateCustomFields (requires TWO permissions) ---
|
||||
|
||||
public function testUsersCreateCustomFieldsAllowedWithBothPermissions(): void
|
||||
|
||||
@@ -7,7 +7,7 @@ use MintyPHP\Repository\Access\PermissionRepositoryInterface;
|
||||
use MintyPHP\Repository\Access\RolePermissionRepositoryInterface;
|
||||
use MintyPHP\Repository\Access\UserRoleRepositoryInterface;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Audit\SystemAuditService;
|
||||
use MintyPHP\Service\Audit\AuditRecorderInterface;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
@@ -16,7 +16,7 @@ class PermissionServiceTest extends TestCase
|
||||
private PermissionRepositoryInterface&MockObject $permissionRepository;
|
||||
private RolePermissionRepositoryInterface&MockObject $rolePermissionRepository;
|
||||
private UserRoleRepositoryInterface&MockObject $userRoleRepository;
|
||||
private SystemAuditService&MockObject $systemAuditService;
|
||||
private AuditRecorderInterface&MockObject $systemAuditService;
|
||||
private SessionStoreInterface&MockObject $sessionStore;
|
||||
private PermissionService $service;
|
||||
|
||||
@@ -25,7 +25,7 @@ class PermissionServiceTest extends TestCase
|
||||
$this->permissionRepository = $this->createMock(PermissionRepositoryInterface::class);
|
||||
$this->rolePermissionRepository = $this->createMock(RolePermissionRepositoryInterface::class);
|
||||
$this->userRoleRepository = $this->createMock(UserRoleRepositoryInterface::class);
|
||||
$this->systemAuditService = $this->createMock(SystemAuditService::class);
|
||||
$this->systemAuditService = $this->createMock(AuditRecorderInterface::class);
|
||||
$this->sessionStore = $this->createMock(SessionStoreInterface::class);
|
||||
|
||||
$this->service = new PermissionService(
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace MintyPHP\Tests\Service\Access;
|
||||
|
||||
use MintyPHP\Repository\Access\RoleRepositoryInterface;
|
||||
use MintyPHP\Service\Access\RoleService;
|
||||
use MintyPHP\Service\Audit\SystemAuditService;
|
||||
use MintyPHP\Service\Audit\AuditRecorderInterface;
|
||||
use MintyPHP\Service\Directory\DirectorySettingsGateway;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@@ -13,14 +13,14 @@ class RoleServiceTest extends TestCase
|
||||
{
|
||||
private RoleRepositoryInterface&MockObject $roleRepo;
|
||||
private DirectorySettingsGateway&MockObject $settingsGateway;
|
||||
private SystemAuditService&MockObject $auditService;
|
||||
private AuditRecorderInterface&MockObject $auditService;
|
||||
private RoleService $service;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->roleRepo = $this->createMock(RoleRepositoryInterface::class);
|
||||
$this->settingsGateway = $this->createMock(DirectorySettingsGateway::class);
|
||||
$this->auditService = $this->createMock(SystemAuditService::class);
|
||||
$this->auditService = $this->createMock(AuditRecorderInterface::class);
|
||||
|
||||
$this->service = new RoleService(
|
||||
$this->roleRepo,
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Audit;
|
||||
|
||||
use MintyPHP\Http\RequestContext;
|
||||
use MintyPHP\Http\RequestRuntime;
|
||||
use MintyPHP\Repository\Audit\ApiAuditLogRepositoryInterface;
|
||||
use MintyPHP\Service\Audit\ApiAuditService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ApiAuditServiceTest extends TestCase
|
||||
{
|
||||
private array $serverBackup = [];
|
||||
private array $getBackup = [];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->serverBackup = $_SERVER;
|
||||
$this->getBackup = $_GET;
|
||||
RequestContext::resetForTests();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$_SERVER = $this->serverBackup;
|
||||
$_GET = $this->getBackup;
|
||||
RequestContext::resetForTests();
|
||||
}
|
||||
|
||||
public function testCurrentRequestIdIsNullBeforeStart(): void
|
||||
{
|
||||
$service = new ApiAuditService(
|
||||
$this->createMock(ApiAuditLogRepositoryInterface::class),
|
||||
new RequestRuntime()
|
||||
);
|
||||
$this->assertNull($service->currentRequestId());
|
||||
}
|
||||
|
||||
public function testCurrentRequestIdIsGeneratedForRegularRequests(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
$_SERVER['REQUEST_URI'] = '/api/v1/me?x=1';
|
||||
$_GET = ['x' => '1'];
|
||||
|
||||
$service = new ApiAuditService(
|
||||
$this->createMock(ApiAuditLogRepositoryInterface::class),
|
||||
new RequestRuntime()
|
||||
);
|
||||
$service->startRequestContext();
|
||||
|
||||
$requestId = $service->currentRequestId();
|
||||
$this->assertNotNull($requestId);
|
||||
$this->assertMatchesRegularExpression(
|
||||
'/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i',
|
||||
$requestId
|
||||
);
|
||||
|
||||
$service->startRequestContext();
|
||||
$this->assertSame($requestId, $service->currentRequestId());
|
||||
}
|
||||
|
||||
public function testCurrentRequestIdStaysNullForOptionsRequests(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'OPTIONS';
|
||||
$_SERVER['REQUEST_URI'] = '/api/v1/me';
|
||||
$_GET = [];
|
||||
|
||||
$service = new ApiAuditService(
|
||||
$this->createMock(ApiAuditLogRepositoryInterface::class),
|
||||
new RequestRuntime()
|
||||
);
|
||||
$service->startRequestContext();
|
||||
|
||||
$this->assertNull($service->currentRequestId());
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Audit;
|
||||
|
||||
use MintyPHP\Repository\User\UserReadRepositoryInterface;
|
||||
use MintyPHP\Service\Audit\AuditMetadataEnricher;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class AuditMetadataEnricherTest extends TestCase
|
||||
{
|
||||
private function newEnricher(UserReadRepositoryInterface $userReadRepository): AuditMetadataEnricher
|
||||
{
|
||||
return new AuditMetadataEnricher($userReadRepository);
|
||||
}
|
||||
|
||||
public function testEnrichResolvesCreatedByAndModifiedBy(): void
|
||||
{
|
||||
$repo = $this->createMock(UserReadRepositoryInterface::class);
|
||||
$repo->method('find')->willReturnMap([
|
||||
[10, ['first_name' => 'Alice', 'last_name' => 'Smith', 'email' => 'alice@example.com', 'uuid' => 'uuid-10']],
|
||||
[20, ['first_name' => 'Bob', 'last_name' => 'Jones', 'email' => 'bob@example.com', 'uuid' => 'uuid-20']],
|
||||
]);
|
||||
|
||||
$entity = ['created_by' => 10, 'modified_by' => 20];
|
||||
$this->newEnricher($repo)->enrich($entity);
|
||||
|
||||
$this->assertSame('Alice Smith', $entity['created_by_label']);
|
||||
$this->assertSame('uuid-10', $entity['created_by_uuid']);
|
||||
$this->assertSame('Bob Jones', $entity['modified_by_label']);
|
||||
$this->assertSame('uuid-20', $entity['modified_by_uuid']);
|
||||
}
|
||||
|
||||
public function testEnrichFallsBackToEmailWhenNameEmpty(): void
|
||||
{
|
||||
$repo = $this->createMock(UserReadRepositoryInterface::class);
|
||||
$repo->method('find')->willReturn(['first_name' => '', 'last_name' => '', 'email' => 'no-name@example.com', 'uuid' => 'uuid-x']);
|
||||
|
||||
$entity = ['created_by' => 5];
|
||||
$this->newEnricher($repo)->enrich($entity, ['created_by']);
|
||||
|
||||
$this->assertSame('no-name@example.com', $entity['created_by_label']);
|
||||
$this->assertSame('uuid-x', $entity['created_by_uuid']);
|
||||
}
|
||||
|
||||
public function testEnrichSkipsZeroAndNegativeIds(): void
|
||||
{
|
||||
$repo = $this->createMock(UserReadRepositoryInterface::class);
|
||||
$repo->expects($this->never())->method('find');
|
||||
|
||||
$entity = ['created_by' => 0, 'modified_by' => -1];
|
||||
$this->newEnricher($repo)->enrich($entity);
|
||||
|
||||
$this->assertArrayNotHasKey('created_by_label', $entity);
|
||||
$this->assertArrayNotHasKey('modified_by_label', $entity);
|
||||
}
|
||||
|
||||
public function testEnrichSkipsMissingFields(): void
|
||||
{
|
||||
$repo = $this->createMock(UserReadRepositoryInterface::class);
|
||||
$repo->expects($this->never())->method('find');
|
||||
|
||||
$entity = [];
|
||||
$this->newEnricher($repo)->enrich($entity, ['created_by', 'modified_by']);
|
||||
|
||||
$this->assertArrayNotHasKey('created_by_label', $entity);
|
||||
}
|
||||
|
||||
public function testEnrichHandlesUserNotFound(): void
|
||||
{
|
||||
$repo = $this->createMock(UserReadRepositoryInterface::class);
|
||||
$repo->method('find')->willReturn(null);
|
||||
|
||||
$entity = ['created_by' => 999];
|
||||
$this->newEnricher($repo)->enrich($entity, ['created_by']);
|
||||
|
||||
$this->assertArrayNotHasKey('created_by_label', $entity);
|
||||
}
|
||||
|
||||
public function testEnrichWithCustomFields(): void
|
||||
{
|
||||
$repo = $this->createMock(UserReadRepositoryInterface::class);
|
||||
$repo->method('find')->willReturn(['first_name' => 'Admin', 'last_name' => '', 'email' => 'admin@co.com', 'uuid' => 'uuid-a']);
|
||||
|
||||
$entity = ['status_changed_by' => 7, 'active_changed_by' => 7];
|
||||
$this->newEnricher($repo)->enrich($entity, ['status_changed_by', 'active_changed_by']);
|
||||
|
||||
$this->assertSame('Admin', $entity['status_changed_by_label']);
|
||||
$this->assertSame('uuid-a', $entity['status_changed_by_uuid']);
|
||||
$this->assertSame('Admin', $entity['active_changed_by_label']);
|
||||
$this->assertSame('uuid-a', $entity['active_changed_by_uuid']);
|
||||
}
|
||||
}
|
||||
@@ -1,332 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Audit;
|
||||
|
||||
use MintyPHP\Http\RequestContext;
|
||||
use MintyPHP\Http\SessionStoreInterface;
|
||||
use MintyPHP\Service\Audit\FrontendTelemetryIngestService;
|
||||
use MintyPHP\Service\Audit\SystemAuditService;
|
||||
use MintyPHP\Service\Security\RateLimiterService;
|
||||
use MintyPHP\Service\Settings\SettingsFrontendTelemetryGateway;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class FrontendTelemetryIngestServiceTest extends TestCase
|
||||
{
|
||||
protected function tearDown(): void
|
||||
{
|
||||
RequestContext::resetForTests();
|
||||
$_SESSION = [];
|
||||
}
|
||||
|
||||
public function testIngestSkipsWhenDisabled(): void
|
||||
{
|
||||
$audit = $this->createMock(SystemAuditService::class);
|
||||
$audit->expects($this->never())->method('record');
|
||||
|
||||
$gateway = $this->createMock(SettingsFrontendTelemetryGateway::class);
|
||||
$gateway->method('isFrontendTelemetryEnabled')->willReturn(false);
|
||||
|
||||
$rateLimiter = $this->createMock(RateLimiterService::class);
|
||||
$sessionStore = $this->createMock(SessionStoreInterface::class);
|
||||
$sessionStore->method('all')->willReturn([]);
|
||||
|
||||
$service = new FrontendTelemetryIngestService($audit, $gateway, $rateLimiter, $sessionStore);
|
||||
$result = $service->ingest([
|
||||
'event_type' => 'frontend.warn_once',
|
||||
'message' => 'warn',
|
||||
]);
|
||||
|
||||
$this->assertSame(['accepted' => false, 'reason' => 'disabled'], $result);
|
||||
}
|
||||
|
||||
public function testIngestRejectsInvalidPayload(): void
|
||||
{
|
||||
$audit = $this->createMock(SystemAuditService::class);
|
||||
$audit->expects($this->never())->method('record');
|
||||
|
||||
$gateway = $this->createMock(SettingsFrontendTelemetryGateway::class);
|
||||
$gateway->method('isFrontendTelemetryEnabled')->willReturn(true);
|
||||
|
||||
$rateLimiter = $this->createMock(RateLimiterService::class);
|
||||
$sessionStore = $this->createMock(SessionStoreInterface::class);
|
||||
$sessionStore->method('all')->willReturn([]);
|
||||
|
||||
$service = new FrontendTelemetryIngestService($audit, $gateway, $rateLimiter, $sessionStore);
|
||||
$result = $service->ingest([
|
||||
'event_type' => 'unknown',
|
||||
'message' => '',
|
||||
]);
|
||||
|
||||
$this->assertSame(['accepted' => false, 'reason' => 'invalid'], $result);
|
||||
}
|
||||
|
||||
public function testIngestRecordsSanitizedPayload(): void
|
||||
{
|
||||
$audit = $this->createMock(SystemAuditService::class);
|
||||
$audit->expects($this->once())
|
||||
->method('record')
|
||||
->with(
|
||||
'frontend.warn_once',
|
||||
'success',
|
||||
$this->callback(function (array $context): bool {
|
||||
$metadata = $context['metadata'] ?? [];
|
||||
$message = (string) ($metadata['message'] ?? '');
|
||||
if (!str_contains($message, '[REDACTED_EMAIL]')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!str_contains($message, 'token=[REDACTED]')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$meta = is_array($metadata['meta'] ?? null) ? $metadata['meta'] : [];
|
||||
$requestPath = (string) ($meta['request_path'] ?? '');
|
||||
return $requestPath === '/admin/users/:id';
|
||||
})
|
||||
)
|
||||
->willReturn(5);
|
||||
|
||||
$gateway = $this->createMock(SettingsFrontendTelemetryGateway::class);
|
||||
$gateway->method('isFrontendTelemetryEnabled')->willReturn(true);
|
||||
$gateway->method('getFrontendTelemetryAllowedEvents')->willReturn(['warn_once', 'ajax_error']);
|
||||
$gateway->method('getFrontendTelemetrySampleRate')->willReturn(1.0);
|
||||
|
||||
$rateLimiter = $this->createMock(RateLimiterService::class);
|
||||
$rateLimiter->method('hit')->willReturn(['allowed' => true, 'retry_after' => 0]);
|
||||
|
||||
$sessionStore = $this->createMock(SessionStoreInterface::class);
|
||||
$sessionStore->method('all')->willReturn(['user' => ['id' => 7]]);
|
||||
|
||||
$service = new FrontendTelemetryIngestService($audit, $gateway, $rateLimiter, $sessionStore);
|
||||
$result = $service->ingest([
|
||||
'event_type' => 'frontend.warn_once',
|
||||
'severity' => 'warning',
|
||||
'message' => 'Problem for user test@example.com token=supersecret1234567890',
|
||||
'fingerprint' => 'v1_warn_once_123',
|
||||
'meta' => ['request_path' => '/admin/users/123?email=a@b.de'],
|
||||
'occurred_at' => '2026-03-05T12:00:00+01:00',
|
||||
]);
|
||||
|
||||
$this->assertSame(['accepted' => true, 'reason' => 'logged'], $result);
|
||||
}
|
||||
|
||||
public function testIngestBuildsFallbackFingerprintUsingRouteBeforeMessage(): void
|
||||
{
|
||||
$audit = $this->createMock(SystemAuditService::class);
|
||||
$audit->expects($this->once())
|
||||
->method('record')
|
||||
->with(
|
||||
'frontend.warn_once',
|
||||
'success',
|
||||
$this->callback(static function (array $context): bool {
|
||||
$metadata = is_array($context['metadata'] ?? null) ? $context['metadata'] : [];
|
||||
$fingerprintHash = (string) ($metadata['fingerprint_hash'] ?? '');
|
||||
$expectedFingerprint = 'v1_' . substr(hash('sha256', 'frontend.warn_once|/admin/users/:id|Grid init failed'), 0, 24);
|
||||
return $fingerprintHash === RequestContext::hashValue($expectedFingerprint);
|
||||
})
|
||||
)
|
||||
->willReturn(6);
|
||||
|
||||
$gateway = $this->createMock(SettingsFrontendTelemetryGateway::class);
|
||||
$gateway->method('isFrontendTelemetryEnabled')->willReturn(true);
|
||||
$gateway->method('getFrontendTelemetryAllowedEvents')->willReturn(['warn_once', 'ajax_error']);
|
||||
$gateway->method('getFrontendTelemetrySampleRate')->willReturn(1.0);
|
||||
|
||||
$rateLimiter = $this->createMock(RateLimiterService::class);
|
||||
$rateLimiter->method('hit')->willReturn(['allowed' => true, 'retry_after' => 0]);
|
||||
|
||||
$sessionStore = $this->createMock(SessionStoreInterface::class);
|
||||
$sessionStore->method('all')->willReturn(['user' => ['id' => 7]]);
|
||||
|
||||
$service = new FrontendTelemetryIngestService($audit, $gateway, $rateLimiter, $sessionStore);
|
||||
$result = $service->ingest([
|
||||
'event_type' => 'frontend.warn_once',
|
||||
'severity' => 'warning',
|
||||
'message' => 'Grid init failed',
|
||||
'meta' => ['location' => '/admin/users/123'],
|
||||
'occurred_at' => '2026-03-05T12:00:00+01:00',
|
||||
]);
|
||||
|
||||
$this->assertSame(['accepted' => true, 'reason' => 'logged'], $result);
|
||||
}
|
||||
|
||||
public function testIngestRejectsInvalidPageRequestIdPattern(): void
|
||||
{
|
||||
$audit = $this->createMock(SystemAuditService::class);
|
||||
$audit->expects($this->once())
|
||||
->method('record')
|
||||
->with(
|
||||
'frontend.warn_once',
|
||||
'success',
|
||||
$this->callback(static function (array $context): bool {
|
||||
$metadata = is_array($context['metadata'] ?? null) ? $context['metadata'] : [];
|
||||
$meta = is_array($metadata['meta'] ?? null) ? $metadata['meta'] : [];
|
||||
return !array_key_exists('page_request_id', $meta);
|
||||
})
|
||||
)
|
||||
->willReturn(8);
|
||||
|
||||
$gateway = $this->createMock(SettingsFrontendTelemetryGateway::class);
|
||||
$gateway->method('isFrontendTelemetryEnabled')->willReturn(true);
|
||||
$gateway->method('getFrontendTelemetryAllowedEvents')->willReturn(['warn_once', 'ajax_error']);
|
||||
$gateway->method('getFrontendTelemetrySampleRate')->willReturn(1.0);
|
||||
|
||||
$rateLimiter = $this->createMock(RateLimiterService::class);
|
||||
$rateLimiter->method('hit')->willReturn(['allowed' => true, 'retry_after' => 0]);
|
||||
|
||||
$sessionStore = $this->createMock(SessionStoreInterface::class);
|
||||
$sessionStore->method('all')->willReturn(['user' => ['id' => 7]]);
|
||||
|
||||
$service = new FrontendTelemetryIngestService($audit, $gateway, $rateLimiter, $sessionStore);
|
||||
$result = $service->ingest([
|
||||
'event_type' => 'frontend.warn_once',
|
||||
'severity' => 'warning',
|
||||
'message' => 'Invalid request id should be dropped',
|
||||
'fingerprint' => 'v1_invalid_request_id',
|
||||
'meta' => ['page_request_id' => '------------------------------------'],
|
||||
]);
|
||||
|
||||
$this->assertSame(['accepted' => true, 'reason' => 'logged'], $result);
|
||||
}
|
||||
|
||||
public function testIngestUsesSingleSessionStateReadWriteForGuards(): void
|
||||
{
|
||||
$audit = $this->createMock(SystemAuditService::class);
|
||||
$audit->expects($this->once())
|
||||
->method('record')
|
||||
->willReturn(7);
|
||||
|
||||
$gateway = $this->createMock(SettingsFrontendTelemetryGateway::class);
|
||||
$gateway->method('isFrontendTelemetryEnabled')->willReturn(true);
|
||||
$gateway->method('getFrontendTelemetryAllowedEvents')->willReturn(['warn_once', 'ajax_error']);
|
||||
$gateway->method('getFrontendTelemetrySampleRate')->willReturn(1.0);
|
||||
|
||||
$rateLimiter = $this->createMock(RateLimiterService::class);
|
||||
$rateLimiter->expects($this->once())
|
||||
->method('hit')
|
||||
->willReturn(['allowed' => true, 'retry_after' => 0]);
|
||||
|
||||
$sessionStore = $this->createMock(SessionStoreInterface::class);
|
||||
$sessionStore->expects($this->once())->method('all')->willReturn(['user' => ['id' => 7]]);
|
||||
$sessionStore->expects($this->once())
|
||||
->method('get')
|
||||
->with('frontend_telemetry_state', [])
|
||||
->willReturn([]);
|
||||
$sessionStore->expects($this->once())
|
||||
->method('set')
|
||||
->with(
|
||||
'frontend_telemetry_state',
|
||||
$this->callback(static function (mixed $value): bool {
|
||||
if (!is_array($value)) {
|
||||
return false;
|
||||
}
|
||||
if (!is_array($value['dedupe'] ?? null)) {
|
||||
return false;
|
||||
}
|
||||
if (!is_array($value['burst'] ?? null)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
);
|
||||
|
||||
$service = new FrontendTelemetryIngestService($audit, $gateway, $rateLimiter, $sessionStore);
|
||||
$result = $service->ingest([
|
||||
'event_type' => 'frontend.warn_once',
|
||||
'severity' => 'warning',
|
||||
'message' => 'single state write',
|
||||
'fingerprint' => 'v1_single_state_write',
|
||||
'meta' => ['location' => '/admin/test'],
|
||||
]);
|
||||
|
||||
$this->assertSame(['accepted' => true, 'reason' => 'logged'], $result);
|
||||
}
|
||||
|
||||
public function testIngestDropsDuplicateFingerprint(): void
|
||||
{
|
||||
$audit = $this->createMock(SystemAuditService::class);
|
||||
$audit->expects($this->once())->method('record')->willReturn(9);
|
||||
|
||||
$gateway = $this->createMock(SettingsFrontendTelemetryGateway::class);
|
||||
$gateway->method('isFrontendTelemetryEnabled')->willReturn(true);
|
||||
$gateway->method('getFrontendTelemetryAllowedEvents')->willReturn(['warn_once', 'ajax_error']);
|
||||
$gateway->method('getFrontendTelemetrySampleRate')->willReturn(1.0);
|
||||
|
||||
$rateLimiter = $this->createMock(RateLimiterService::class);
|
||||
$rateLimiter->method('hit')->willReturn(['allowed' => true, 'retry_after' => 0]);
|
||||
|
||||
$sessionData = ['user' => ['id' => 7]];
|
||||
$sessionStore = $this->createMock(SessionStoreInterface::class);
|
||||
$sessionStore->method('all')->willReturnCallback(static fn (): array => $sessionData);
|
||||
$sessionStore->method('get')->willReturnCallback(static function (string $key, mixed $default = null) use (&$sessionData): mixed {
|
||||
return $sessionData[$key] ?? $default;
|
||||
});
|
||||
$sessionStore->method('set')->willReturnCallback(static function (string $key, mixed $value) use (&$sessionData): void {
|
||||
$sessionData[$key] = $value;
|
||||
});
|
||||
|
||||
$service = new FrontendTelemetryIngestService($audit, $gateway, $rateLimiter, $sessionStore);
|
||||
$first = $service->ingest([
|
||||
'event_type' => 'frontend.ajax_error',
|
||||
'message' => 'AJAX request failed',
|
||||
'fingerprint' => 'v1_ajax_error_1',
|
||||
]);
|
||||
$second = $service->ingest([
|
||||
'event_type' => 'frontend.ajax_error',
|
||||
'message' => 'AJAX request failed',
|
||||
'fingerprint' => 'v1_ajax_error_1',
|
||||
]);
|
||||
|
||||
$this->assertSame(['accepted' => true, 'reason' => 'logged'], $first);
|
||||
$this->assertSame(['accepted' => false, 'reason' => 'duplicate'], $second);
|
||||
}
|
||||
|
||||
public function testIngestAppliesSessionBurstFastDropBeforeGlobalRateLimit(): void
|
||||
{
|
||||
$recordCalls = 0;
|
||||
$rateLimiterCalls = 0;
|
||||
|
||||
$audit = $this->createMock(SystemAuditService::class);
|
||||
$audit->method('record')->willReturnCallback(static function () use (&$recordCalls): int {
|
||||
$recordCalls++;
|
||||
return $recordCalls;
|
||||
});
|
||||
|
||||
$gateway = $this->createMock(SettingsFrontendTelemetryGateway::class);
|
||||
$gateway->method('isFrontendTelemetryEnabled')->willReturn(true);
|
||||
$gateway->method('getFrontendTelemetryAllowedEvents')->willReturn(['warn_once', 'ajax_error']);
|
||||
$gateway->method('getFrontendTelemetrySampleRate')->willReturn(1.0);
|
||||
|
||||
$rateLimiter = $this->createMock(RateLimiterService::class);
|
||||
$rateLimiter->method('hit')->willReturnCallback(static function () use (&$rateLimiterCalls): array {
|
||||
$rateLimiterCalls++;
|
||||
return ['allowed' => true, 'retry_after' => 0];
|
||||
});
|
||||
|
||||
$sessionData = ['user' => ['id' => 7]];
|
||||
$sessionStore = $this->createMock(SessionStoreInterface::class);
|
||||
$sessionStore->method('all')->willReturnCallback(static fn (): array => $sessionData);
|
||||
$sessionStore->method('get')->willReturnCallback(static function (string $key, mixed $default = null) use (&$sessionData): mixed {
|
||||
return $sessionData[$key] ?? $default;
|
||||
});
|
||||
$sessionStore->method('set')->willReturnCallback(static function (string $key, mixed $value) use (&$sessionData): void {
|
||||
$sessionData[$key] = $value;
|
||||
});
|
||||
|
||||
$service = new FrontendTelemetryIngestService($audit, $gateway, $rateLimiter, $sessionStore);
|
||||
|
||||
$lastResult = ['accepted' => true, 'reason' => 'logged'];
|
||||
for ($index = 0; $index < 120; $index++) {
|
||||
$lastResult = $service->ingest([
|
||||
'event_type' => 'frontend.ajax_error',
|
||||
'message' => 'AJAX request failed',
|
||||
'fingerprint' => 'v1_ajax_error_' . $index,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->assertSame(['accepted' => false, 'reason' => 'burst_limited'], $lastResult);
|
||||
$this->assertGreaterThan(0, $recordCalls);
|
||||
$this->assertLessThan(120, $recordCalls);
|
||||
$this->assertSame($recordCalls, $rateLimiterCalls);
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Audit;
|
||||
|
||||
use MintyPHP\Repository\Audit\ImportAuditRunRepository;
|
||||
use MintyPHP\Service\Audit\ImportAuditService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ImportAuditServiceTest extends TestCase
|
||||
{
|
||||
public function testStartRunNormalizesFilenameAndMappedTargets(): void
|
||||
{
|
||||
$captured = null;
|
||||
$repository = $this->createMock(ImportAuditRunRepository::class);
|
||||
$repository->expects($this->once())
|
||||
->method('createRunning')
|
||||
->with($this->callback(function (array $payload) use (&$captured): bool {
|
||||
$captured = $payload;
|
||||
return true;
|
||||
}))
|
||||
->willReturn(77);
|
||||
|
||||
$service = new ImportAuditService($repository);
|
||||
$runId = $service->startRun('users', ['email', 'first_name', 'email'], '/tmp/../users.csv', 12, 5);
|
||||
|
||||
$this->assertSame(77, $runId);
|
||||
$this->assertSame('users.csv', (string) ($captured['source_filename'] ?? ''));
|
||||
$this->assertSame('email,first_name', (string) ($captured['mapped_targets_csv'] ?? ''));
|
||||
$this->assertSame(12, (int) ($captured['user_id'] ?? 0));
|
||||
$this->assertSame(5, (int) ($captured['current_tenant_id'] ?? 0));
|
||||
}
|
||||
|
||||
public function testFinishRunDerivesPartialStatusAndAggregatesErrors(): void
|
||||
{
|
||||
$captured = null;
|
||||
$repository = $this->createMock(ImportAuditRunRepository::class);
|
||||
$repository->expects($this->once())
|
||||
->method('finishById')
|
||||
->with(
|
||||
88,
|
||||
$this->callback(function (array $payload) use (&$captured): bool {
|
||||
$captured = $payload;
|
||||
return true;
|
||||
})
|
||||
)
|
||||
->willReturn(true);
|
||||
|
||||
$service = new ImportAuditService($repository);
|
||||
$service->finishRun(88, [
|
||||
'ok' => true,
|
||||
'processed' => 3,
|
||||
'created' => 1,
|
||||
'skipped' => 1,
|
||||
'failed' => 1,
|
||||
'errors' => [
|
||||
['code' => 'email_exists'],
|
||||
['code' => 'email_exists'],
|
||||
['code' => 'invalid_email'],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertSame('partial', (string) ($captured['status'] ?? ''));
|
||||
$this->assertSame(3, (int) ($captured['rows_total'] ?? 0));
|
||||
$this->assertSame(1, (int) ($captured['created_count'] ?? 0));
|
||||
$this->assertSame(1, (int) ($captured['skipped_count'] ?? 0));
|
||||
$this->assertSame(1, (int) ($captured['failed_count'] ?? 0));
|
||||
$errorJson = (string) ($captured['error_codes_json'] ?? '');
|
||||
$this->assertStringContainsString('email_exists', $errorJson);
|
||||
$this->assertStringContainsString('invalid_email', $errorJson);
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Audit;
|
||||
|
||||
use MintyPHP\Service\Audit\SystemAuditRedactionService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class SystemAuditRedactionServiceTest extends TestCase
|
||||
{
|
||||
public function testSensitiveKeysAreRedacted(): void
|
||||
{
|
||||
$service = new SystemAuditRedactionService();
|
||||
|
||||
$redacted = $service->redactMetadata([
|
||||
'email' => 'john@example.com',
|
||||
'token' => 'abc',
|
||||
'active' => 1,
|
||||
'nested' => ['password' => 'secret'],
|
||||
]);
|
||||
|
||||
$this->assertSame('[REDACTED]', $redacted['email']);
|
||||
$this->assertSame('[REDACTED]', $redacted['token']);
|
||||
$this->assertSame(1, $redacted['active']);
|
||||
$this->assertSame('[REDACTED]', $redacted['nested']['password']);
|
||||
}
|
||||
|
||||
public function testBuildChangeSetUsesAllowlistAndRedactsOtherValues(): void
|
||||
{
|
||||
$service = new SystemAuditRedactionService();
|
||||
|
||||
$changeSet = $service->buildChangeSet(
|
||||
['active' => 0, 'description' => 'old'],
|
||||
['active' => 1, 'description' => 'new']
|
||||
);
|
||||
|
||||
$this->assertSame(['active', 'description'], $changeSet['changed_fields']);
|
||||
$this->assertSame(0, $changeSet['changes']['active']['before']);
|
||||
$this->assertSame(1, $changeSet['changes']['active']['after']);
|
||||
$this->assertSame('[REDACTED]', $changeSet['changes']['description']['before']);
|
||||
$this->assertSame('[REDACTED]', $changeSet['changes']['description']['after']);
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Audit;
|
||||
|
||||
use MintyPHP\Http\RequestContext;
|
||||
use MintyPHP\Http\SessionStore;
|
||||
use MintyPHP\Repository\Audit\SystemAuditLogRepository;
|
||||
use MintyPHP\Service\Audit\SystemAuditRedactionService;
|
||||
use MintyPHP\Service\Audit\SystemAuditService;
|
||||
use MintyPHP\Service\Settings\SettingsSystemAuditGateway;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class SystemAuditServiceTest extends TestCase
|
||||
{
|
||||
protected function tearDown(): void
|
||||
{
|
||||
RequestContext::resetForTests();
|
||||
$_SESSION = [];
|
||||
}
|
||||
|
||||
public function testRecordReturnsNullWhenDisabled(): void
|
||||
{
|
||||
$repo = $this->createMock(SystemAuditLogRepository::class);
|
||||
$repo->expects($this->never())->method('create');
|
||||
|
||||
$settings = $this->createMock(SettingsSystemAuditGateway::class);
|
||||
$settings->expects($this->once())->method('isSystemAuditEnabled')->willReturn(false);
|
||||
|
||||
$service = new SystemAuditService($repo, new SystemAuditRedactionService(), $settings, new SessionStore());
|
||||
|
||||
$this->assertNull($service->record('admin.users.update', 'success'));
|
||||
}
|
||||
|
||||
public function testRecordWritesWhenEnabled(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'POST';
|
||||
$_SERVER['REQUEST_URI'] = '/admin/users/edit/abc';
|
||||
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
|
||||
$_SERVER['HTTP_USER_AGENT'] = 'phpunit';
|
||||
|
||||
$repo = $this->createMock(SystemAuditLogRepository::class);
|
||||
$repo->expects($this->once())->method('create')->willReturn(5);
|
||||
|
||||
$settings = $this->createMock(SettingsSystemAuditGateway::class);
|
||||
$settings->method('isSystemAuditEnabled')->willReturn(true);
|
||||
|
||||
$service = new SystemAuditService($repo, new SystemAuditRedactionService(), $settings, new SessionStore());
|
||||
|
||||
$id = $service->record('admin.users.update', 'success', [
|
||||
'target_type' => 'user',
|
||||
'target_id' => 10,
|
||||
'before' => ['active' => 0],
|
||||
'after' => ['active' => 1],
|
||||
]);
|
||||
|
||||
$this->assertSame(5, $id);
|
||||
}
|
||||
|
||||
public function testRecordIsFailOpenOnRepositoryError(): void
|
||||
{
|
||||
$repo = $this->createMock(SystemAuditLogRepository::class);
|
||||
$repo->method('create')->willThrowException(new \RuntimeException('db down'));
|
||||
|
||||
$settings = $this->createMock(SettingsSystemAuditGateway::class);
|
||||
$settings->method('isSystemAuditEnabled')->willReturn(true);
|
||||
|
||||
$service = new SystemAuditService($repo, new SystemAuditRedactionService(), $settings, new SessionStore());
|
||||
|
||||
$this->assertNull($service->record('admin.users.update', 'success'));
|
||||
}
|
||||
|
||||
public function testPurgeUsesConfiguredRetention(): void
|
||||
{
|
||||
$repo = $this->createMock(SystemAuditLogRepository::class);
|
||||
$repo->expects($this->once())->method('purgeOlderThanDays')->with(120)->willReturn(3);
|
||||
|
||||
$settings = $this->createMock(SettingsSystemAuditGateway::class);
|
||||
$settings->method('getSystemAuditRetentionDays')->willReturn(120);
|
||||
|
||||
$service = new SystemAuditService($repo, new SystemAuditRedactionService(), $settings, new SessionStore());
|
||||
|
||||
$this->assertSame(3, $service->purgeExpired());
|
||||
}
|
||||
}
|
||||
@@ -1,365 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Audit;
|
||||
|
||||
use MintyPHP\Repository\Audit\UserLifecycleAuditRepositoryInterface;
|
||||
use MintyPHP\Service\Audit\UserLifecycleAuditService;
|
||||
use MintyPHP\Support\Crypto;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class UserLifecycleAuditServiceTest extends TestCase
|
||||
{
|
||||
private const SNAPSHOT_FIELDS = [
|
||||
'uuid',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'display_name',
|
||||
'email',
|
||||
'profile_description',
|
||||
'job_title',
|
||||
'phone',
|
||||
'mobile',
|
||||
'short_dial',
|
||||
'address',
|
||||
'postal_code',
|
||||
'city',
|
||||
'region',
|
||||
'country',
|
||||
'hire_date',
|
||||
'locale',
|
||||
'theme',
|
||||
'email_verified_at',
|
||||
'last_login_at',
|
||||
'last_login_provider',
|
||||
'primary_tenant_id',
|
||||
'current_tenant_id',
|
||||
'created',
|
||||
'modified',
|
||||
'active_changed_at',
|
||||
];
|
||||
|
||||
private static bool $cryptoAvailable = false;
|
||||
|
||||
public static function setUpBeforeClass(): void
|
||||
{
|
||||
if (!defined('APP_CRYPTO_KEY')) {
|
||||
define('APP_CRYPTO_KEY', str_repeat('ab', 32));
|
||||
}
|
||||
self::$cryptoAvailable = Crypto::isConfigured();
|
||||
}
|
||||
|
||||
// ── logDeactivate ───────────────────────────────────────────────────
|
||||
|
||||
public function testLogDeactivateCallsRepoCreateWithCorrectRow(): void
|
||||
{
|
||||
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
||||
$repo->expects($this->once())
|
||||
->method('create')
|
||||
->with($this->callback(static function (array $row): bool {
|
||||
return $row['action'] === 'deactivate'
|
||||
&& $row['trigger_type'] === 'manual'
|
||||
&& $row['status'] === 'success'
|
||||
&& $row['run_uuid'] === 'run-1'
|
||||
&& $row['target_user_id'] === 5
|
||||
&& $row['target_user_uuid'] === 'user-uuid-5'
|
||||
&& $row['target_user_email'] === 'test@example.com'
|
||||
&& $row['actor_user_id'] === 99
|
||||
&& $row['policy_deactivate_days'] === 30
|
||||
&& $row['policy_delete_days'] === 60
|
||||
&& $row['snapshot_enc'] === null
|
||||
&& $row['snapshot_version'] === 1;
|
||||
}))
|
||||
->willReturn(1);
|
||||
|
||||
$service = new UserLifecycleAuditService($repo);
|
||||
$result = $service->logDeactivate(
|
||||
'run-1',
|
||||
'manual',
|
||||
['deactivate_days' => 30, 'delete_days' => 60],
|
||||
99,
|
||||
['id' => 5, 'uuid' => 'user-uuid-5', 'email' => 'test@example.com'],
|
||||
'success',
|
||||
null
|
||||
);
|
||||
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
public function testLogDeactivateReturnsFalseOnRepoException(): void
|
||||
{
|
||||
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
||||
$repo->method('create')->willThrowException(new \RuntimeException('db error'));
|
||||
|
||||
$service = new UserLifecycleAuditService($repo);
|
||||
$result = $service->logDeactivate('run-1', 'manual', [], null, []);
|
||||
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
public function testLogDeactivateReturnsFalseWhenRepoReturnsFalse(): void
|
||||
{
|
||||
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
||||
$repo->method('create')->willReturn(false);
|
||||
|
||||
$service = new UserLifecycleAuditService($repo);
|
||||
$result = $service->logDeactivate('run-1', 'manual', [], null, []);
|
||||
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
// ── logDeleteWithSnapshot ───────────────────────────────────────────
|
||||
|
||||
public function testLogDeleteWithSnapshotCreatesEncryptedSnapshot(): void
|
||||
{
|
||||
if (!self::$cryptoAvailable) {
|
||||
$this->markTestSkipped('Crypto not configured');
|
||||
}
|
||||
|
||||
$targetUser = [];
|
||||
foreach (self::SNAPSHOT_FIELDS as $field) {
|
||||
$targetUser[$field] = 'val_' . $field;
|
||||
}
|
||||
$targetUser['id'] = 5;
|
||||
$targetUser['extra_field'] = 'should_be_excluded';
|
||||
|
||||
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
||||
$repo->expects($this->once())
|
||||
->method('create')
|
||||
->with($this->callback(static function (array $row): bool {
|
||||
return $row['action'] === 'delete'
|
||||
&& $row['status'] === 'success'
|
||||
&& $row['snapshot_version'] === 1
|
||||
&& is_string($row['snapshot_enc'])
|
||||
&& $row['snapshot_enc'] !== '';
|
||||
}))
|
||||
->willReturn(7);
|
||||
|
||||
$service = new UserLifecycleAuditService($repo);
|
||||
$result = $service->logDeleteWithSnapshot('run-2', 'cron', ['deactivate_days' => 30, 'delete_days' => 90], null, $targetUser);
|
||||
|
||||
$this->assertSame(7, $result);
|
||||
}
|
||||
|
||||
public function testLogDeleteWithSnapshotContainsOnlySnapshotFields(): void
|
||||
{
|
||||
if (!self::$cryptoAvailable) {
|
||||
$this->markTestSkipped('Crypto not configured');
|
||||
}
|
||||
|
||||
$targetUser = ['id' => 5, 'uuid' => 'u5', 'email' => 'e@example.com', 'extra' => 'no'];
|
||||
|
||||
$capturedRow = null;
|
||||
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
||||
$repo->expects($this->once())
|
||||
->method('create')
|
||||
->with($this->callback(static function (array $row) use (&$capturedRow): bool {
|
||||
$capturedRow = $row;
|
||||
return true;
|
||||
}))
|
||||
->willReturn(1);
|
||||
|
||||
$service = new UserLifecycleAuditService($repo);
|
||||
$service->logDeleteWithSnapshot('run-3', 'manual', [], null, $targetUser);
|
||||
|
||||
$this->assertNotNull($capturedRow);
|
||||
$decryptedJson = Crypto::decryptString($capturedRow['snapshot_enc']);
|
||||
$snapshot = json_decode($decryptedJson, true);
|
||||
$this->assertIsArray($snapshot);
|
||||
$this->assertSame(self::SNAPSHOT_FIELDS, array_keys($snapshot));
|
||||
$this->assertArrayNotHasKey('extra', $snapshot);
|
||||
$this->assertArrayNotHasKey('id', $snapshot);
|
||||
}
|
||||
|
||||
public function testLogDeleteWithSnapshotReturnsFalseOnCryptoFailure(): void
|
||||
{
|
||||
// Simulate a scenario where Crypto throws by passing a target user that will
|
||||
// cause the try/catch to fire. We cannot easily override static Crypto, but we
|
||||
// know that if APP_CRYPTO_KEY were missing, encryptString would throw.
|
||||
// Since we defined a valid key, let's test the repo exception path instead.
|
||||
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
||||
$repo->method('create')->willThrowException(new \RuntimeException('db error'));
|
||||
|
||||
$service = new UserLifecycleAuditService($repo);
|
||||
$result = $service->logDeleteWithSnapshot('run-4', 'manual', [], null, ['id' => 1, 'uuid' => 'u']);
|
||||
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
// ── logRestore ──────────────────────────────────────────────────────
|
||||
|
||||
public function testLogRestoreCallsRepoWithRestoreAction(): void
|
||||
{
|
||||
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
||||
$repo->expects($this->once())
|
||||
->method('create')
|
||||
->with($this->callback(static function (array $row): bool {
|
||||
return $row['action'] === 'restore'
|
||||
&& $row['trigger_type'] === 'manual'
|
||||
&& $row['status'] === 'success';
|
||||
}))
|
||||
->willReturn(10);
|
||||
|
||||
$service = new UserLifecycleAuditService($repo);
|
||||
$result = $service->logRestore('run-5', 'manual', [], 99, ['id' => 5, 'uuid' => 'u5', 'email' => 'e@x.com']);
|
||||
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
public function testLogRestoreReturnsFalseOnException(): void
|
||||
{
|
||||
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
||||
$repo->method('create')->willThrowException(new \RuntimeException('fail'));
|
||||
|
||||
$service = new UserLifecycleAuditService($repo);
|
||||
$result = $service->logRestore('run-5', 'manual', [], null, []);
|
||||
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
// ── decryptSnapshot ─────────────────────────────────────────────────
|
||||
|
||||
public function testDecryptSnapshotWithValidPayload(): void
|
||||
{
|
||||
if (!self::$cryptoAvailable) {
|
||||
$this->markTestSkipped('Crypto not configured');
|
||||
}
|
||||
|
||||
$original = ['uuid' => 'test-uuid', 'email' => 'test@example.com'];
|
||||
$encrypted = Crypto::encryptString(json_encode($original));
|
||||
|
||||
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
||||
$service = new UserLifecycleAuditService($repo);
|
||||
|
||||
$result = $service->decryptSnapshot(['snapshot_enc' => $encrypted]);
|
||||
$this->assertSame($original, $result);
|
||||
}
|
||||
|
||||
public function testDecryptSnapshotReturnsNullForEmptyPayload(): void
|
||||
{
|
||||
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
||||
$service = new UserLifecycleAuditService($repo);
|
||||
|
||||
$this->assertNull($service->decryptSnapshot([]));
|
||||
$this->assertNull($service->decryptSnapshot(['snapshot_enc' => '']));
|
||||
$this->assertNull($service->decryptSnapshot(['snapshot_enc' => ' ']));
|
||||
}
|
||||
|
||||
public function testDecryptSnapshotReturnsNullForInvalidPayload(): void
|
||||
{
|
||||
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
||||
$service = new UserLifecycleAuditService($repo);
|
||||
|
||||
$result = $service->decryptSnapshot(['snapshot_enc' => 'not-valid-encrypted-data']);
|
||||
$this->assertNull($result);
|
||||
}
|
||||
|
||||
// ── markDeleteEventRestored ─────────────────────────────────────────
|
||||
|
||||
public function testMarkDeleteEventRestoredDelegatesToRepo(): void
|
||||
{
|
||||
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
||||
$repo->expects($this->once())
|
||||
->method('markRestored')
|
||||
->with(1, 99, 200)
|
||||
->willReturn(true);
|
||||
|
||||
$service = new UserLifecycleAuditService($repo);
|
||||
$this->assertTrue($service->markDeleteEventRestored(1, 99, 200));
|
||||
}
|
||||
|
||||
public function testMarkDeleteEventRestoredReturnsFalseOnException(): void
|
||||
{
|
||||
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
||||
$repo->method('markRestored')->willThrowException(new \RuntimeException('fail'));
|
||||
|
||||
$service = new UserLifecycleAuditService($repo);
|
||||
$this->assertFalse($service->markDeleteEventRestored(1, 99, 200));
|
||||
}
|
||||
|
||||
// ── purgeExpired ────────────────────────────────────────────────────
|
||||
|
||||
public function testPurgeExpiredDelegatesToRepoWith365Days(): void
|
||||
{
|
||||
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
||||
$repo->expects($this->once())
|
||||
->method('purgeOlderThanDays')
|
||||
->with(365)
|
||||
->willReturn(5);
|
||||
|
||||
$service = new UserLifecycleAuditService($repo);
|
||||
$this->assertSame(5, $service->purgeExpired());
|
||||
}
|
||||
|
||||
// ── buildBaseRow enum normalization ──────────────────────────────────
|
||||
|
||||
public function testBuildBaseRowNormalizesInvalidTriggerTypeToDefault(): void
|
||||
{
|
||||
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
||||
$repo->expects($this->once())
|
||||
->method('create')
|
||||
->with($this->callback(static function (array $row): bool {
|
||||
return $row['trigger_type'] === 'system'; // fallback for invalid trigger
|
||||
}))
|
||||
->willReturn(1);
|
||||
|
||||
$service = new UserLifecycleAuditService($repo);
|
||||
$service->logDeactivate('run-x', 'INVALID_TRIGGER', [], null, []);
|
||||
}
|
||||
|
||||
public function testBuildBaseRowNormalizesInvalidStatusToFailed(): void
|
||||
{
|
||||
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
||||
$repo->expects($this->once())
|
||||
->method('create')
|
||||
->with($this->callback(static function (array $row): bool {
|
||||
return $row['status'] === 'failed'; // fallback for invalid status
|
||||
}))
|
||||
->willReturn(1);
|
||||
|
||||
$service = new UserLifecycleAuditService($repo);
|
||||
$service->logDeactivate('run-x', 'manual', [], null, [], 'INVALID_STATUS');
|
||||
}
|
||||
|
||||
public function testBuildBaseRowNormalizesNullActorToNull(): void
|
||||
{
|
||||
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
||||
$repo->expects($this->once())
|
||||
->method('create')
|
||||
->with($this->callback(static function (array $row): bool {
|
||||
return $row['actor_user_id'] === null;
|
||||
}))
|
||||
->willReturn(1);
|
||||
|
||||
$service = new UserLifecycleAuditService($repo);
|
||||
$service->logDeactivate('run-x', 'manual', [], null, []);
|
||||
}
|
||||
|
||||
public function testBuildBaseRowNormalizesZeroActorToNull(): void
|
||||
{
|
||||
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
||||
$repo->expects($this->once())
|
||||
->method('create')
|
||||
->with($this->callback(static function (array $row): bool {
|
||||
return $row['actor_user_id'] === null;
|
||||
}))
|
||||
->willReturn(1);
|
||||
|
||||
$service = new UserLifecycleAuditService($repo);
|
||||
$service->logDeactivate('run-x', 'manual', [], 0, []);
|
||||
}
|
||||
|
||||
public function testBuildBaseRowSetsNegativePolicyDaysToZero(): void
|
||||
{
|
||||
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
||||
$repo->expects($this->once())
|
||||
->method('create')
|
||||
->with($this->callback(static function (array $row): bool {
|
||||
return $row['policy_deactivate_days'] === 0
|
||||
&& $row['policy_delete_days'] === 0;
|
||||
}))
|
||||
->willReturn(1);
|
||||
|
||||
$service = new UserLifecycleAuditService($repo);
|
||||
$service->logDeactivate('run-x', 'manual', ['deactivate_days' => -10, 'delete_days' => -5], null, []);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use MintyPHP\Http\SessionStoreInterface;
|
||||
use MintyPHP\Repository\User\UserReadRepositoryInterface;
|
||||
use MintyPHP\Repository\User\UserWriteRepositoryInterface;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Audit\SystemAuditService;
|
||||
use MintyPHP\Service\Audit\AuditRecorderInterface;
|
||||
use MintyPHP\Service\Auth\AuthService;
|
||||
use MintyPHP\Service\Auth\AuthSessionTenantContextService;
|
||||
use MintyPHP\Service\Auth\EmailVerificationService;
|
||||
@@ -265,7 +265,7 @@ class AuthServiceTest extends TestCase
|
||||
$tenantCtxService = $this->createMock(AuthSessionTenantContextService::class);
|
||||
$tenantCtxService->expects($this->once())->method('hydrateForUser')->with(5);
|
||||
|
||||
$audit = $this->createMock(SystemAuditService::class);
|
||||
$audit = $this->createMock(AuditRecorderInterface::class);
|
||||
$audit->expects($this->once())->method('record')
|
||||
->with('auth.login.success', 'success', $this->anything());
|
||||
|
||||
@@ -301,7 +301,7 @@ class AuthServiceTest extends TestCase
|
||||
return $default;
|
||||
});
|
||||
|
||||
$audit = $this->createMock(SystemAuditService::class);
|
||||
$audit = $this->createMock(AuditRecorderInterface::class);
|
||||
$audit->expects($this->once())->method('record')
|
||||
->with('auth.login.failed', 'failed', $this->anything());
|
||||
|
||||
@@ -360,7 +360,7 @@ class AuthServiceTest extends TestCase
|
||||
'email_verified_at' => null,
|
||||
]);
|
||||
|
||||
$audit = $this->createMock(SystemAuditService::class);
|
||||
$audit = $this->createMock(AuditRecorderInterface::class);
|
||||
$audit->expects($this->once())->method('record')
|
||||
->with('auth.login.failed', 'failed', $this->callback(function (array $ctx): bool {
|
||||
return ($ctx['error_code'] ?? '') === 'email_not_verified';
|
||||
@@ -398,7 +398,7 @@ class AuthServiceTest extends TestCase
|
||||
$rememberMeService = $this->createMock(RememberMeService::class);
|
||||
$rememberMeService->expects($this->never())->method('forgetCurrentUser');
|
||||
|
||||
$audit = $this->createMock(SystemAuditService::class);
|
||||
$audit = $this->createMock(AuditRecorderInterface::class);
|
||||
$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
|
||||
@@ -432,7 +432,7 @@ class AuthServiceTest extends TestCase
|
||||
['current_tenant', [], ['id' => 11]],
|
||||
]);
|
||||
|
||||
$audit = $this->createMock(SystemAuditService::class);
|
||||
$audit = $this->createMock(AuditRecorderInterface::class);
|
||||
$audit->expects($this->once())->method('record')
|
||||
->with('auth.logout', 'success', $this->callback(function (array $context): bool {
|
||||
return (int) ($context['actor_user_id'] ?? 0) === 5
|
||||
@@ -462,7 +462,7 @@ class AuthServiceTest extends TestCase
|
||||
?PermissionService $permissionService = null,
|
||||
?TenantSsoService $tenantSsoService = null,
|
||||
?AuthSessionTenantContextService $authSessionTenantContextService = null,
|
||||
?SystemAuditService $systemAuditService = null,
|
||||
?AuditRecorderInterface $systemAuditService = null,
|
||||
?SessionStoreInterface $sessionStore = null
|
||||
): AuthService {
|
||||
return new AuthService(
|
||||
@@ -475,7 +475,7 @@ class AuthServiceTest extends TestCase
|
||||
$permissionService ?? $this->createMock(PermissionService::class),
|
||||
$tenantSsoService ?? $this->createMock(TenantSsoService::class),
|
||||
$authSessionTenantContextService ?? $this->createMock(AuthSessionTenantContextService::class),
|
||||
$systemAuditService ?? $this->createMock(SystemAuditService::class),
|
||||
$systemAuditService ?? $this->createMock(AuditRecorderInterface::class),
|
||||
$sessionStore ?? $this->createMock(SessionStoreInterface::class)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace MintyPHP\Tests\Service\Import;
|
||||
|
||||
use MintyPHP\Http\SessionStoreInterface;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Audit\ImportAuditService;
|
||||
use MintyPHP\Service\Audit\ImportAuditInterface;
|
||||
use MintyPHP\Service\Import\CsvReaderService;
|
||||
use MintyPHP\Service\Import\ImportService;
|
||||
use MintyPHP\Service\Import\ImportStateStoreService;
|
||||
@@ -117,7 +117,7 @@ class ImportServiceTest extends TestCase
|
||||
): ImportService {
|
||||
$csvReader = $csvReader ?? $this->createMock(CsvReaderService::class);
|
||||
$tempFileService = $tempFileService ?? $this->createMock(ImportTempFileService::class);
|
||||
$auditService = $this->createMock(ImportAuditService::class);
|
||||
$auditService = $this->createMock(ImportAuditInterface::class);
|
||||
$stateStore = $stateStore ?? $this->createMock(ImportStateStoreService::class);
|
||||
$permissionService = $this->createMock(PermissionService::class);
|
||||
$permissionService->method('userHas')->willReturn(false);
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace MintyPHP\Tests\Service\Org;
|
||||
|
||||
use MintyPHP\Repository\Org\DepartmentRepositoryInterface;
|
||||
use MintyPHP\Repository\Org\UserDepartmentRepositoryInterface;
|
||||
use MintyPHP\Service\Audit\SystemAuditService;
|
||||
use MintyPHP\Service\Audit\AuditRecorderInterface;
|
||||
use MintyPHP\Service\Directory\DirectorySettingsGateway;
|
||||
use MintyPHP\Service\Org\DepartmentService;
|
||||
use MintyPHP\Service\Tenant\TenantScopeService;
|
||||
@@ -19,7 +19,7 @@ class DepartmentServiceTest extends TestCase
|
||||
private DepartmentRepositoryInterface&MockObject $departmentRepository;
|
||||
private DirectorySettingsGateway&MockObject $settingsGateway;
|
||||
private TenantScopeService&MockObject $scopeGateway;
|
||||
private SystemAuditService&MockObject $systemAuditService;
|
||||
private AuditRecorderInterface&MockObject $systemAuditService;
|
||||
private DepartmentService $service;
|
||||
|
||||
protected function setUp(): void
|
||||
@@ -28,7 +28,7 @@ class DepartmentServiceTest extends TestCase
|
||||
$this->departmentRepository = $this->createMock(DepartmentRepositoryInterface::class);
|
||||
$this->settingsGateway = $this->createMock(DirectorySettingsGateway::class);
|
||||
$this->scopeGateway = $this->createMock(TenantScopeService::class);
|
||||
$this->systemAuditService = $this->createMock(SystemAuditService::class);
|
||||
$this->systemAuditService = $this->createMock(AuditRecorderInterface::class);
|
||||
|
||||
$this->service = new DepartmentService(
|
||||
$this->userServicesFactory,
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace MintyPHP\Tests\Service\Scheduler;
|
||||
|
||||
use MintyPHP\App\AppContainer;
|
||||
use MintyPHP\App\Module\ModuleRegistry;
|
||||
use MintyPHP\Service\Audit\SystemAuditService;
|
||||
use MintyPHP\Service\Scheduler\Handler\ScheduledJobHandlerInterface;
|
||||
use MintyPHP\Service\Scheduler\ScheduledJobRegistry;
|
||||
use MintyPHP\Service\User\UserLifecycleService;
|
||||
@@ -37,7 +36,6 @@ final class ScheduledJobRegistryTest extends TestCase
|
||||
|
||||
$registry = new ScheduledJobRegistry(
|
||||
$this->createMock(UserLifecycleService::class),
|
||||
$this->createMock(SystemAuditService::class),
|
||||
$container
|
||||
);
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use MintyPHP\Repository\Scheduler\ScheduledJobRepository;
|
||||
use MintyPHP\Repository\Scheduler\ScheduledJobRunRepository;
|
||||
use MintyPHP\Repository\Scheduler\SchedulerRuntimeRepository;
|
||||
use MintyPHP\Repository\Support\DatabaseSessionRepository;
|
||||
use MintyPHP\Service\Audit\SystemAuditService;
|
||||
use MintyPHP\Service\Audit\AuditRecorderInterface;
|
||||
use MintyPHP\Service\Scheduler\ScheduleCalculator;
|
||||
use MintyPHP\Service\Scheduler\ScheduledJobRegistry;
|
||||
use MintyPHP\Service\Scheduler\ScheduledJobService;
|
||||
@@ -19,7 +19,7 @@ class SchedulerRunServiceTest extends TestCase
|
||||
{
|
||||
$scheduledJobService = $this->createMock(ScheduledJobService::class);
|
||||
$scheduledJobService->expects($this->once())->method('ensureSystemJobs');
|
||||
$systemAuditService = $this->createMock(SystemAuditService::class);
|
||||
$systemAuditService = $this->createMock(AuditRecorderInterface::class);
|
||||
$systemAuditService->expects($this->once())
|
||||
->method('record')
|
||||
->with(
|
||||
@@ -86,7 +86,7 @@ class SchedulerRunServiceTest extends TestCase
|
||||
->with('scheduled_jobs_runner');
|
||||
|
||||
$records = [];
|
||||
$systemAuditService = $this->createMock(SystemAuditService::class);
|
||||
$systemAuditService = $this->createMock(AuditRecorderInterface::class);
|
||||
$systemAuditService->expects($this->exactly(2))
|
||||
->method('record')
|
||||
->willReturnCallback(static function (string $eventType, string $outcome, array $context) use (&$records): int {
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace MintyPHP\Tests\Service\Settings;
|
||||
use MintyPHP\Repository\Auth\ApiTokenRepository;
|
||||
use MintyPHP\Repository\Auth\RememberTokenRepository;
|
||||
use MintyPHP\Service\Access\RoleService;
|
||||
use MintyPHP\Service\Audit\SystemAuditService;
|
||||
use MintyPHP\Service\Audit\AuditRecorderInterface;
|
||||
use MintyPHP\Service\Org\DepartmentService;
|
||||
use MintyPHP\Service\Settings\AdminSettingsService;
|
||||
use MintyPHP\Service\Settings\SettingCacheService;
|
||||
@@ -461,7 +461,7 @@ class AdminSettingsServiceApiLifecycleTest extends TestCase
|
||||
$apiTokenRepository = $this->createConfiguredMock(ApiTokenRepository::class, [
|
||||
'countActive' => 0,
|
||||
]);
|
||||
$systemAuditService = $this->createConfiguredMock(SystemAuditService::class, [
|
||||
$systemAuditService = $this->createConfiguredMock(AuditRecorderInterface::class, [
|
||||
'record' => null,
|
||||
]);
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace MintyPHP\Tests\Service\Settings;
|
||||
use MintyPHP\Repository\Auth\ApiTokenRepository;
|
||||
use MintyPHP\Repository\Auth\RememberTokenRepository;
|
||||
use MintyPHP\Service\Access\RoleService;
|
||||
use MintyPHP\Service\Audit\SystemAuditService;
|
||||
use MintyPHP\Service\Audit\AuditRecorderInterface;
|
||||
use MintyPHP\Service\Org\DepartmentService;
|
||||
use MintyPHP\Service\Settings\AdminSettingsService;
|
||||
use MintyPHP\Service\Settings\SettingCacheService;
|
||||
@@ -426,7 +426,7 @@ class AdminSettingsServiceAppTest extends TestCase
|
||||
$apiTokenRepository = $apiTokenRepository ?? $this->createConfiguredMock(ApiTokenRepository::class, [
|
||||
'countActive' => 0,
|
||||
]);
|
||||
$systemAuditService = $this->createConfiguredMock(SystemAuditService::class, [
|
||||
$systemAuditService = $this->createConfiguredMock(AuditRecorderInterface::class, [
|
||||
'record' => null,
|
||||
]);
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace MintyPHP\Tests\Service\Settings;
|
||||
use MintyPHP\Repository\Auth\ApiTokenRepository;
|
||||
use MintyPHP\Repository\Auth\RememberTokenRepository;
|
||||
use MintyPHP\Service\Access\RoleService;
|
||||
use MintyPHP\Service\Audit\SystemAuditService;
|
||||
use MintyPHP\Service\Audit\AuditRecorderInterface;
|
||||
use MintyPHP\Service\Org\DepartmentService;
|
||||
use MintyPHP\Service\Settings\AdminSettingsService;
|
||||
use MintyPHP\Service\Settings\SettingCacheService;
|
||||
@@ -545,7 +545,7 @@ class AdminSettingsServiceSecurityTest extends TestCase
|
||||
$apiTokenRepository = $this->createConfiguredMock(ApiTokenRepository::class, [
|
||||
'countActive' => 0,
|
||||
]);
|
||||
$systemAuditService = $this->createConfiguredMock(SystemAuditService::class, [
|
||||
$systemAuditService = $this->createConfiguredMock(AuditRecorderInterface::class, [
|
||||
'record' => null,
|
||||
]);
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace MintyPHP\Tests\Service\Settings;
|
||||
use MintyPHP\Repository\Auth\ApiTokenRepository;
|
||||
use MintyPHP\Repository\Auth\RememberTokenRepository;
|
||||
use MintyPHP\Service\Access\RoleService;
|
||||
use MintyPHP\Service\Audit\SystemAuditService;
|
||||
use MintyPHP\Service\Audit\AuditRecorderInterface;
|
||||
use MintyPHP\Service\Org\DepartmentService;
|
||||
use MintyPHP\Service\Settings\AdminSettingsService;
|
||||
use MintyPHP\Service\Settings\SettingCacheService;
|
||||
@@ -344,7 +344,7 @@ class AdminSettingsServiceSessionPolicyTest extends TestCase
|
||||
$apiTokenRepository = $this->createConfiguredMock(ApiTokenRepository::class, [
|
||||
'countActive' => 0,
|
||||
]);
|
||||
$systemAuditService = $this->createConfiguredMock(SystemAuditService::class, [
|
||||
$systemAuditService = $this->createConfiguredMock(AuditRecorderInterface::class, [
|
||||
'record' => null,
|
||||
]);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace MintyPHP\Tests\Service\Tenant;
|
||||
|
||||
use MintyPHP\Repository\Org\DepartmentRepositoryInterface;
|
||||
use MintyPHP\Repository\Tenant\TenantRepositoryInterface;
|
||||
use MintyPHP\Service\Audit\SystemAuditService;
|
||||
use MintyPHP\Service\Audit\AuditRecorderInterface;
|
||||
use MintyPHP\Service\Directory\DirectorySettingsGateway;
|
||||
use MintyPHP\Service\Tenant\TenantService;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
@@ -15,7 +15,7 @@ class TenantServiceTest extends TestCase
|
||||
private TenantRepositoryInterface&MockObject $tenantRepository;
|
||||
private DepartmentRepositoryInterface&MockObject $departmentRepository;
|
||||
private DirectorySettingsGateway&MockObject $settingsGateway;
|
||||
private SystemAuditService&MockObject $systemAuditService;
|
||||
private AuditRecorderInterface&MockObject $systemAuditService;
|
||||
private TenantService $service;
|
||||
|
||||
protected function setUp(): void
|
||||
@@ -23,7 +23,7 @@ class TenantServiceTest extends TestCase
|
||||
$this->tenantRepository = $this->createMock(TenantRepositoryInterface::class);
|
||||
$this->departmentRepository = $this->createMock(DepartmentRepositoryInterface::class);
|
||||
$this->settingsGateway = $this->createMock(DirectorySettingsGateway::class);
|
||||
$this->systemAuditService = $this->createMock(SystemAuditService::class);
|
||||
$this->systemAuditService = $this->createMock(AuditRecorderInterface::class);
|
||||
|
||||
$this->service = new TenantService(
|
||||
$this->tenantRepository,
|
||||
|
||||
@@ -6,7 +6,7 @@ use MintyPHP\Repository\Support\DatabaseSessionRepository;
|
||||
use MintyPHP\Repository\User\UserListQueryRepositoryInterface;
|
||||
use MintyPHP\Repository\User\UserReadRepositoryInterface;
|
||||
use MintyPHP\Repository\User\UserWriteRepositoryInterface;
|
||||
use MintyPHP\Service\Audit\SystemAuditService;
|
||||
use MintyPHP\Service\Audit\AuditRecorderInterface;
|
||||
use MintyPHP\Service\Tenant\TenantScopeService;
|
||||
use MintyPHP\Service\User\UserAccountService;
|
||||
use MintyPHP\Service\User\UserAssignmentService;
|
||||
@@ -96,7 +96,7 @@ class UserAccountServiceTest extends TestCase
|
||||
->with(11)
|
||||
->willReturn(true);
|
||||
|
||||
$auditService = $this->createMock(SystemAuditService::class);
|
||||
$auditService = $this->createMock(AuditRecorderInterface::class);
|
||||
$auditService->expects($this->once())
|
||||
->method('record')
|
||||
->with(
|
||||
@@ -130,7 +130,7 @@ class UserAccountServiceTest extends TestCase
|
||||
->with(11)
|
||||
->willReturn(false);
|
||||
|
||||
$auditService = $this->createMock(SystemAuditService::class);
|
||||
$auditService = $this->createMock(AuditRecorderInterface::class);
|
||||
$auditService->expects($this->never())->method('record');
|
||||
|
||||
$service = $this->newService($readRepository, $writeRepository, null, null, null, null, null, null, $auditService);
|
||||
@@ -230,7 +230,7 @@ class UserAccountServiceTest extends TestCase
|
||||
$assignmentService = $this->createMock(UserAssignmentService::class);
|
||||
$assignmentService->expects($this->never())->method('bumpAuthzVersion');
|
||||
|
||||
$auditService = $this->createMock(SystemAuditService::class);
|
||||
$auditService = $this->createMock(AuditRecorderInterface::class);
|
||||
$auditService->expects($this->never())->method('record');
|
||||
|
||||
$service = $this->newService($readRepository, $writeRepository, null, $assignmentService, null, null, null, null, $auditService);
|
||||
@@ -257,7 +257,7 @@ class UserAccountServiceTest extends TestCase
|
||||
->method('bumpAuthzVersion')
|
||||
->with(33);
|
||||
|
||||
$auditService = $this->createMock(SystemAuditService::class);
|
||||
$auditService = $this->createMock(AuditRecorderInterface::class);
|
||||
$auditService->expects($this->once())
|
||||
->method('record')
|
||||
->with(
|
||||
@@ -293,7 +293,7 @@ class UserAccountServiceTest extends TestCase
|
||||
$scopeGateway = $this->createMock(TenantScopeService::class);
|
||||
$scopeGateway->method('canAccess')->willReturn(true);
|
||||
|
||||
$auditService = $this->createMock(SystemAuditService::class);
|
||||
$auditService = $this->createMock(AuditRecorderInterface::class);
|
||||
$auditService->expects($this->once())
|
||||
->method('record')
|
||||
->with(
|
||||
@@ -398,7 +398,7 @@ class UserAccountServiceTest extends TestCase
|
||||
$assignmentService->expects($this->never())->method('syncRoles');
|
||||
$assignmentService->expects($this->never())->method('syncDepartments');
|
||||
|
||||
$auditService = $this->createMock(SystemAuditService::class);
|
||||
$auditService = $this->createMock(AuditRecorderInterface::class);
|
||||
$auditService->expects($this->never())->method('record');
|
||||
|
||||
$databaseSessionRepository = $this->createMock(DatabaseSessionRepository::class);
|
||||
@@ -628,7 +628,7 @@ class UserAccountServiceTest extends TestCase
|
||||
?UserSettingsGateway $settingsGateway = null,
|
||||
?TenantScopeService $scopeGateway = null,
|
||||
?UserDirectoryGateway $directoryGateway = null,
|
||||
?SystemAuditService $auditService = null,
|
||||
?AuditRecorderInterface $auditService = null,
|
||||
?DatabaseSessionRepository $databaseSessionRepository = null
|
||||
): UserAccountService {
|
||||
$readRepository = $readRepository ?? $this->createMock(UserReadRepositoryInterface::class);
|
||||
@@ -673,7 +673,7 @@ class UserAccountServiceTest extends TestCase
|
||||
$directoryGateway->method('findTenantByUuid')->willReturn(null);
|
||||
}
|
||||
|
||||
$auditService = $auditService ?? $this->createMock(SystemAuditService::class);
|
||||
$auditService = $auditService ?? $this->createMock(AuditRecorderInterface::class);
|
||||
$databaseSessionRepository = $databaseSessionRepository ?? $this->createMock(DatabaseSessionRepository::class);
|
||||
|
||||
return new UserAccountService(
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace MintyPHP\Tests\Service\User;
|
||||
use MintyPHP\Repository\Support\DatabaseSessionRepository;
|
||||
use MintyPHP\Repository\User\UserReadRepositoryInterface;
|
||||
use MintyPHP\Repository\User\UserWriteRepositoryInterface;
|
||||
use MintyPHP\Service\Audit\UserLifecycleAuditService;
|
||||
use MintyPHP\Service\Audit\UserLifecycleAuditInterface;
|
||||
use MintyPHP\Service\User\UserLifecycleRestoreService;
|
||||
use MintyPHP\Service\User\UserSettingsGateway;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@@ -32,7 +32,7 @@ class UserLifecycleRestoreServiceTest extends TestCase
|
||||
'theme' => 'dark',
|
||||
];
|
||||
|
||||
$auditService = $this->createMock(UserLifecycleAuditService::class);
|
||||
$auditService = $this->createMock(UserLifecycleAuditInterface::class);
|
||||
$auditService->expects($this->once())
|
||||
->method('findDeleteEventForRestore')
|
||||
->with(1, true)
|
||||
@@ -143,7 +143,7 @@ class UserLifecycleRestoreServiceTest extends TestCase
|
||||
|
||||
public function testRestoreReturnsAuditEventNotFoundWhenEventIsNull(): void
|
||||
{
|
||||
$auditService = $this->createMock(UserLifecycleAuditService::class);
|
||||
$auditService = $this->createMock(UserLifecycleAuditInterface::class);
|
||||
$auditService->expects($this->once())
|
||||
->method('findDeleteEventForRestore')
|
||||
->with(1, true)
|
||||
@@ -169,7 +169,7 @@ class UserLifecycleRestoreServiceTest extends TestCase
|
||||
|
||||
public function testRestoreReturnsAlreadyRestoredWhenEventHasRestoredAt(): void
|
||||
{
|
||||
$auditService = $this->createMock(UserLifecycleAuditService::class);
|
||||
$auditService = $this->createMock(UserLifecycleAuditInterface::class);
|
||||
$auditService->method('findDeleteEventForRestore')->willReturn([
|
||||
'id' => 1,
|
||||
'restored_at' => '2025-01-01 00:00:00',
|
||||
@@ -193,7 +193,7 @@ class UserLifecycleRestoreServiceTest extends TestCase
|
||||
|
||||
public function testRestoreReturnsSnapshotUnavailableWhenDecryptReturnsNull(): void
|
||||
{
|
||||
$auditService = $this->createMock(UserLifecycleAuditService::class);
|
||||
$auditService = $this->createMock(UserLifecycleAuditInterface::class);
|
||||
$auditService->method('findDeleteEventForRestore')->willReturn([
|
||||
'id' => 1,
|
||||
'restored_at' => null,
|
||||
@@ -218,7 +218,7 @@ class UserLifecycleRestoreServiceTest extends TestCase
|
||||
|
||||
public function testRestoreReturnsSnapshotInvalidWhenUuidIsMissing(): void
|
||||
{
|
||||
$auditService = $this->createMock(UserLifecycleAuditService::class);
|
||||
$auditService = $this->createMock(UserLifecycleAuditInterface::class);
|
||||
$auditService->method('findDeleteEventForRestore')->willReturn(['id' => 1, 'restored_at' => null]);
|
||||
$auditService->method('decryptSnapshot')->willReturn(['uuid' => '', 'email' => 'a@b.com']);
|
||||
|
||||
@@ -238,7 +238,7 @@ class UserLifecycleRestoreServiceTest extends TestCase
|
||||
|
||||
public function testRestoreReturnsSnapshotInvalidWhenEmailIsMissing(): void
|
||||
{
|
||||
$auditService = $this->createMock(UserLifecycleAuditService::class);
|
||||
$auditService = $this->createMock(UserLifecycleAuditInterface::class);
|
||||
$auditService->method('findDeleteEventForRestore')->willReturn(['id' => 1, 'restored_at' => null]);
|
||||
$auditService->method('decryptSnapshot')->willReturn(['uuid' => 'u1', 'email' => '']);
|
||||
|
||||
@@ -260,7 +260,7 @@ class UserLifecycleRestoreServiceTest extends TestCase
|
||||
|
||||
public function testRestoreReturnsUuidExistsWhenUserWithUuidAlreadyExists(): void
|
||||
{
|
||||
$auditService = $this->createMock(UserLifecycleAuditService::class);
|
||||
$auditService = $this->createMock(UserLifecycleAuditInterface::class);
|
||||
$auditService->method('findDeleteEventForRestore')->willReturn(['id' => 1, 'restored_at' => null]);
|
||||
$auditService->method('decryptSnapshot')->willReturn(['uuid' => 'existing-uuid', 'email' => 'new@example.com']);
|
||||
|
||||
@@ -287,7 +287,7 @@ class UserLifecycleRestoreServiceTest extends TestCase
|
||||
|
||||
public function testRestoreReturnsEmailExistsWhenUserWithEmailAlreadyExists(): void
|
||||
{
|
||||
$auditService = $this->createMock(UserLifecycleAuditService::class);
|
||||
$auditService = $this->createMock(UserLifecycleAuditInterface::class);
|
||||
$auditService->method('findDeleteEventForRestore')->willReturn(['id' => 1, 'restored_at' => null]);
|
||||
$auditService->method('decryptSnapshot')->willReturn(['uuid' => 'new-uuid', 'email' => 'existing@example.com']);
|
||||
|
||||
@@ -315,7 +315,7 @@ class UserLifecycleRestoreServiceTest extends TestCase
|
||||
|
||||
public function testRestoreReturnsCreateFailedWhenWriteRepoReturnsZero(): void
|
||||
{
|
||||
$auditService = $this->createMock(UserLifecycleAuditService::class);
|
||||
$auditService = $this->createMock(UserLifecycleAuditInterface::class);
|
||||
$auditService->method('findDeleteEventForRestore')->willReturn(['id' => 1, 'restored_at' => null]);
|
||||
$auditService->method('decryptSnapshot')->willReturn(['uuid' => 'u1', 'email' => 'e@x.com']);
|
||||
|
||||
@@ -348,7 +348,7 @@ class UserLifecycleRestoreServiceTest extends TestCase
|
||||
|
||||
public function testRestoreReturnsCreateFailedWhenWriteRepoReturnsFalse(): void
|
||||
{
|
||||
$auditService = $this->createMock(UserLifecycleAuditService::class);
|
||||
$auditService = $this->createMock(UserLifecycleAuditInterface::class);
|
||||
$auditService->method('findDeleteEventForRestore')->willReturn(['id' => 1, 'restored_at' => null]);
|
||||
$auditService->method('decryptSnapshot')->willReturn(['uuid' => 'u1', 'email' => 'e@x.com']);
|
||||
|
||||
@@ -383,7 +383,7 @@ class UserLifecycleRestoreServiceTest extends TestCase
|
||||
|
||||
public function testRestoreReturnsMarkFailedWhenMarkRestoredReturnsFalse(): void
|
||||
{
|
||||
$auditService = $this->createMock(UserLifecycleAuditService::class);
|
||||
$auditService = $this->createMock(UserLifecycleAuditInterface::class);
|
||||
$auditService->method('findDeleteEventForRestore')->willReturn(['id' => 1, 'restored_at' => null]);
|
||||
$auditService->method('decryptSnapshot')->willReturn(['uuid' => 'u1', 'email' => 'e@x.com']);
|
||||
$auditService->expects($this->once())
|
||||
@@ -423,7 +423,7 @@ class UserLifecycleRestoreServiceTest extends TestCase
|
||||
|
||||
public function testRestoreRollsBackOnUnexpectedException(): void
|
||||
{
|
||||
$auditService = $this->createMock(UserLifecycleAuditService::class);
|
||||
$auditService = $this->createMock(UserLifecycleAuditInterface::class);
|
||||
$auditService->method('findDeleteEventForRestore')
|
||||
->willThrowException(new \RuntimeException('unexpected'));
|
||||
|
||||
@@ -445,7 +445,7 @@ class UserLifecycleRestoreServiceTest extends TestCase
|
||||
|
||||
public function testRestoreRollbackQuietlySwallowsRollbackException(): void
|
||||
{
|
||||
$auditService = $this->createMock(UserLifecycleAuditService::class);
|
||||
$auditService = $this->createMock(UserLifecycleAuditInterface::class);
|
||||
$auditService->method('findDeleteEventForRestore')
|
||||
->willThrowException(new \RuntimeException('db error'));
|
||||
|
||||
@@ -470,14 +470,14 @@ class UserLifecycleRestoreServiceTest extends TestCase
|
||||
private function newService(
|
||||
?UserReadRepositoryInterface $userReadRepository = null,
|
||||
?UserWriteRepositoryInterface $userWriteRepository = null,
|
||||
?UserLifecycleAuditService $userLifecycleAuditService = null,
|
||||
?UserLifecycleAuditInterface $userLifecycleAuditService = null,
|
||||
?DatabaseSessionRepository $databaseSessionRepository = null,
|
||||
?UserSettingsGateway $userSettingsGateway = null,
|
||||
): UserLifecycleRestoreService {
|
||||
return new UserLifecycleRestoreService(
|
||||
$userReadRepository ?? $this->createMock(UserReadRepositoryInterface::class),
|
||||
$userWriteRepository ?? $this->createMock(UserWriteRepositoryInterface::class),
|
||||
$userLifecycleAuditService ?? $this->createMock(UserLifecycleAuditService::class),
|
||||
$userLifecycleAuditService ?? $this->createMock(UserLifecycleAuditInterface::class),
|
||||
$databaseSessionRepository ?? $this->createMock(DatabaseSessionRepository::class),
|
||||
$userSettingsGateway ?? $this->createMock(UserSettingsGateway::class),
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace MintyPHP\Tests\Service\User;
|
||||
use MintyPHP\Repository\Support\DatabaseSessionRepository;
|
||||
use MintyPHP\Repository\User\UserReadRepositoryInterface;
|
||||
use MintyPHP\Repository\User\UserWriteRepositoryInterface;
|
||||
use MintyPHP\Service\Audit\UserLifecycleAuditService;
|
||||
use MintyPHP\Service\Audit\UserLifecycleAuditInterface;
|
||||
use MintyPHP\Service\User\UserLifecycleService;
|
||||
use MintyPHP\Service\User\UserSettingsGateway;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
@@ -16,7 +16,7 @@ class UserLifecycleServiceTest extends TestCase
|
||||
private UserReadRepositoryInterface&MockObject $userReadRepository;
|
||||
private UserWriteRepositoryInterface&MockObject $userWriteRepository;
|
||||
private UserSettingsGateway&MockObject $settingsGateway;
|
||||
private UserLifecycleAuditService&MockObject $auditService;
|
||||
private UserLifecycleAuditInterface&MockObject $auditService;
|
||||
private DatabaseSessionRepository&MockObject $databaseSessionRepository;
|
||||
|
||||
protected function setUp(): void
|
||||
@@ -24,7 +24,7 @@ class UserLifecycleServiceTest extends TestCase
|
||||
$this->userReadRepository = $this->createMock(UserReadRepositoryInterface::class);
|
||||
$this->userWriteRepository = $this->createMock(UserWriteRepositoryInterface::class);
|
||||
$this->settingsGateway = $this->createMock(UserSettingsGateway::class);
|
||||
$this->auditService = $this->createMock(UserLifecycleAuditService::class);
|
||||
$this->auditService = $this->createMock(UserLifecycleAuditInterface::class);
|
||||
$this->databaseSessionRepository = $this->createMock(DatabaseSessionRepository::class);
|
||||
|
||||
// Default: lock succeeds, release is a no-op
|
||||
|
||||
@@ -6,7 +6,7 @@ use MintyPHP\App\AppContainer;
|
||||
use MintyPHP\App\Module\Contracts\EventListener;
|
||||
use MintyPHP\App\Module\ModuleEventDispatcher;
|
||||
use MintyPHP\Http\RequestContext;
|
||||
use MintyPHP\Service\Audit\SystemAuditService;
|
||||
use MintyPHP\Service\Audit\AuditRecorderInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class ModuleEventDispatcherTest extends TestCase
|
||||
@@ -119,7 +119,7 @@ final class ModuleEventDispatcherTest extends TestCase
|
||||
$container = new AppContainer();
|
||||
$container->set($failingListener::class, static fn () => $failingListener);
|
||||
|
||||
$systemAuditService = $this->createMock(SystemAuditService::class);
|
||||
$systemAuditService = $this->createMock(AuditRecorderInterface::class);
|
||||
$systemAuditService->expects($this->once())
|
||||
->method('record')
|
||||
->with(
|
||||
|
||||
Reference in New Issue
Block a user