forked from fa/breadcrumb-the-shire
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>
701 lines
29 KiB
PHP
701 lines
29 KiB
PHP
<?php
|
|
|
|
namespace MintyPHP\Tests\Service\User;
|
|
|
|
use MintyPHP\Repository\Support\DatabaseSessionRepository;
|
|
use MintyPHP\Repository\User\UserListQueryRepositoryInterface;
|
|
use MintyPHP\Repository\User\UserReadRepositoryInterface;
|
|
use MintyPHP\Repository\User\UserWriteRepositoryInterface;
|
|
use MintyPHP\Service\Audit\AuditRecorderInterface;
|
|
use MintyPHP\Service\Tenant\TenantScopeService;
|
|
use MintyPHP\Service\User\UserAccountService;
|
|
use MintyPHP\Service\User\UserAssignmentService;
|
|
use MintyPHP\Service\User\UserDirectoryGateway;
|
|
use MintyPHP\Service\User\UserPasswordService;
|
|
use MintyPHP\Service\User\UserSettingsGateway;
|
|
use PHPUnit\Framework\Attributes\DataProvider;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
class UserAccountServiceTest extends TestCase
|
|
{
|
|
public function testListPagedDropsTenantUserIdForGlobalAccess(): void
|
|
{
|
|
$scopeGateway = $this->createMock(TenantScopeService::class);
|
|
$scopeGateway->expects($this->once())
|
|
->method('hasGlobalAccess')
|
|
->with(99)
|
|
->willReturn(true);
|
|
|
|
$listQueryRepository = $this->createMock(UserListQueryRepositoryInterface::class);
|
|
$listQueryRepository->expects($this->once())
|
|
->method('listPaged')
|
|
->with($this->callback(static function (array $options): bool {
|
|
return !isset($options['tenantUserId'])
|
|
&& (int) ($options['page'] ?? 0) === 2;
|
|
}))
|
|
->willReturn(['rows' => [], 'total' => 0]);
|
|
|
|
$service = $this->newService(null, null, $listQueryRepository, null, null, null, $scopeGateway);
|
|
$result = $service->listPaged(['page' => 2, 'tenantUserId' => 99]);
|
|
|
|
$this->assertSame(0, (int) ($result['total'] ?? -1));
|
|
}
|
|
|
|
#[DataProvider('missingDeleteByUuidInputProvider')]
|
|
public function testDeleteByUuidReturnsNotFoundForMissingInput(string $uuid, ?array $resolvedUser): void
|
|
{
|
|
$readRepository = $this->createMock(UserReadRepositoryInterface::class);
|
|
if ($uuid !== '') {
|
|
$readRepository->expects($this->once())
|
|
->method('findByUuid')
|
|
->with($uuid)
|
|
->willReturn($resolvedUser);
|
|
} else {
|
|
$readRepository->expects($this->never())->method('findByUuid');
|
|
}
|
|
|
|
$service = $this->newService($readRepository);
|
|
|
|
$result = $service->deleteByUuid($uuid);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame(404, (int) ($result['status'] ?? 0));
|
|
$this->assertSame('not_found', (string) ($result['error'] ?? ''));
|
|
}
|
|
|
|
public function testDeleteByUuidPreventsSelfDelete(): void
|
|
{
|
|
$readRepository = $this->createMock(UserReadRepositoryInterface::class);
|
|
$readRepository->expects($this->once())
|
|
->method('findByUuid')
|
|
->with('user-uuid')
|
|
->willReturn(['id' => 10, 'uuid' => 'user-uuid']);
|
|
|
|
$writeRepository = $this->createMock(UserWriteRepositoryInterface::class);
|
|
$writeRepository->expects($this->never())->method('delete');
|
|
|
|
$service = $this->newService($readRepository, $writeRepository);
|
|
$result = $service->deleteByUuid('user-uuid', 10);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame(400, (int) ($result['status'] ?? 0));
|
|
$this->assertSame('self_delete', (string) ($result['error'] ?? ''));
|
|
}
|
|
|
|
public function testDeleteByUuidDeletesAndRecordsAuditEvent(): void
|
|
{
|
|
$readRepository = $this->createMock(UserReadRepositoryInterface::class);
|
|
$readRepository->expects($this->once())
|
|
->method('findByUuid')
|
|
->with('user-uuid')
|
|
->willReturn(['id' => 11, 'uuid' => 'user-uuid']);
|
|
|
|
$writeRepository = $this->createMock(UserWriteRepositoryInterface::class);
|
|
$writeRepository->expects($this->once())
|
|
->method('delete')
|
|
->with(11)
|
|
->willReturn(true);
|
|
|
|
$auditService = $this->createMock(AuditRecorderInterface::class);
|
|
$auditService->expects($this->once())
|
|
->method('record')
|
|
->with(
|
|
'admin.users.delete',
|
|
'success',
|
|
$this->callback(static function (array $context): bool {
|
|
return (int) ($context['actor_user_id'] ?? 0) === 44
|
|
&& (int) ($context['target_id'] ?? 0) === 11
|
|
&& (string) ($context['target_uuid'] ?? '') === 'user-uuid';
|
|
})
|
|
);
|
|
|
|
$service = $this->newService($readRepository, $writeRepository, null, null, null, null, null, null, $auditService);
|
|
$result = $service->deleteByUuid('user-uuid', 44);
|
|
|
|
$this->assertTrue($result['ok']);
|
|
$this->assertSame(11, (int) (($result['user'] ?? [])['id'] ?? 0));
|
|
}
|
|
|
|
public function testDeleteByUuidReturnsDeleteFailedWhenRepositoryDeleteFails(): void
|
|
{
|
|
$readRepository = $this->createMock(UserReadRepositoryInterface::class);
|
|
$readRepository->expects($this->once())
|
|
->method('findByUuid')
|
|
->with('user-uuid')
|
|
->willReturn(['id' => 11, 'uuid' => 'user-uuid']);
|
|
|
|
$writeRepository = $this->createMock(UserWriteRepositoryInterface::class);
|
|
$writeRepository->expects($this->once())
|
|
->method('delete')
|
|
->with(11)
|
|
->willReturn(false);
|
|
|
|
$auditService = $this->createMock(AuditRecorderInterface::class);
|
|
$auditService->expects($this->never())->method('record');
|
|
|
|
$service = $this->newService($readRepository, $writeRepository, null, null, null, null, null, null, $auditService);
|
|
$result = $service->deleteByUuid('user-uuid', 44);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame(500, (int) ($result['status'] ?? 0));
|
|
$this->assertSame('delete_failed', (string) ($result['error'] ?? ''));
|
|
}
|
|
|
|
public function testDeleteByUuidsReturnsPermissionDeniedWhenNothingIsInScope(): void
|
|
{
|
|
$readRepository = $this->createMock(UserReadRepositoryInterface::class);
|
|
$readRepository->method('findByUuid')->willReturn(['id' => 51, 'uuid' => 'other-uuid']);
|
|
|
|
$scopeGateway = $this->createMock(TenantScopeService::class);
|
|
$scopeGateway->method('canAccess')->willReturn(false);
|
|
|
|
$writeRepository = $this->createMock(UserWriteRepositoryInterface::class);
|
|
$writeRepository->expects($this->never())->method('deleteByUuids');
|
|
|
|
$service = $this->newService($readRepository, $writeRepository, null, null, null, null, $scopeGateway);
|
|
$result = $service->deleteByUuids(['other-uuid'], 99);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame('permission_denied', (string) ($result['error'] ?? ''));
|
|
}
|
|
|
|
public function testDeleteByUuidsReturnsSelfDeleteWhenOnlyOwnUuidRemains(): void
|
|
{
|
|
$readRepository = $this->createMock(UserReadRepositoryInterface::class);
|
|
$readRepository->expects($this->once())
|
|
->method('findByUuid')
|
|
->with('self-uuid')
|
|
->willReturn(['id' => 99, 'uuid' => 'self-uuid']);
|
|
$readRepository->expects($this->once())
|
|
->method('find')
|
|
->with(99)
|
|
->willReturn(['id' => 99, 'uuid' => 'self-uuid']);
|
|
|
|
$writeRepository = $this->createMock(UserWriteRepositoryInterface::class);
|
|
$writeRepository->expects($this->never())->method('deleteByUuids');
|
|
|
|
$service = $this->newService($readRepository, $writeRepository);
|
|
$result = $service->deleteByUuids(['self-uuid'], 99);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame('self_delete', (string) ($result['error'] ?? ''));
|
|
}
|
|
|
|
public function testSetActiveByUuidPreventsSelfDeactivate(): void
|
|
{
|
|
$readRepository = $this->createMock(UserReadRepositoryInterface::class);
|
|
$readRepository->method('findByUuid')->willReturn(['id' => 42, 'uuid' => 'u-42']);
|
|
|
|
$writeRepository = $this->createMock(UserWriteRepositoryInterface::class);
|
|
$writeRepository->expects($this->never())->method('setActive');
|
|
|
|
$service = $this->newService($readRepository, $writeRepository);
|
|
$result = $service->setActiveByUuid('u-42', false, 42);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame(400, (int) ($result['status'] ?? 0));
|
|
$this->assertSame('self_deactivate', (string) ($result['error'] ?? ''));
|
|
}
|
|
|
|
public function testSetActiveByUuidReturnsNotFoundForMissingUser(): void
|
|
{
|
|
$readRepository = $this->createMock(UserReadRepositoryInterface::class);
|
|
$readRepository->expects($this->once())
|
|
->method('findByUuid')
|
|
->with('u-missing')
|
|
->willReturn(null);
|
|
|
|
$writeRepository = $this->createMock(UserWriteRepositoryInterface::class);
|
|
$writeRepository->expects($this->never())->method('setActive');
|
|
|
|
$service = $this->newService($readRepository, $writeRepository);
|
|
$result = $service->setActiveByUuid('u-missing', true, 42);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame(404, (int) ($result['status'] ?? 0));
|
|
$this->assertSame('not_found', (string) ($result['error'] ?? ''));
|
|
}
|
|
|
|
public function testSetActiveByUuidReturnsUpdateFailedWhenRepositoryRejects(): void
|
|
{
|
|
$readRepository = $this->createMock(UserReadRepositoryInterface::class);
|
|
$readRepository->method('findByUuid')->willReturn(['id' => 33, 'uuid' => 'u-33', 'active' => 0]);
|
|
|
|
$writeRepository = $this->createMock(UserWriteRepositoryInterface::class);
|
|
$writeRepository->expects($this->once())
|
|
->method('setActive')
|
|
->with(33, true, 90)
|
|
->willReturn(false);
|
|
|
|
$assignmentService = $this->createMock(UserAssignmentService::class);
|
|
$assignmentService->expects($this->never())->method('bumpAuthzVersion');
|
|
|
|
$auditService = $this->createMock(AuditRecorderInterface::class);
|
|
$auditService->expects($this->never())->method('record');
|
|
|
|
$service = $this->newService($readRepository, $writeRepository, null, $assignmentService, null, null, null, null, $auditService);
|
|
$result = $service->setActiveByUuid('u-33', true, 90);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame(500, (int) ($result['status'] ?? 0));
|
|
$this->assertSame('update_failed', (string) ($result['error'] ?? ''));
|
|
}
|
|
|
|
public function testSetActiveByUuidUpdatesAssignmentsAndAudit(): void
|
|
{
|
|
$readRepository = $this->createMock(UserReadRepositoryInterface::class);
|
|
$readRepository->method('findByUuid')->willReturn(['id' => 33, 'uuid' => 'u-33', 'active' => 0]);
|
|
|
|
$writeRepository = $this->createMock(UserWriteRepositoryInterface::class);
|
|
$writeRepository->expects($this->once())
|
|
->method('setActive')
|
|
->with(33, true, 90)
|
|
->willReturn(true);
|
|
|
|
$assignmentService = $this->createMock(UserAssignmentService::class);
|
|
$assignmentService->expects($this->once())
|
|
->method('bumpAuthzVersion')
|
|
->with(33);
|
|
|
|
$auditService = $this->createMock(AuditRecorderInterface::class);
|
|
$auditService->expects($this->once())
|
|
->method('record')
|
|
->with(
|
|
'admin.users.activate',
|
|
'success',
|
|
$this->callback(static fn ($context): bool => is_array($context))
|
|
);
|
|
|
|
$service = $this->newService($readRepository, $writeRepository, null, $assignmentService, null, null, null, null, $auditService);
|
|
$result = $service->setActiveByUuid('u-33', true, 90);
|
|
|
|
$this->assertTrue($result['ok']);
|
|
}
|
|
|
|
public function testSetActiveByUuidsBumpsAuthzVersionForResolvedUserIds(): void
|
|
{
|
|
$writeRepository = $this->createMock(UserWriteRepositoryInterface::class);
|
|
$writeRepository->expects($this->once())
|
|
->method('setActiveByUuids')
|
|
->with(['u-1', 'u-2'], false, 7)
|
|
->willReturn(true);
|
|
$writeRepository->expects($this->once())
|
|
->method('bumpAuthzVersionByUserIds')
|
|
->with([11, 12])
|
|
->willReturn(2);
|
|
|
|
$readRepository = $this->createMock(UserReadRepositoryInterface::class);
|
|
$readRepository->method('findByUuid')->willReturnMap([
|
|
['u-1', ['id' => 11, 'uuid' => 'u-1']],
|
|
['u-2', ['id' => 12, 'uuid' => 'u-2']],
|
|
]);
|
|
|
|
$scopeGateway = $this->createMock(TenantScopeService::class);
|
|
$scopeGateway->method('canAccess')->willReturn(true);
|
|
|
|
$auditService = $this->createMock(AuditRecorderInterface::class);
|
|
$auditService->expects($this->once())
|
|
->method('record')
|
|
->with(
|
|
'admin.users.bulk_update',
|
|
'success',
|
|
$this->callback(static fn ($context): bool => is_array($context))
|
|
);
|
|
|
|
$service = $this->newService($readRepository, $writeRepository, null, null, null, null, $scopeGateway, null, $auditService);
|
|
$result = $service->setActiveByUuids(['u-1', 'u-2'], false, 7);
|
|
|
|
$this->assertTrue($result['ok']);
|
|
$this->assertSame(2, (int) ($result['count'] ?? 0));
|
|
}
|
|
|
|
public function testRegisterCreatesUserAndAssignsDefaultEntities(): void
|
|
{
|
|
$passwordService = $this->createMock(UserPasswordService::class);
|
|
$passwordService->method('validatePassword')->willReturn([]);
|
|
|
|
$settingsGateway = $this->createMock(UserSettingsGateway::class);
|
|
$settingsGateway->method('getAppTheme')->willReturn('dark');
|
|
$settingsGateway->method('getDefaultTenantId')->willReturn(3);
|
|
$settingsGateway->method('getDefaultRoleId')->willReturn(4);
|
|
$settingsGateway->method('getDefaultDepartmentId')->willReturn(5);
|
|
$settingsGateway->method('normalizeTheme')
|
|
->willReturnCallback(static fn (?string $theme): string => $theme ?? 'light');
|
|
|
|
$writeRepository = $this->createMock(UserWriteRepositoryInterface::class);
|
|
$writeRepository->expects($this->once())
|
|
->method('create')
|
|
->with($this->callback(static function (array $data): bool {
|
|
return (string) ($data['email'] ?? '') === 'user@example.com'
|
|
&& (string) ($data['theme'] ?? '') === 'dark'
|
|
&& (int) ($data['primary_tenant_id'] ?? 0) === 3
|
|
&& (int) ($data['active'] ?? 0) === 1;
|
|
}))
|
|
->willReturn(77);
|
|
|
|
$readRepository = $this->createMock(UserReadRepositoryInterface::class);
|
|
$readRepository->expects($this->exactly(2))
|
|
->method('findByEmail')
|
|
->with('user@example.com')
|
|
->willReturnOnConsecutiveCalls(
|
|
null,
|
|
['id' => 77, 'uuid' => 'user-uuid']
|
|
);
|
|
|
|
$assignmentService = $this->createMock(UserAssignmentService::class);
|
|
$assignmentService->expects($this->once())->method('syncTenants')->with(77, [3])->willReturn(true);
|
|
$assignmentService->expects($this->once())->method('syncRoles')->with(77, [4])->willReturn(true);
|
|
$assignmentService->expects($this->once())->method('syncDepartments')->with(77, [5])->willReturn(true);
|
|
|
|
$service = $this->newService(
|
|
$readRepository,
|
|
$writeRepository,
|
|
null,
|
|
$assignmentService,
|
|
$passwordService,
|
|
$settingsGateway
|
|
);
|
|
|
|
$result = $service->register([
|
|
'first_name' => 'Max',
|
|
'last_name' => 'Mustermann',
|
|
'email' => 'user@example.com',
|
|
'password' => 'StrongPass1!',
|
|
'password2' => 'StrongPass1!',
|
|
]);
|
|
|
|
$this->assertTrue($result['ok']);
|
|
}
|
|
|
|
public function testCreateFromAdminRollsBackTransactionWhenTenantSyncFails(): void
|
|
{
|
|
$passwordService = $this->createMock(UserPasswordService::class);
|
|
$passwordService->method('validatePassword')->willReturn([]);
|
|
|
|
$settingsGateway = $this->createMock(UserSettingsGateway::class);
|
|
$settingsGateway->method('getDefaultTenantId')->willReturn(null);
|
|
$settingsGateway->method('getDefaultRoleId')->willReturn(null);
|
|
$settingsGateway->method('getDefaultDepartmentId')->willReturn(null);
|
|
$settingsGateway->method('getAppTheme')->willReturn('light');
|
|
$settingsGateway->method('normalizeTheme')
|
|
->willReturnCallback(static fn (?string $theme): string => $theme ?? 'light');
|
|
|
|
$writeRepository = $this->createMock(UserWriteRepositoryInterface::class);
|
|
$writeRepository->expects($this->once())->method('create')->willReturn(88);
|
|
|
|
$readRepository = $this->createMock(UserReadRepositoryInterface::class);
|
|
$readRepository->expects($this->exactly(2))
|
|
->method('findByEmail')
|
|
->with('create@example.com')
|
|
->willReturnOnConsecutiveCalls(
|
|
null,
|
|
['id' => 88, 'uuid' => 'user-uuid-88']
|
|
);
|
|
|
|
$assignmentService = $this->createMock(UserAssignmentService::class);
|
|
$assignmentService->expects($this->once())->method('normalizeTenantIds')->with([3])->willReturn([3]);
|
|
$assignmentService->expects($this->once())->method('syncTenants')->with(88, [3])->willReturn(false);
|
|
$assignmentService->expects($this->never())->method('syncRoles');
|
|
$assignmentService->expects($this->never())->method('syncDepartments');
|
|
|
|
$auditService = $this->createMock(AuditRecorderInterface::class);
|
|
$auditService->expects($this->never())->method('record');
|
|
|
|
$databaseSessionRepository = $this->createMock(DatabaseSessionRepository::class);
|
|
$databaseSessionRepository->expects($this->once())->method('beginTransaction');
|
|
$databaseSessionRepository->expects($this->never())->method('commitTransaction');
|
|
$databaseSessionRepository->expects($this->once())->method('rollbackTransaction');
|
|
|
|
$service = $this->newService(
|
|
$readRepository,
|
|
$writeRepository,
|
|
null,
|
|
$assignmentService,
|
|
$passwordService,
|
|
$settingsGateway,
|
|
null,
|
|
null,
|
|
$auditService,
|
|
$databaseSessionRepository
|
|
);
|
|
|
|
$result = $service->createFromAdmin([
|
|
'first_name' => 'Max',
|
|
'last_name' => 'Mustermann',
|
|
'email' => 'create@example.com',
|
|
'password' => 'StrongPass1!',
|
|
'password2' => 'StrongPass1!',
|
|
'tenant_ids' => [3],
|
|
'active' => 1,
|
|
], 42);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertNotEmpty($result['errors'] ?? []);
|
|
}
|
|
|
|
public function testRegisterRollsBackTransactionWhenRoleSyncFails(): void
|
|
{
|
|
$passwordService = $this->createMock(UserPasswordService::class);
|
|
$passwordService->method('validatePassword')->willReturn([]);
|
|
|
|
$settingsGateway = $this->createMock(UserSettingsGateway::class);
|
|
$settingsGateway->method('getAppTheme')->willReturn('dark');
|
|
$settingsGateway->method('getDefaultTenantId')->willReturn(3);
|
|
$settingsGateway->method('getDefaultRoleId')->willReturn(4);
|
|
$settingsGateway->method('getDefaultDepartmentId')->willReturn(5);
|
|
$settingsGateway->method('normalizeTheme')
|
|
->willReturnCallback(static fn (?string $theme): string => $theme ?? 'light');
|
|
|
|
$writeRepository = $this->createMock(UserWriteRepositoryInterface::class);
|
|
$writeRepository->expects($this->once())->method('create')->willReturn(77);
|
|
|
|
$readRepository = $this->createMock(UserReadRepositoryInterface::class);
|
|
$readRepository->expects($this->exactly(2))
|
|
->method('findByEmail')
|
|
->with('user@example.com')
|
|
->willReturnOnConsecutiveCalls(
|
|
null,
|
|
['id' => 77, 'uuid' => 'user-uuid']
|
|
);
|
|
|
|
$assignmentService = $this->createMock(UserAssignmentService::class);
|
|
$assignmentService->expects($this->once())->method('syncTenants')->with(77, [3])->willReturn(true);
|
|
$assignmentService->expects($this->once())->method('syncRoles')->with(77, [4])->willReturn(false);
|
|
$assignmentService->expects($this->never())->method('syncDepartments');
|
|
|
|
$databaseSessionRepository = $this->createMock(DatabaseSessionRepository::class);
|
|
$databaseSessionRepository->expects($this->once())->method('beginTransaction');
|
|
$databaseSessionRepository->expects($this->never())->method('commitTransaction');
|
|
$databaseSessionRepository->expects($this->once())->method('rollbackTransaction');
|
|
|
|
$service = $this->newService(
|
|
$readRepository,
|
|
$writeRepository,
|
|
null,
|
|
$assignmentService,
|
|
$passwordService,
|
|
$settingsGateway,
|
|
null,
|
|
null,
|
|
null,
|
|
$databaseSessionRepository
|
|
);
|
|
|
|
$result = $service->register([
|
|
'first_name' => 'Max',
|
|
'last_name' => 'Mustermann',
|
|
'email' => 'user@example.com',
|
|
'password' => 'StrongPass1!',
|
|
'password2' => 'StrongPass1!',
|
|
]);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertNotSame('', trim((string) ($result['error'] ?? '')));
|
|
}
|
|
|
|
public function testUpdateSelfProfileReturnsErrorWhenRequestedTenantIsNotAssigned(): void
|
|
{
|
|
$readRepository = $this->createMock(UserReadRepositoryInterface::class);
|
|
$readRepository->expects($this->once())
|
|
->method('find')
|
|
->with(15)
|
|
->willReturn([
|
|
'id' => 15,
|
|
'first_name' => 'Max',
|
|
'last_name' => 'Mustermann',
|
|
'locale' => 'de',
|
|
'theme' => 'light',
|
|
]);
|
|
|
|
$directoryGateway = $this->createMock(UserDirectoryGateway::class);
|
|
$directoryGateway->expects($this->once())
|
|
->method('findTenantByUuid')
|
|
->with('tenant-uuid-2')
|
|
->willReturn(['id' => 2, 'uuid' => 'tenant-uuid-2']);
|
|
|
|
$assignmentService = $this->createMock(UserAssignmentService::class);
|
|
$assignmentService->expects($this->once())
|
|
->method('buildAssignmentsForUser')
|
|
->with(15)
|
|
->willReturn([
|
|
'tenants' => [['id' => 1, 'uuid' => 'tenant-uuid-1']],
|
|
'roles' => [],
|
|
'departments' => [],
|
|
]);
|
|
|
|
$writeRepository = $this->createMock(UserWriteRepositoryInterface::class);
|
|
$writeRepository->expects($this->never())->method('update');
|
|
|
|
$service = $this->newService(
|
|
$readRepository,
|
|
$writeRepository,
|
|
null,
|
|
$assignmentService,
|
|
null,
|
|
null,
|
|
null,
|
|
$directoryGateway
|
|
);
|
|
|
|
$result = $service->updateSelfProfile(15, [
|
|
'first_name' => 'Max',
|
|
'last_name' => 'Mustermann',
|
|
'current_tenant_uuid' => 'tenant-uuid-2',
|
|
]);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertNotEmpty($result['errors']);
|
|
}
|
|
|
|
public function testUpdateSelfProfilePersistsDataAndTenantSwitchWhenAllowed(): void
|
|
{
|
|
$readRepository = $this->createMock(UserReadRepositoryInterface::class);
|
|
$readRepository->expects($this->once())
|
|
->method('find')
|
|
->with(21)
|
|
->willReturn([
|
|
'id' => 21,
|
|
'first_name' => 'Max',
|
|
'last_name' => 'Mustermann',
|
|
'profile_description' => '',
|
|
'job_title' => '',
|
|
'phone' => '',
|
|
'mobile' => '',
|
|
'short_dial' => '',
|
|
'address' => '',
|
|
'postal_code' => '',
|
|
'city' => '',
|
|
'country' => '',
|
|
'region' => '',
|
|
'locale' => 'de',
|
|
'theme' => 'light',
|
|
]);
|
|
|
|
$directoryGateway = $this->createMock(UserDirectoryGateway::class);
|
|
$directoryGateway->expects($this->once())
|
|
->method('findTenantByUuid')
|
|
->with('tenant-uuid-9')
|
|
->willReturn(['id' => 9, 'uuid' => 'tenant-uuid-9']);
|
|
|
|
$assignmentService = $this->createMock(UserAssignmentService::class);
|
|
$assignmentService->expects($this->once())
|
|
->method('buildAssignmentsForUser')
|
|
->with(21)
|
|
->willReturn([
|
|
'tenants' => [['id' => 9, 'uuid' => 'tenant-uuid-9']],
|
|
'roles' => [],
|
|
'departments' => [],
|
|
]);
|
|
|
|
$writeRepository = $this->createMock(UserWriteRepositoryInterface::class);
|
|
$writeRepository->expects($this->once())
|
|
->method('update')
|
|
->with(21, $this->callback(static fn ($payload): bool => is_array($payload)))
|
|
->willReturn(true);
|
|
$writeRepository->expects($this->once())
|
|
->method('setCurrentTenant')
|
|
->with(21, 9)
|
|
->willReturn(true);
|
|
|
|
$service = $this->newService(
|
|
$readRepository,
|
|
$writeRepository,
|
|
null,
|
|
$assignmentService,
|
|
null,
|
|
null,
|
|
null,
|
|
$directoryGateway
|
|
);
|
|
|
|
$result = $service->updateSelfProfile(21, [
|
|
'first_name' => 'Max',
|
|
'last_name' => 'Mustermann',
|
|
'current_tenant_uuid' => 'tenant-uuid-9',
|
|
'locale' => 'de',
|
|
'theme' => 'light',
|
|
]);
|
|
|
|
$this->assertTrue($result['ok']);
|
|
}
|
|
|
|
private function newService(
|
|
?UserReadRepositoryInterface $readRepository = null,
|
|
?UserWriteRepositoryInterface $writeRepository = null,
|
|
?UserListQueryRepositoryInterface $listQueryRepository = null,
|
|
?UserAssignmentService $assignmentService = null,
|
|
?UserPasswordService $passwordService = null,
|
|
?UserSettingsGateway $settingsGateway = null,
|
|
?TenantScopeService $scopeGateway = null,
|
|
?UserDirectoryGateway $directoryGateway = null,
|
|
?AuditRecorderInterface $auditService = null,
|
|
?DatabaseSessionRepository $databaseSessionRepository = null
|
|
): UserAccountService {
|
|
$readRepository = $readRepository ?? $this->createMock(UserReadRepositoryInterface::class);
|
|
$writeRepository = $writeRepository ?? $this->createMock(UserWriteRepositoryInterface::class);
|
|
$listQueryRepository = $listQueryRepository ?? $this->createMock(UserListQueryRepositoryInterface::class);
|
|
|
|
if ($assignmentService === null) {
|
|
$assignmentService = $this->createMock(UserAssignmentService::class);
|
|
$assignmentService->method('normalizeTenantIds')->willReturn([]);
|
|
$assignmentService->method('normalizeIdInput')->willReturn([]);
|
|
$assignmentService->method('buildAssignmentsForUser')->willReturn([
|
|
'tenants' => [],
|
|
'roles' => [],
|
|
'departments' => [],
|
|
]);
|
|
$assignmentService->method('syncTenants')->willReturn(true);
|
|
$assignmentService->method('syncRoles')->willReturn(true);
|
|
$assignmentService->method('syncDepartments')->willReturn(true);
|
|
}
|
|
|
|
if ($passwordService === null) {
|
|
$passwordService = $this->createMock(UserPasswordService::class);
|
|
$passwordService->method('validatePassword')->willReturn([]);
|
|
}
|
|
|
|
if ($settingsGateway === null) {
|
|
$settingsGateway = $this->createMock(UserSettingsGateway::class);
|
|
$settingsGateway->method('getDefaultTenantId')->willReturn(null);
|
|
$settingsGateway->method('getDefaultRoleId')->willReturn(null);
|
|
$settingsGateway->method('getDefaultDepartmentId')->willReturn(null);
|
|
$settingsGateway->method('getAppTheme')->willReturn('light');
|
|
}
|
|
|
|
if ($scopeGateway === null) {
|
|
$scopeGateway = $this->createMock(TenantScopeService::class);
|
|
$scopeGateway->method('hasGlobalAccess')->willReturn(false);
|
|
$scopeGateway->method('canAccess')->willReturn(false);
|
|
}
|
|
|
|
if ($directoryGateway === null) {
|
|
$directoryGateway = $this->createMock(UserDirectoryGateway::class);
|
|
$directoryGateway->method('findTenantByUuid')->willReturn(null);
|
|
}
|
|
|
|
$auditService = $auditService ?? $this->createMock(AuditRecorderInterface::class);
|
|
$databaseSessionRepository = $databaseSessionRepository ?? $this->createMock(DatabaseSessionRepository::class);
|
|
|
|
return new UserAccountService(
|
|
$readRepository,
|
|
$writeRepository,
|
|
$listQueryRepository,
|
|
$assignmentService,
|
|
$passwordService,
|
|
$settingsGateway,
|
|
$scopeGateway,
|
|
$directoryGateway,
|
|
$auditService,
|
|
$databaseSessionRepository
|
|
);
|
|
}
|
|
|
|
public static function missingDeleteByUuidInputProvider(): array
|
|
{
|
|
return [
|
|
'empty uuid' => ['', null],
|
|
'missing user' => ['missing-uuid', null],
|
|
];
|
|
}
|
|
}
|