Files
myep-team/tests/BusProNet/ResponseParserTest.php
T

273 lines
14 KiB
PHP

<?php
namespace App\Tests\BusProNet;
use App\BusProNet\ApiClient;
use App\BusProNet\Model\CrmAttributesResponse;
use App\BusProNet\Model\ProfileResponse;
use App\BusProNet\Model\ProfileUpdateResponse;
use App\BusProNet\Model\RegistrationResponse;
use App\BusProNet\ResponseParser;
use App\BusProNet\ResponseParserException;
use PHPUnit\Framework\TestCase;
class ResponseParserTest extends TestCase
{
public function testParseUnsuccessfulResponse(): void
{
$content = '<?xml version="1.0" encoding="utf-8"?><ergebnis><satz typ="HINWEIS"><nr>853</nr><text>ID, EMail oder Passwort falsch</text></satz></ergebnis>';
$parser = $this->getParserInstance();
$response = $parser->parseXmlString(ApiClient::TYPE_NOTIFICATION, $content);
$this->assertEquals(853, $response->getCode());
$this->assertEquals('ID, EMail oder Passwort falsch', $response->getMessage());
$this->assertFalse($response->isSuccessful());
}
public function testParseSuccessfulProfileResponse(): void
{
$content = '<?xml version="1.0" encoding="utf-8"?><ergebnis><satz typ="KUNDENKONTO"></satz><art>Adressdaten</art><idadresse>141747</idadresse><idperson>224526</idperson><adressdaten><name>Fromme</name><vorname>Björn</vorname><anrede>Herr</anrede><titel></titel><geschlecht>M</geschlecht><geburtsdatum>16.04.1972</geburtsdatum><nationalitaet>D</nationalitaet><anschrift><id>119447</id><strasse>Emilienstraße 57</strasse><plz>42853</plz><ort>Remscheid</ort><ortsteil></ortsteil><land>D</land></anschrift><kommunikation><telefonmobil></telefonmobil><email>[email protected]</email><newsletter>False</newsletter><telefonprivat>02191-4615837</telefonprivat></kommunikation></adressdaten></ergebnis>';
$parser = $this->getParserInstance();
$response = $parser->parseXmlString(ApiClient::TYPE_CUSTOMER_DATA, $content);
$this->assertInstanceOf(ProfileResponse::class, $response);
}
public function testParseSuccessfulCrmAttributesResponse(): void
{
$content = '<?xml version="1.0" encoding="utf-8"?><ergebnis><satz typ="KUNDENKONTO"></satz><art>SelektionCRM</art><idadresse>141747</idadresse><idperson>224526</idperson><selektionsmerkmale><selektionsgruppe id="6" bezeichnung="Gruppen - Art (NUR direkt dem GR-Kunden zuordnen)"><selektion id="1156" bezeichnung="Gruppen-Buchungsportal" aenderbar="False" auswahl="True"></selektion></selektionsgruppe><selektionsgruppe id="10" bezeichnung="TEAM"><selektion id="1070" bezeichnung="E&amp;P Teamer - allg. Merkmal" aenderbar="False" auswahl="True"></selektion></selektionsgruppe><selektionsgruppe id="83" bezeichnung="Interessen"><selektion id="1064" bezeichnung="Sportclub-Reisen" aenderbar="True" auswahl="False"></selektion><selektion id="1065" bezeichnung="Individual-Ferienwohnungen/Gasthöfe" aenderbar="True" auswahl="False"></selektion><selektion id="1066" bezeichnung="Kurztrips" aenderbar="True" auswahl="False"></selektion><selektion id="1067" bezeichnung="Eventreisen" aenderbar="True" auswahl="False"></selektion><selektion id="1068" bezeichnung="Gruppen-Angebote" aenderbar="True" auswahl="False"></selektion><selektion id="1069" bezeichnung="Firmen-Angebote" aenderbar="True" auswahl="False"></selektion></selektionsgruppe></selektionsmerkmale><crmaktionen><crmaktion id="428" code="18DDW40" bezeichnung="Deal Der Woche KW40-2018" aenderbar="False" auswahl="False"></crmaktion><crmaktion id="276" code="NOMAIL" bezeichnung="Ich möchte keine Werbung per Mail erhalten" aenderbar="True" auswahl="True"></crmaktion></crmaktionen></ergebnis>';
$parser = $this->getParserInstance();
$response = $parser->parseXmlString(ApiClient::TYPE_CUSTOMER_DATA, $content);
$this->assertInstanceOf(CrmAttributesResponse::class, $response);
$this->assertCount(3, $response->getAttributeGroups());
$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());
}
/**
* The house of a "Hausleitung" selection comes from the configured id map, never from the
* label: an id that is not mapped grants nothing. That is also what keeps the houses
* commented out in bpn_crm_house_manager_ids from letting someone in who would then see
* no assignments and no dispositions at all.
*/
public function testParseCrmAttributesIgnoresAnUnmappedHouseManagerSelection(): void
{
$content = '<?xml version="1.0" encoding="utf-8"?><ergebnis><satz typ="KUNDENKONTO"></satz><art>SelektionCRM</art><idadresse>141747</idadresse><idperson>224526</idperson><selektionsmerkmale><selektionsgruppe id="10" bezeichnung="TEAM"><selektion id="1070" bezeichnung="E&amp;P Teamer - allg. Merkmal" aenderbar="False" auswahl="True"></selektion><selektion id="9999" bezeichnung="Hausleitung XYZ" aenderbar="False" auswahl="True"></selektion></selektionsgruppe></selektionsmerkmale></ergebnis>';
$parser = $this->getParserInstance();
$response = $parser->parseXmlString(ApiClient::TYPE_CUSTOMER_DATA, $content);
$this->assertInstanceOf(CrmAttributesResponse::class, $response);
$this->assertTrue($response->isTeamer());
$this->assertFalse($response->isHouseManager());
$this->assertSame([], $response->getHotelCodes());
}
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>';
$parser = $this->getParserInstance();
$response = $parser->parseXmlString(ApiClient::TYPE_CUSTOMER_DATA, $content);
$this->assertInstanceOf(ProfileUpdateResponse::class, $response);
$this->assertSame(141747, $response->getAddressId());
$this->assertSame(224526, $response->getPersonId());
$this->assertTrue($response->isUpdated());
}
/**
* A broken response used to collapse into one opaque "Unable to parse XML response",
* which is what made the production import failure undiagnosable. Each shape must now
* name itself.
*/
public function testParseEmptyResponse(): void
{
$parser = $this->getParserInstance();
$this->expectException(ResponseParserException::class);
$this->expectExceptionMessage('empty response');
$parser->parseXmlString(ApiClient::TYPE_CUSTOMER_DATA, '');
}
public function testParseTruncatedResponse(): void
{
$content = '<?xml version="1.0" encoding="utf-8"?><ergebnis><satz typ="KUNDENKONTO"></satz><art>Adressdaten</art';
$parser = $this->getParserInstance();
try {
$parser->parseXmlString(ApiClient::TYPE_CUSTOMER_DATA, $content);
$this->fail('Expected a ResponseParserException');
} catch (ResponseParserException $e) {
$this->assertStringContainsString('('.strlen($content).' bytes)', $e->getMessage());
$this->assertStringContainsString('line 1', $e->getMessage());
}
}
public function testParseWellFormedResponseOfAnUnknownType(): void
{
$content = '<?xml version="1.0" encoding="utf-8"?><ergebnis><satz typ="STAMMIRGENDWAS"></satz></ergebnis>';
$parser = $this->getParserInstance();
$this->expectException(ResponseParserException::class);
$this->expectExceptionMessage('Unrecognised BusProNet response type "STAMMIRGENDWAS"');
$parser->parseXmlString(ApiClient::TYPE_CUSTOMER_DATA, $content);
}
public function testParsingDoesNotLeakLibxmlErrorState(): void
{
$previous = libxml_use_internal_errors(false);
try {
try {
$this->getParserInstance()->parseXmlString(ApiClient::TYPE_CUSTOMER_DATA, '<ergebnis');
} catch (ResponseParserException) {
}
$this->assertFalse(libxml_use_internal_errors(false));
$this->assertSame([], libxml_get_errors());
} finally {
libxml_use_internal_errors($previous);
}
}
/**
* The write answers idadresse + idperson + änderung, not a selection tree - the shape
* createProfileUpdateResponse() reads. It used to be routed to the CRM attribute parser, which
* would have returned an empty response with every role flag false
* (.ddev/plans/buspro-crm-selection-write.md).
*/
public function testParseCrmSelectionWriteResponse(): void
{
$content = '<?xml version="1.0" encoding="utf-8"?><ergebnis><satz typ="KUNDENKONTO"></satz><art>SelektionCRM_Ändern</art><idadresse>141747</idadresse><idperson>224526</idperson><änderung>True</änderung></ergebnis>';
$response = $this->getParserInstance()->parseXmlString(ApiClient::TYPE_CUSTOMER_DATA, $content);
$this->assertInstanceOf(ProfileUpdateResponse::class, $response);
$this->assertSame(141747, $response->getAddressId());
$this->assertSame(224526, $response->getPersonId());
$this->assertTrue($response->isUpdated());
}
public function testParseNewAddressResponse(): void
{
$content = '<?xml version="1.0" encoding="utf-8"?><ergebnis><satz typ="KUNDENKONTO"></satz><art>Adresse_Neu</art><idadresse>141747</idadresse><idperson>224526</idperson><neuanlage>True</neuanlage></ergebnis>';
$response = $this->getParserInstance()->parseXmlString(ApiClient::TYPE_CUSTOMER_DATA, $content);
$this->assertInstanceOf(RegistrationResponse::class, $response);
$this->assertSame(141747, $response->getAddressId());
$this->assertSame(224526, $response->getPersonId());
$this->assertTrue($response->isNewRecord());
}
/**
* BusPro recognised the person and answered with the ids of the address it already held. Not a
* failure and not a duplicate - provisioning carries on with those ids.
*/
public function testParseNewAddressResponseForAnExistingPerson(): void
{
$content = '<?xml version="1.0" encoding="utf-8"?><ergebnis><satz typ="KUNDENKONTO"></satz><art>Adresse_Neu</art><idadresse>141747</idadresse><idperson>224526</idperson><neuanlage>False</neuanlage></ergebnis>';
$response = $this->getParserInstance()->parseXmlString(ApiClient::TYPE_CUSTOMER_DATA, $content);
$this->assertInstanceOf(RegistrationResponse::class, $response);
$this->assertFalse($response->isNewRecord());
}
/**
* Without both ids there is nothing to provision with, and a silent 0 would be stored on the
* account and never match again.
*/
public function testParseNewAddressResponseWithoutIdsFails(): void
{
$content = '<?xml version="1.0" encoding="utf-8"?><ergebnis><satz typ="KUNDENKONTO"></satz><art>Adresse_Neu</art><neuanlage>True</neuanlage></ergebnis>';
$this->expectException(ResponseParserException::class);
$this->getParserInstance()->parseXmlString(ApiClient::TYPE_CUSTOMER_DATA, $content);
}
private function loadFixture(string $filename): string
{
return file_get_contents(__DIR__.'/../Resources/'.$filename);
}
private function getParserInstance(): ResponseParser
{
return new ResponseParser([
'bpn_crm_id_team_admin' => 1292,
'bpn_crm_id_manager' => 1293,
'bpn_crm_id_teamer' => 1070,
// Deliberately a small excerpt of the configured map: the fixtures only carry
// these three houses, and 1301 is here to be a mapped id no fixture selects.
'bpn_crm_house_manager_ids' => [
1299 => 'SSL',
1301 => 'MVK',
1304 => 'DKS',
1305 => 'DGS',
],
]);
}
}