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:
2026-03-25 21:12:49 +01:00
parent 12a837bda9
commit 0c351f6aff
176 changed files with 2157 additions and 834 deletions

View File

@@ -3,6 +3,7 @@
namespace MintyPHP\Tests\Module\Notifications\Service;
use MintyPHP\Module\Notifications\Repository\NotificationRepositoryInterface;
use MintyPHP\Module\Notifications\Service\NotificationMessage;
use MintyPHP\Module\Notifications\Service\NotificationService;
use MintyPHP\Repository\Tenant\UserTenantRepositoryInterface;
use PHPUnit\Framework\MockObject\MockObject;
@@ -282,4 +283,95 @@ class NotificationServiceTest extends TestCase
$result = $this->service->createForUser(5, 1, 'system', 'System', null, null, ['user_id' => 33]);
$this->assertSame(56, $result);
}
public function testCreateFromMessagePersistsLocalizationMetadata(): void
{
$message = NotificationMessage::localized(
'user.created',
'New user: %s',
['Alice'],
null,
[],
'admin/users/edit/abc',
['user_id' => 33]
);
$this->notifRepo->expects($this->once())
->method('create')
->with($this->callback(function (array $data): bool {
if (!isset($data['data']['__message']) || !is_array($data['data']['__message'])) {
return false;
}
return $data['type'] === 'user.created'
&& $data['data']['__message']['title_key'] === 'New user: %s'
&& ($data['data']['__message']['title_params'] ?? []) === ['Alice']
&& $data['data']['user_id'] === 33;
}))
->willReturn(57);
$result = $this->service->createFromMessage(5, 1, $message);
$this->assertSame(57, $result);
}
public function testCreateForTenantUsersFromMessageCreatesForAllEligibleUsers(): void
{
$message = NotificationMessage::plain('system', 'Hello', null, null, []);
$this->utRepo->expects($this->once())
->method('listActiveUserIdsByTenantId')
->with(1)
->willReturn([10, 20, 30]);
$this->notifRepo->expects($this->exactly(2))
->method('create')
->willReturn(1);
$result = $this->service->createForTenantUsersFromMessage(1, $message, 20);
$this->assertSame(2, $result);
}
public function testCreateForTenantAdminUsersFromMessageRejectsEmptyAllowedSet(): void
{
$message = NotificationMessage::plain('system', 'Hello', null, null, []);
$this->utRepo->expects($this->never())->method('listActiveUserIdsByTenantId');
$this->notifRepo->expects($this->never())->method('create');
$result = $this->service->createForTenantAdminUsersFromMessage(1, $message, null, []);
$this->assertSame(0, $result);
}
public function testListForUserLocalizesFromStoredMessageMetadata(): void
{
$this->notifRepo->expects($this->once())
->method('listByUser')
->with(3, null, 50, 0)
->willReturn([
[
'id' => 1,
'type' => 'user.created',
'title' => 'fallback',
'body' => null,
'link' => null,
'data' => json_encode([
'user_id' => 10,
'__message' => [
'title_key' => 'New user: %s',
'title_params' => ['Alice'],
'body_key' => '',
'body_params' => [],
],
], JSON_THROW_ON_ERROR),
'is_read' => 0,
'created' => '2026-03-19 10:00:00',
],
]);
$result = $this->service->listForUser(3);
$this->assertCount(1, $result);
$this->assertSame(t('New user: %s', 'Alice'), $result[0]['title']);
$this->assertSame(['user_id' => 10], $result[0]['data']);
}
}