feat: anonymization command for request/response XML dumps
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
"doctrine/doctrine-migrations-bundle": "^3.3",
|
||||
"doctrine/orm": "^3.3",
|
||||
"easycorp/easyadmin-bundle": "^4.27",
|
||||
"fakerphp/faker": "^1.24",
|
||||
"league/flysystem-bundle": "^3.4",
|
||||
"league/flysystem-sftp-v3": "^3.29",
|
||||
"league/oauth2-server-bundle": "^1.0",
|
||||
|
||||
Generated
+64
-1
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "e3fae85e0cff82354aeffcb409015eec",
|
||||
"content-hash": "ff6bf7b0734ba021721679b2c94873a8",
|
||||
"packages": [
|
||||
{
|
||||
"name": "carbonphp/carbon-doctrine-types",
|
||||
@@ -1578,6 +1578,69 @@
|
||||
],
|
||||
"time": "2025-03-06T22:45:56+00:00"
|
||||
},
|
||||
{
|
||||
"name": "fakerphp/faker",
|
||||
"version": "v1.24.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/FakerPHP/Faker.git",
|
||||
"reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5",
|
||||
"reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.4 || ^8.0",
|
||||
"psr/container": "^1.0 || ^2.0",
|
||||
"symfony/deprecation-contracts": "^2.2 || ^3.0"
|
||||
},
|
||||
"conflict": {
|
||||
"fzaninotto/faker": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"bamarni/composer-bin-plugin": "^1.4.1",
|
||||
"doctrine/persistence": "^1.3 || ^2.0",
|
||||
"ext-intl": "*",
|
||||
"phpunit/phpunit": "^9.5.26",
|
||||
"symfony/phpunit-bridge": "^5.4.16"
|
||||
},
|
||||
"suggest": {
|
||||
"doctrine/orm": "Required to use Faker\\ORM\\Doctrine",
|
||||
"ext-curl": "Required by Faker\\Provider\\Image to download images.",
|
||||
"ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.",
|
||||
"ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.",
|
||||
"ext-mbstring": "Required for multibyte Unicode string functionality."
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Faker\\": "src/Faker/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "François Zaninotto"
|
||||
}
|
||||
],
|
||||
"description": "Faker is a PHP library that generates fake data for you.",
|
||||
"keywords": [
|
||||
"data",
|
||||
"faker",
|
||||
"fixtures"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/FakerPHP/Faker/issues",
|
||||
"source": "https://github.com/FakerPHP/Faker/tree/v1.24.1"
|
||||
},
|
||||
"time": "2024-11-21T13:46:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "lcobucci/clock",
|
||||
"version": "3.5.0",
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Service\BpnXmlAnonymizer;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:bpn:xml-anonymize',
|
||||
description: 'Anonymize personal data in BPN XML request/response dumps',
|
||||
)]
|
||||
class BpnXmlAnonymizeCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private readonly BpnXmlAnonymizer $xmlAnonymizer,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addArgument('infile', InputArgument::REQUIRED, 'Path to the input XML file')
|
||||
->addArgument('outfile', InputArgument::OPTIONAL, 'Path to the output XML file (defaults to infile)')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$inputFile = (string) $input->getArgument('infile');
|
||||
$outputFile = $input->getArgument('outfile');
|
||||
|
||||
if (false === is_file($inputFile)) {
|
||||
$io->error(sprintf('Input file not found: %s', $inputFile));
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
if (false === is_readable($inputFile)) {
|
||||
$io->error(sprintf('Input file is not readable: %s', $inputFile));
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$targetFile = null === $outputFile ? $inputFile : (string) $outputFile;
|
||||
$this->xmlAnonymizer->resetState();
|
||||
|
||||
$processingResult = $this->anonymizeFile($io, $inputFile, $targetFile, false);
|
||||
if (Command::SUCCESS !== $processingResult) {
|
||||
return $processingResult;
|
||||
}
|
||||
|
||||
$pairedInputFile = $this->derivePairedFilename($inputFile);
|
||||
if (null !== $pairedInputFile && true === is_file($pairedInputFile)) {
|
||||
if (false === is_readable($pairedInputFile)) {
|
||||
$io->error(sprintf('Paired file is not readable: %s', $pairedInputFile));
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$pairedTargetFile = $this->derivePairedOutputFilename($targetFile, $inputFile, $pairedInputFile);
|
||||
if (null === $pairedTargetFile) {
|
||||
$io->error(sprintf(
|
||||
'Unable to derive paired output filename from: %s',
|
||||
$targetFile
|
||||
));
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$processingResult = $this->anonymizeFile($io, $pairedInputFile, $pairedTargetFile, true);
|
||||
if (Command::SUCCESS !== $processingResult) {
|
||||
return $processingResult;
|
||||
}
|
||||
|
||||
$io->note(sprintf('Paired file anonymized: %s -> %s', $pairedInputFile, $pairedTargetFile));
|
||||
}
|
||||
|
||||
$io->success(sprintf(
|
||||
'Anonymized %d persons from %s to %s',
|
||||
$this->xmlAnonymizer->getLastIdentityCount(),
|
||||
$inputFile,
|
||||
$targetFile,
|
||||
));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function anonymizeFile(SymfonyStyle $io, string $inputFile, string $targetFile, bool $keepState): int
|
||||
{
|
||||
$xml = file_get_contents($inputFile);
|
||||
if (false === $xml) {
|
||||
$io->error(sprintf('Unable to read input file: %s', $inputFile));
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
try {
|
||||
$anonymizedXml = $this->xmlAnonymizer->anonymize($xml, false === $keepState);
|
||||
} catch (\InvalidArgumentException|\RuntimeException $exception) {
|
||||
$io->error(sprintf('Failed to anonymize %s: %s', $inputFile, $exception->getMessage()));
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$writeResult = file_put_contents($targetFile, $anonymizedXml);
|
||||
if (false === $writeResult) {
|
||||
$io->error(sprintf('Unable to write output file: %s', $targetFile));
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function derivePairedFilename(string $filename): ?string
|
||||
{
|
||||
if (str_ends_with($filename, '_request.xml')) {
|
||||
return substr($filename, 0, -12).'_response.xml';
|
||||
}
|
||||
|
||||
if (str_ends_with($filename, '_response.xml')) {
|
||||
return substr($filename, 0, -13).'_request.xml';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function derivePairedOutputFilename(string $outputFile, string $inputFile, string $pairedInputFile): ?string
|
||||
{
|
||||
if (str_ends_with($inputFile, '_request.xml') && str_ends_with($pairedInputFile, '_response.xml')) {
|
||||
if (false === str_ends_with($outputFile, '_request.xml')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return substr($outputFile, 0, -12).'_response.xml';
|
||||
}
|
||||
|
||||
if (str_ends_with($inputFile, '_response.xml') && str_ends_with($pairedInputFile, '_request.xml')) {
|
||||
if (false === str_ends_with($outputFile, '_response.xml')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return substr($outputFile, 0, -13).'_request.xml';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use Faker\Factory;
|
||||
use Faker\Generator;
|
||||
|
||||
class BpnXmlAnonymizer
|
||||
{
|
||||
private const SAFE_EMAIL_DOMAIN = 'example.com';
|
||||
|
||||
private Generator $faker;
|
||||
|
||||
/**
|
||||
* @var array<string, array{firstName: string, lastName: string, street: string, postalCode: string, city: string, email: string, mobilePhone: string}>
|
||||
*/
|
||||
private array $identities = [];
|
||||
|
||||
private int $fallbackIdentityCounter = 0;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->faker = Factory::create('de_DE');
|
||||
}
|
||||
|
||||
public function anonymize(string $xml, bool $resetState = true): string
|
||||
{
|
||||
if (true === $resetState) {
|
||||
$this->resetState();
|
||||
}
|
||||
|
||||
$document = new \DOMDocument();
|
||||
$document->preserveWhiteSpace = true;
|
||||
$document->formatOutput = false;
|
||||
|
||||
if (false === @$document->loadXML($xml)) {
|
||||
throw new \InvalidArgumentException('Invalid XML input.');
|
||||
}
|
||||
|
||||
$xpath = new \DOMXPath($document);
|
||||
$personNodes = $xpath->query('//anmelder | //teilnehmerliste/teilnehmer | //kundennamen/kunde');
|
||||
|
||||
if (false === $personNodes) {
|
||||
throw new \RuntimeException('Unable to query person nodes.');
|
||||
}
|
||||
|
||||
foreach ($personNodes as $index => $personNode) {
|
||||
if (false === $personNode instanceof \DOMElement) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$personId = $this->resolvePersonId($personNode, (int) $index);
|
||||
$identity = $this->getOrCreateIdentity($personId);
|
||||
|
||||
$this->replaceNameFields($personNode, $identity);
|
||||
$this->replaceEmailFields($personNode, $identity);
|
||||
$this->replaceAddressFields($personNode, $identity);
|
||||
}
|
||||
|
||||
$result = $document->saveXML();
|
||||
if (false === $result) {
|
||||
throw new \RuntimeException('Unable to serialize anonymized XML.');
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getLastIdentityCount(): int
|
||||
{
|
||||
return \count($this->identities);
|
||||
}
|
||||
|
||||
public function resetState(): void
|
||||
{
|
||||
$this->identities = [];
|
||||
$this->fallbackIdentityCounter = 0;
|
||||
}
|
||||
|
||||
private function resolvePersonId(\DOMElement $personNode, int $nodeIndex): string
|
||||
{
|
||||
foreach (['personid', 'idperson', 'idadresseperson'] as $tagName) {
|
||||
$child = $this->getDirectChild($personNode, $tagName);
|
||||
if (null !== $child) {
|
||||
$value = trim($child->textContent);
|
||||
if ('' !== $value) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$fingerprintKey = $this->buildFingerprintKey($personNode);
|
||||
if (null !== $fingerprintKey) {
|
||||
return $fingerprintKey;
|
||||
}
|
||||
|
||||
if (true === $personNode->hasAttribute('id')) {
|
||||
$attributeValue = trim((string) $personNode->getAttribute('id'));
|
||||
if ('' !== $attributeValue) {
|
||||
return sprintf('attr-id:%s', $attributeValue);
|
||||
}
|
||||
}
|
||||
|
||||
++$this->fallbackIdentityCounter;
|
||||
|
||||
return sprintf('fallback:%d:%d', $nodeIndex, $this->fallbackIdentityCounter);
|
||||
}
|
||||
|
||||
private function buildFingerprintKey(\DOMElement $personNode): ?string
|
||||
{
|
||||
$nameValues = [];
|
||||
$nameNode = $this->getDirectChild($personNode, 'name');
|
||||
if (null !== $nameNode) {
|
||||
$nameValues[] = trim($nameNode->textContent);
|
||||
}
|
||||
if (true === $personNode->hasAttribute('name')) {
|
||||
$nameValues[] = trim((string) $personNode->getAttribute('name'));
|
||||
}
|
||||
|
||||
$firstNameNode = $this->getDirectChild($personNode, 'vorname');
|
||||
if (null !== $firstNameNode) {
|
||||
$nameValues[] = trim($firstNameNode->textContent);
|
||||
}
|
||||
if (true === $personNode->hasAttribute('vorname')) {
|
||||
$nameValues[] = trim((string) $personNode->getAttribute('vorname'));
|
||||
}
|
||||
|
||||
$communicationNode = $this->getDirectChild($personNode, 'kommunikation');
|
||||
if (null !== $communicationNode) {
|
||||
$emailNode = $this->getDirectChild($communicationNode, 'email');
|
||||
if (null !== $emailNode) {
|
||||
$nameValues[] = trim($emailNode->textContent);
|
||||
}
|
||||
}
|
||||
|
||||
$addressNode = $this->getDirectChild($personNode, 'anschrift');
|
||||
if (null !== $addressNode) {
|
||||
foreach (['strasse', 'plz', 'ort'] as $fieldName) {
|
||||
$fieldNode = $this->getDirectChild($addressNode, $fieldName);
|
||||
if (null !== $fieldNode) {
|
||||
$nameValues[] = trim($fieldNode->textContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$normalizedParts = [];
|
||||
foreach ($nameValues as $value) {
|
||||
if ('' === $value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$normalizedParts[] = mb_strtolower($value);
|
||||
}
|
||||
|
||||
if (0 === \count($normalizedParts)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return sprintf('fp:%s', md5(implode('|', $normalizedParts)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $personId
|
||||
*
|
||||
* @return array{firstName: string, lastName: string, street: string, postalCode: string, city: string, email: string, mobilePhone: string}
|
||||
*/
|
||||
private function getOrCreateIdentity(string $personId): array
|
||||
{
|
||||
if (true === isset($this->identities[$personId])) {
|
||||
return $this->identities[$personId];
|
||||
}
|
||||
|
||||
$firstName = $this->faker->firstName();
|
||||
$lastName = $this->faker->lastName();
|
||||
|
||||
$street = sprintf('%s %s', $this->faker->streetName(), $this->faker->buildingNumber());
|
||||
$postalCode = $this->faker->postcode();
|
||||
$city = $this->faker->city();
|
||||
$mobilePhone = $this->faker->numerify('01#########');
|
||||
|
||||
$emailLocal = sprintf('%s.%s', $this->slug($firstName), $this->slug($lastName));
|
||||
$email = sprintf('%s@%s', $emailLocal, self::SAFE_EMAIL_DOMAIN);
|
||||
|
||||
$this->identities[$personId] = [
|
||||
'firstName' => $firstName,
|
||||
'lastName' => $lastName,
|
||||
'street' => $street,
|
||||
'postalCode' => $postalCode,
|
||||
'city' => $city,
|
||||
'email' => strtolower($email),
|
||||
'mobilePhone' => $mobilePhone,
|
||||
];
|
||||
|
||||
return $this->identities[$personId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{firstName: string, lastName: string, street: string, postalCode: string, city: string, email: string, mobilePhone: string} $identity
|
||||
*/
|
||||
private function replaceNameFields(\DOMElement $personNode, array $identity): void
|
||||
{
|
||||
$nameNode = $this->getDirectChild($personNode, 'name');
|
||||
if (null !== $nameNode) {
|
||||
$nameNode->nodeValue = $identity['lastName'];
|
||||
}
|
||||
|
||||
$firstNameNode = $this->getDirectChild($personNode, 'vorname');
|
||||
if (null !== $firstNameNode) {
|
||||
$firstNameNode->nodeValue = $identity['firstName'];
|
||||
}
|
||||
|
||||
if (true === $personNode->hasAttribute('name')) {
|
||||
$personNode->setAttribute('name', $identity['lastName']);
|
||||
}
|
||||
|
||||
if (true === $personNode->hasAttribute('vorname')) {
|
||||
$personNode->setAttribute('vorname', $identity['firstName']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{firstName: string, lastName: string, street: string, postalCode: string, city: string, email: string, mobilePhone: string} $identity
|
||||
*/
|
||||
private function replaceEmailFields(\DOMElement $personNode, array $identity): void
|
||||
{
|
||||
$communicationNode = $this->getDirectChild($personNode, 'kommunikation');
|
||||
if (null === $communicationNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
$emailNode = $this->getDirectChild($communicationNode, 'email');
|
||||
if (null !== $emailNode) {
|
||||
$emailNode->nodeValue = $identity['email'];
|
||||
}
|
||||
|
||||
$mobileNode = $this->getDirectChild($communicationNode, 'telefonmobil');
|
||||
if (null !== $mobileNode) {
|
||||
$mobileNode->nodeValue = $identity['mobilePhone'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{firstName: string, lastName: string, street: string, postalCode: string, city: string, email: string, mobilePhone: string} $identity
|
||||
*/
|
||||
private function replaceAddressFields(\DOMElement $personNode, array $identity): void
|
||||
{
|
||||
$addressNode = $this->getDirectChild($personNode, 'anschrift');
|
||||
if (null === $addressNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
$streetNode = $this->getDirectChild($addressNode, 'strasse');
|
||||
if (null !== $streetNode) {
|
||||
$streetNode->nodeValue = $identity['street'];
|
||||
}
|
||||
|
||||
$postalCodeNode = $this->getDirectChild($addressNode, 'plz');
|
||||
if (null !== $postalCodeNode) {
|
||||
$postalCodeNode->nodeValue = $identity['postalCode'];
|
||||
}
|
||||
|
||||
$cityNode = $this->getDirectChild($addressNode, 'ort');
|
||||
if (null !== $cityNode) {
|
||||
$cityNode->nodeValue = $identity['city'];
|
||||
}
|
||||
}
|
||||
|
||||
private function getDirectChild(\DOMElement $parent, string $name): ?\DOMElement
|
||||
{
|
||||
foreach ($parent->childNodes as $childNode) {
|
||||
if (false === $childNode instanceof \DOMElement) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($childNode->tagName === $name) {
|
||||
return $childNode;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function slug(string $value): string
|
||||
{
|
||||
$normalizedValue = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value);
|
||||
if (false === $normalizedValue) {
|
||||
$normalizedValue = $value;
|
||||
}
|
||||
|
||||
$normalizedValue = strtolower($normalizedValue);
|
||||
$normalizedValue = preg_replace('/[^a-z0-9]+/', '.', $normalizedValue);
|
||||
$normalizedValue = trim((string) $normalizedValue, '.');
|
||||
|
||||
if ('' === $normalizedValue) {
|
||||
return 'user';
|
||||
}
|
||||
|
||||
return $normalizedValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Service;
|
||||
|
||||
use App\Service\BpnXmlAnonymizer;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class BpnXmlAnonymizerTest extends TestCase
|
||||
{
|
||||
public function testAnonymizeReusesIdentityForSamePersonId(): void
|
||||
{
|
||||
$xml = <<<'XML'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<anfrage>
|
||||
<anmelder>
|
||||
<name>Schlegelmilch</name>
|
||||
<vorname>Mandy</vorname>
|
||||
<anschrift>
|
||||
<strasse>Am Jägersteig 19</strasse>
|
||||
<plz>40724</plz>
|
||||
<ort>Hilden</ort>
|
||||
</anschrift>
|
||||
<kommunikation>
|
||||
<email>[email protected]</email>
|
||||
<telefonmobil>02212722760</telefonmobil>
|
||||
</kommunikation>
|
||||
<idadresseperson>255752</idadresseperson>
|
||||
</anmelder>
|
||||
<teilnehmerliste>
|
||||
<teilnehmer id="1">
|
||||
<name>Schlegelmilch</name>
|
||||
<vorname>Mandy</vorname>
|
||||
<anschrift>
|
||||
<strasse>Am Jägersteig 19</strasse>
|
||||
<plz>40724</plz>
|
||||
<ort>Hilden</ort>
|
||||
</anschrift>
|
||||
<kommunikation>
|
||||
<email>[email protected]</email>
|
||||
<telefonmobil>02212722760</telefonmobil>
|
||||
</kommunikation>
|
||||
<idadresseperson>255752</idadresseperson>
|
||||
</teilnehmer>
|
||||
<teilnehmer id="2">
|
||||
<name>Danielzik</name>
|
||||
<vorname>Leon</vorname>
|
||||
<kommunikation>
|
||||
<email>[email protected]</email>
|
||||
</kommunikation>
|
||||
<idadresseperson>306886</idadresseperson>
|
||||
</teilnehmer>
|
||||
</teilnehmerliste>
|
||||
</anfrage>
|
||||
XML;
|
||||
|
||||
$anonymizer = new BpnXmlAnonymizer();
|
||||
$result = $anonymizer->anonymize($xml);
|
||||
|
||||
$document = new \DOMDocument();
|
||||
$this->assertTrue($document->loadXML($result));
|
||||
|
||||
$xpath = new \DOMXPath($document);
|
||||
|
||||
$applicantName = trim((string) $xpath->evaluate('string(//anmelder/name)'));
|
||||
$participantOneName = trim((string) $xpath->evaluate('string(//teilnehmer[@id="1"]/name)'));
|
||||
$participantOneEmail = trim((string) $xpath->evaluate('string(//teilnehmer[@id="1"]/kommunikation/email)'));
|
||||
$applicantMobilePhone = trim((string) $xpath->evaluate('string(//anmelder/kommunikation/telefonmobil)'));
|
||||
$participantOneMobilePhone = trim((string) $xpath->evaluate('string(//teilnehmer[@id="1"]/kommunikation/telefonmobil)'));
|
||||
$participantTwoName = trim((string) $xpath->evaluate('string(//teilnehmer[@id="2"]/name)'));
|
||||
|
||||
$this->assertNotSame('Schlegelmilch', $applicantName);
|
||||
$this->assertSame($applicantName, $participantOneName);
|
||||
$this->assertStringEndsWith('@example.com', $participantOneEmail);
|
||||
$this->assertNotSame('02212722760', $participantOneMobilePhone);
|
||||
$this->assertSame($applicantMobilePhone, $participantOneMobilePhone);
|
||||
$this->assertNotSame($participantOneName, $participantTwoName);
|
||||
$this->assertSame(2, $anonymizer->getLastIdentityCount());
|
||||
}
|
||||
|
||||
public function testAnonymizeReplacesKundeAttributes(): void
|
||||
{
|
||||
$xml = <<<'XML'
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ergebnis>
|
||||
<vorgaenge>
|
||||
<vorgang>
|
||||
<kundennamen>
|
||||
<kunde id="1" art="Anmelder/Teilnehmer" name="Fromme" vorname="Björn"></kunde>
|
||||
<kunde id="2" art="Teilnehmer" name="Rütten" vorname="Manuela"></kunde>
|
||||
</kundennamen>
|
||||
</vorgang>
|
||||
</vorgaenge>
|
||||
</ergebnis>
|
||||
XML;
|
||||
|
||||
$anonymizer = new BpnXmlAnonymizer();
|
||||
$result = $anonymizer->anonymize($xml);
|
||||
|
||||
$document = new \DOMDocument();
|
||||
$this->assertTrue($document->loadXML($result));
|
||||
|
||||
$xpath = new \DOMXPath($document);
|
||||
|
||||
$firstCustomerLastName = trim((string) $xpath->evaluate('string(//kunde[@id="1"]/@name)'));
|
||||
$firstCustomerFirstName = trim((string) $xpath->evaluate('string(//kunde[@id="1"]/@vorname)'));
|
||||
|
||||
$this->assertNotSame('Fromme', $firstCustomerLastName);
|
||||
$this->assertNotSame('Björn', $firstCustomerFirstName);
|
||||
}
|
||||
|
||||
public function testAnonymizeThrowsOnInvalidXml(): void
|
||||
{
|
||||
$anonymizer = new BpnXmlAnonymizer();
|
||||
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
|
||||
$anonymizer->anonymize('<invalid');
|
||||
}
|
||||
|
||||
public function testAnonymizeLinksApplicantAndParticipantWhenOnlyDataMatches(): void
|
||||
{
|
||||
$xml = <<<'XML'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<anfrage>
|
||||
<anmelder>
|
||||
<name>Schlegelmilch</name>
|
||||
<vorname>Mandy</vorname>
|
||||
<anschrift>
|
||||
<strasse>Am Jägersteig 19</strasse>
|
||||
<plz>40724</plz>
|
||||
<ort>Hilden</ort>
|
||||
</anschrift>
|
||||
<kommunikation>
|
||||
<email>[email protected]</email>
|
||||
</kommunikation>
|
||||
</anmelder>
|
||||
<teilnehmerliste>
|
||||
<teilnehmer id="1">
|
||||
<name>Schlegelmilch</name>
|
||||
<vorname>Mandy</vorname>
|
||||
<anschrift>
|
||||
<strasse>Am Jägersteig 19</strasse>
|
||||
<plz>40724</plz>
|
||||
<ort>Hilden</ort>
|
||||
</anschrift>
|
||||
<kommunikation>
|
||||
<email>[email protected]</email>
|
||||
</kommunikation>
|
||||
</teilnehmer>
|
||||
</teilnehmerliste>
|
||||
</anfrage>
|
||||
XML;
|
||||
|
||||
$anonymizer = new BpnXmlAnonymizer();
|
||||
$result = $anonymizer->anonymize($xml);
|
||||
|
||||
$document = new \DOMDocument();
|
||||
$this->assertTrue($document->loadXML($result));
|
||||
|
||||
$xpath = new \DOMXPath($document);
|
||||
|
||||
$applicantName = trim((string) $xpath->evaluate('string(//anmelder/name)'));
|
||||
$participantName = trim((string) $xpath->evaluate('string(//teilnehmer[@id="1"]/name)'));
|
||||
|
||||
$this->assertSame($applicantName, $participantName);
|
||||
$this->assertSame(1, $anonymizer->getLastIdentityCount());
|
||||
}
|
||||
|
||||
public function testAnonymizeCanReuseIdentityAcrossMultipleXmlDocuments(): void
|
||||
{
|
||||
$requestXml = <<<'XML'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<anfrage>
|
||||
<anmelder>
|
||||
<name>Schlegelmilch</name>
|
||||
<vorname>Mandy</vorname>
|
||||
<kommunikation>
|
||||
<email>[email protected]</email>
|
||||
</kommunikation>
|
||||
<idadresseperson>255752</idadresseperson>
|
||||
</anmelder>
|
||||
</anfrage>
|
||||
XML;
|
||||
|
||||
$responseXml = <<<'XML'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ergebnis>
|
||||
<teilnehmerliste>
|
||||
<teilnehmer id="1">
|
||||
<name>Schlegelmilch</name>
|
||||
<vorname>Mandy</vorname>
|
||||
<kommunikation>
|
||||
<email>[email protected]</email>
|
||||
</kommunikation>
|
||||
<idadresseperson>255752</idadresseperson>
|
||||
</teilnehmer>
|
||||
</teilnehmerliste>
|
||||
</ergebnis>
|
||||
XML;
|
||||
|
||||
$anonymizer = new BpnXmlAnonymizer();
|
||||
$anonymizedRequest = $anonymizer->anonymize($requestXml, true);
|
||||
$anonymizedResponse = $anonymizer->anonymize($responseXml, false);
|
||||
|
||||
$requestDocument = new \DOMDocument();
|
||||
$this->assertTrue($requestDocument->loadXML($anonymizedRequest));
|
||||
$requestXpath = new \DOMXPath($requestDocument);
|
||||
$requestName = trim((string) $requestXpath->evaluate('string(//anmelder/name)'));
|
||||
|
||||
$responseDocument = new \DOMDocument();
|
||||
$this->assertTrue($responseDocument->loadXML($anonymizedResponse));
|
||||
$responseXpath = new \DOMXPath($responseDocument);
|
||||
$responseName = trim((string) $responseXpath->evaluate('string(//teilnehmer[@id="1"]/name)'));
|
||||
|
||||
$this->assertSame($requestName, $responseName);
|
||||
$this->assertSame(1, $anonymizer->getLastIdentityCount());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user