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>
375 lines
11 KiB
PHP
375 lines
11 KiB
PHP
<?php
|
|
|
|
namespace MintyPHP\Tests\Service\Access;
|
|
|
|
use MintyPHP\Repository\Access\RoleRepositoryInterface;
|
|
use MintyPHP\Service\Access\RoleService;
|
|
use MintyPHP\Service\Audit\AuditRecorderInterface;
|
|
use MintyPHP\Service\Directory\DirectorySettingsGateway;
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
class RoleServiceTest extends TestCase
|
|
{
|
|
private RoleRepositoryInterface&MockObject $roleRepo;
|
|
private DirectorySettingsGateway&MockObject $settingsGateway;
|
|
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(AuditRecorderInterface::class);
|
|
|
|
$this->service = new RoleService(
|
|
$this->roleRepo,
|
|
$this->settingsGateway,
|
|
$this->auditService
|
|
);
|
|
}
|
|
|
|
public function testCreateFromAdminSucceeds(): void
|
|
{
|
|
$this->roleRepo
|
|
->expects($this->once())
|
|
->method('existsByCode')
|
|
->with('EDITOR', 0)
|
|
->willReturn(false);
|
|
|
|
$this->roleRepo
|
|
->expects($this->once())
|
|
->method('create')
|
|
->willReturn(42);
|
|
|
|
$this->roleRepo
|
|
->expects($this->once())
|
|
->method('find')
|
|
->with(42)
|
|
->willReturn(['id' => 42, 'uuid' => 'uuid-42', 'description' => 'Editor']);
|
|
|
|
$this->settingsGateway
|
|
->expects($this->never())
|
|
->method('setDefaultRoleId');
|
|
|
|
$this->auditService
|
|
->expects($this->once())
|
|
->method('record')
|
|
->with('admin.roles.create', 'success', $this->anything());
|
|
|
|
$result = $this->service->createFromAdmin([
|
|
'description' => 'Editor',
|
|
'code' => 'EDITOR',
|
|
'active' => 1,
|
|
], 5);
|
|
|
|
$this->assertTrue($result['ok']);
|
|
$this->assertSame('uuid-42', $result['uuid']);
|
|
$this->assertSame(42, $result['id']);
|
|
$this->assertEmpty($result['warnings']);
|
|
}
|
|
|
|
public function testCreateFromAdminReturnsDuplicateCodeAsWarning(): void
|
|
{
|
|
$this->roleRepo
|
|
->expects($this->any())
|
|
->method('existsByCode')
|
|
->with('DUPE', 0)
|
|
->willReturn(true);
|
|
|
|
$this->roleRepo
|
|
->expects($this->any())
|
|
->method('create')
|
|
->willReturn(50);
|
|
|
|
$this->roleRepo
|
|
->expects($this->any())
|
|
->method('find')
|
|
->with(50)
|
|
->willReturn(['id' => 50, 'uuid' => 'uuid-50', 'description' => 'Dupe Role']);
|
|
|
|
$this->auditService
|
|
->expects($this->once())
|
|
->method('record');
|
|
|
|
$result = $this->service->createFromAdmin([
|
|
'description' => 'Dupe Role',
|
|
'code' => 'DUPE',
|
|
'active' => 1,
|
|
], 1);
|
|
|
|
$this->assertTrue($result['ok']);
|
|
$this->assertNotEmpty($result['warnings']);
|
|
$this->assertEmpty($result['errors'] ?? []);
|
|
}
|
|
|
|
public function testCreateFromAdminRejectsEmptyDescription(): void
|
|
{
|
|
$this->roleRepo
|
|
->expects($this->never())
|
|
->method('create');
|
|
|
|
$result = $this->service->createFromAdmin([
|
|
'description' => ' ',
|
|
'code' => '',
|
|
'active' => 1,
|
|
]);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertNotEmpty($result['errors']);
|
|
}
|
|
|
|
public function testCreateFromAdminReturnsErrorWhenRepositoryCreateFails(): void
|
|
{
|
|
$this->roleRepo
|
|
->expects($this->once())
|
|
->method('existsByCode')
|
|
->with('EDITOR', 0)
|
|
->willReturn(false);
|
|
|
|
$this->roleRepo
|
|
->expects($this->once())
|
|
->method('create')
|
|
->willReturn(false);
|
|
|
|
$this->roleRepo
|
|
->expects($this->never())
|
|
->method('find');
|
|
|
|
$this->auditService
|
|
->expects($this->never())
|
|
->method('record');
|
|
|
|
$result = $this->service->createFromAdmin([
|
|
'description' => 'Editor',
|
|
'code' => 'EDITOR',
|
|
'active' => 1,
|
|
], 5);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertNotEmpty($result['errors']);
|
|
}
|
|
|
|
public function testCreateFromAdminSetsDefaultRoleWhenRequested(): void
|
|
{
|
|
$this->roleRepo
|
|
->method('existsByCode')
|
|
->willReturn(false);
|
|
|
|
$this->roleRepo
|
|
->method('create')
|
|
->willReturn(99);
|
|
|
|
$this->roleRepo
|
|
->expects($this->any())
|
|
->method('find')
|
|
->with(99)
|
|
->willReturn(['id' => 99, 'uuid' => 'uuid-99', 'description' => 'Default']);
|
|
|
|
$this->settingsGateway
|
|
->expects($this->once())
|
|
->method('setDefaultRoleId')
|
|
->with(99);
|
|
|
|
$this->auditService
|
|
->expects($this->once())
|
|
->method('record');
|
|
|
|
$result = $this->service->createFromAdmin([
|
|
'description' => 'Default',
|
|
'code' => '',
|
|
'active' => 1,
|
|
'is_default' => 1,
|
|
], 3);
|
|
|
|
$this->assertTrue($result['ok']);
|
|
}
|
|
|
|
public function testUpdateFromAdminSucceeds(): void
|
|
{
|
|
$this->roleRepo
|
|
->expects($this->once())
|
|
->method('existsByCode')
|
|
->with('MGR', 10)
|
|
->willReturn(false);
|
|
|
|
$this->roleRepo
|
|
->expects($this->once())
|
|
->method('find')
|
|
->with(10)
|
|
->willReturn(['id' => 10, 'uuid' => 'uuid-10', 'active' => 1]);
|
|
|
|
$this->roleRepo
|
|
->expects($this->once())
|
|
->method('update')
|
|
->with(10, $this->anything())
|
|
->willReturn(true);
|
|
|
|
$this->auditService
|
|
->expects($this->once())
|
|
->method('record')
|
|
->with('admin.roles.update', 'success', $this->anything());
|
|
|
|
$result = $this->service->updateFromAdmin(10, [
|
|
'description' => 'Manager',
|
|
'code' => 'MGR',
|
|
'active' => 1,
|
|
], 5);
|
|
|
|
$this->assertTrue($result['ok']);
|
|
}
|
|
|
|
public function testUpdateFromAdminReturnsDuplicateCodeAsWarning(): void
|
|
{
|
|
$this->roleRepo
|
|
->expects($this->once())
|
|
->method('existsByCode')
|
|
->with('ADMIN', 10)
|
|
->willReturn(true);
|
|
|
|
$this->roleRepo
|
|
->expects($this->once())
|
|
->method('find')
|
|
->with(10)
|
|
->willReturn(['id' => 10, 'uuid' => 'uuid-10', 'active' => 1]);
|
|
|
|
$this->roleRepo
|
|
->expects($this->once())
|
|
->method('update')
|
|
->with(10, $this->anything())
|
|
->willReturn(true);
|
|
|
|
$this->auditService
|
|
->expects($this->once())
|
|
->method('record');
|
|
|
|
$result = $this->service->updateFromAdmin(10, [
|
|
'description' => 'Manager',
|
|
'code' => 'ADMIN',
|
|
'active' => 1,
|
|
], 5);
|
|
|
|
$this->assertTrue($result['ok']);
|
|
$this->assertNotEmpty($result['warnings']);
|
|
$this->assertEmpty($result['errors'] ?? []);
|
|
}
|
|
|
|
public function testUpdateFromAdminReturnsErrorWhenRepositoryUpdateFails(): void
|
|
{
|
|
$this->roleRepo
|
|
->expects($this->once())
|
|
->method('existsByCode')
|
|
->with('MGR', 10)
|
|
->willReturn(false);
|
|
|
|
$this->roleRepo
|
|
->expects($this->once())
|
|
->method('find')
|
|
->with(10)
|
|
->willReturn(['id' => 10, 'uuid' => 'uuid-10', 'active' => 1]);
|
|
|
|
$this->roleRepo
|
|
->expects($this->once())
|
|
->method('update')
|
|
->with(10, $this->anything())
|
|
->willReturn(false);
|
|
|
|
$this->auditService
|
|
->expects($this->never())
|
|
->method('record');
|
|
|
|
$result = $this->service->updateFromAdmin(10, [
|
|
'description' => 'Manager',
|
|
'code' => 'MGR',
|
|
'active' => 1,
|
|
], 5);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertNotEmpty($result['errors']);
|
|
}
|
|
|
|
public function testDeleteByUuidSucceeds(): void
|
|
{
|
|
$role = ['id' => 7, 'uuid' => 'uuid-7', 'description' => 'Guest'];
|
|
|
|
$this->roleRepo
|
|
->expects($this->once())
|
|
->method('findByUuid')
|
|
->with('uuid-7')
|
|
->willReturn($role);
|
|
|
|
$this->roleRepo
|
|
->expects($this->once())
|
|
->method('delete')
|
|
->with(7)
|
|
->willReturn(true);
|
|
|
|
$this->auditService
|
|
->expects($this->once())
|
|
->method('record')
|
|
->with('admin.roles.delete', 'success', $this->anything());
|
|
|
|
$result = $this->service->deleteByUuid('uuid-7');
|
|
|
|
$this->assertTrue($result['ok']);
|
|
$this->assertSame($role, $result['role']);
|
|
}
|
|
|
|
public function testDeleteByUuidProtectsAdminRole(): void
|
|
{
|
|
$this->roleRepo
|
|
->expects($this->once())
|
|
->method('findByUuid')
|
|
->with('uuid-admin')
|
|
->willReturn(['id' => 1, 'uuid' => 'uuid-admin', 'description' => 'Admin']);
|
|
|
|
$this->roleRepo
|
|
->expects($this->never())
|
|
->method('delete');
|
|
|
|
$result = $this->service->deleteByUuid('uuid-admin');
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame(403, $result['status']);
|
|
$this->assertSame('admin_role_protected', $result['error']);
|
|
}
|
|
|
|
public function testDeleteByUuidReturnsNotFoundForEmptyUuid(): void
|
|
{
|
|
$this->roleRepo
|
|
->expects($this->never())
|
|
->method('findByUuid');
|
|
|
|
$result = $this->service->deleteByUuid(' ');
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame(404, $result['status']);
|
|
$this->assertSame('not_found', $result['error']);
|
|
}
|
|
|
|
public function testDeleteByUuidReturnsDeleteFailedOnRepoFailure(): void
|
|
{
|
|
$this->roleRepo
|
|
->expects($this->once())
|
|
->method('findByUuid')
|
|
->with('uuid-8')
|
|
->willReturn(['id' => 8, 'uuid' => 'uuid-8', 'description' => 'Viewer']);
|
|
|
|
$this->roleRepo
|
|
->expects($this->once())
|
|
->method('delete')
|
|
->with(8)
|
|
->willReturn(false);
|
|
|
|
$this->auditService
|
|
->expects($this->never())
|
|
->method('record');
|
|
|
|
$result = $this->service->deleteByUuid('uuid-8');
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame(500, $result['status']);
|
|
$this->assertSame('delete_failed', $result['error']);
|
|
}
|
|
}
|