fix(security): enforce atomic role/permission writes and user assignment rollback
This commit is contained in:
@@ -260,9 +260,9 @@ class UserAccountServiceTest extends TestCase
|
||||
);
|
||||
|
||||
$assignmentService = $this->createMock(UserAssignmentService::class);
|
||||
$assignmentService->expects($this->once())->method('syncTenants')->with(77, [3]);
|
||||
$assignmentService->expects($this->once())->method('syncRoles')->with(77, [4]);
|
||||
$assignmentService->expects($this->once())->method('syncDepartments')->with(77, [5]);
|
||||
$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,
|
||||
@@ -284,6 +284,128 @@ class UserAccountServiceTest extends TestCase
|
||||
$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');
|
||||
|
||||
$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(SystemAuditService::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);
|
||||
|
||||
$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);
|
||||
|
||||
Reference in New Issue
Block a user