feat: streamlined role-revocation logic

This commit is contained in:
Björn Fromme
2026-08-10 16:24:41 +02:00
parent b39a78da82
commit fd5d478a5c
14 changed files with 426 additions and 22 deletions
+58
View File
@@ -48,6 +48,59 @@ class ResponseParserTest extends TestCase
$this->assertTrue($response->isTeamer());
}
/**
* BusPro always returns the full attribute tree and expresses the roles a person holds
* through the "auswahl" flag, so a revoked role arrives as a selection set to False,
* never as a missing group. The demotion path in BpnAuthenticator relies on that: it
* treats "no roles" as a revocation only when attribute groups are present.
*/
public function testParseCrmAttributesOfAUserHoldingEveryRole(): void
{
$parser = $this->getParserInstance();
$response = $parser->parseXmlString(ApiClient::TYPE_CUSTOMER_DATA, $this->loadFixture('crm_attributes_granted.xml'));
$this->assertInstanceOf(CrmAttributesResponse::class, $response);
$this->assertTrue($response->isTeamer());
$this->assertTrue($response->isAdmin());
$this->assertTrue($response->isManager());
$this->assertTrue($response->isHouseManager());
$this->assertSame(['DKS'], $response->getHotelCodes());
// "Preisrechner Admin" is matched by id, never by its label
$this->assertCount(3, $response->getAttributeGroups());
}
public function testParseCrmAttributesOfAUserWhoseRolesWereRevoked(): void
{
$parser = $this->getParserInstance();
$response = $parser->parseXmlString(ApiClient::TYPE_CUSTOMER_DATA, $this->loadFixture('crm_attributes_revoked.xml'));
$this->assertInstanceOf(CrmAttributesResponse::class, $response);
$this->assertFalse($response->isTeamer());
$this->assertFalse($response->isAdmin());
$this->assertFalse($response->isManager());
$this->assertFalse($response->isHouseManager());
$this->assertSame([], $response->getHotelCodes());
// the groups still arrive, so this is a revocation and not a degraded response
$this->assertCount(3, $response->getAttributeGroups());
}
public function testParseCrmAttributesOfAnEmptyResponse(): void
{
$content = '<?xml version="1.0" encoding="utf-8"?><ergebnis><satz typ="KUNDENKONTO"></satz><art>SelektionCRM</art><idadresse>141747</idadresse><idperson>224526</idperson><selektionsmerkmale></selektionsmerkmale></ergebnis>';
$parser = $this->getParserInstance();
$response = $parser->parseXmlString(ApiClient::TYPE_CUSTOMER_DATA, $content);
// indistinguishable from a revocation by the roles alone, which is why the empty
// group set is what the demotion path checks
$this->assertInstanceOf(CrmAttributesResponse::class, $response);
$this->assertFalse($response->isTeamer());
$this->assertSame([], $response->getAttributeGroups());
$this->assertSame([], $response->toArray());
}
public function testParseSuccessfulProfileUpdateResponseWithoutAddressData(): void
{
$content = '<?xml version="1.0" encoding="utf-8"?><ergebnis><satz typ="KUNDENKONTO"></satz><art>Adressdaten_Ändern</art><idadresse>141747</idadresse><idperson>224526</idperson><änderung>True</änderung></ergebnis>';
@@ -106,6 +159,11 @@ class ResponseParserTest extends TestCase
$this->assertEquals('Gaststätte', $hotel->getType());
}
private function loadFixture(string $filename): string
{
return file_get_contents(__DIR__.'/../Resources/'.$filename);
}
private function getParserInstance(): ResponseParser
{
return new ResponseParser([
+51 -2
View File
@@ -213,6 +213,54 @@ class UserDataHandlerTest extends TestCase
];
}
public function testUpdateLocalUserGrantsTheTeamerRoleToAUserWhoBecameATeamer(): void
{
// created as a candidate for approval, made a teamer in the CRM afterwards
$user = (new User())
->setFirstName('First')
->setLastName('Last')
->setEmail('[email protected]')
->setRoles([User::PENDING_ROLES['ROLE_ADMIN']])
;
$handler = new UserDataHandler($this->entityManager, $this->logger);
$handler->updateLocalUser($user, $this->createProfileResponse(), true, [], [User::PENDING_ROLES['ROLE_ADMIN']]);
$this->assertSame(['ROLE_TEAMER'], $user->getAssignedRoles());
$this->assertSame([User::PENDING_ROLES['ROLE_ADMIN']], $user->getPendingRoles());
}
public function testUpdateLocalUserKeepsTheTeamerRoleOfSomebodyTheCrmNoLongerReportsAsTeamer(): void
{
// the role may have been granted manually and must survive a login
$user = (new User())
->setFirstName('First')
->setLastName('Last')
->setEmail('[email protected]')
->setRoles(['ROLE_ADMIN', 'ROLE_TEAMER'])
;
$handler = new UserDataHandler($this->entityManager, $this->logger);
$handler->updateLocalUser($user, $this->createProfileResponse(), false, [], []);
$this->assertSame(['ROLE_ADMIN', 'ROLE_TEAMER'], $user->getAssignedRoles());
}
public function testUpdateLocalUserGrantsTheTeamerRoleOnlyOnce(): void
{
$user = (new User())
->setFirstName('First')
->setLastName('Last')
->setEmail('[email protected]')
->setRoles(['ROLE_TEAMER'])
;
$handler = new UserDataHandler($this->entityManager, $this->logger);
$handler->updateLocalUser($user, $this->createProfileResponse(), true, [], []);
$this->assertSame(['ROLE_TEAMER'], $user->getAssignedRoles());
}
public function testDisableForRevokedCrmRolesBlocksTheUserAndDropsThePendingMarkers(): void
{
$user = (new User())
@@ -239,7 +287,7 @@ class UserDataHandlerTest extends TestCase
$this->assertSame([], $user->getPendingRoles());
}
public function testDisableForRevokedCrmRolesLeavesAnExistingBlockUntouched(): void
public function testDisableForRevokedCrmRolesLeavesAnExistingBlockUntouchedButStillFlushes(): void
{
$disabledAt = new \DateTimeImmutable('2026-01-01 08:00:00');
@@ -253,8 +301,9 @@ class UserDataHandlerTest extends TestCase
->setDisabledReasonInternal('Siehe Vorgang 4711.')
;
// findLocalUser() may have refreshed the BusPro ids on the way here
$this->entityManager
->expects($this->never())
->expects($this->once())
->method('flush');
$handler = new UserDataHandler($this->entityManager, $this->logger);