-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement forget() builder for user data anonymization and/or deletion
- Loading branch information
Showing
9 changed files
with
357 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace Lmc\Matej\Model\Command; | ||
|
||
use Lmc\Matej\Model\Assertion; | ||
|
||
/** | ||
* UserForget any user in Matej, either by anonymizing or by deleting their entries. | ||
* Anonymization and deletion is done server-side, and is GDPR-compliant. When anonymizing the data, new user-id is | ||
* generated server-side and client library won't ever know it. | ||
*/ | ||
class UserForget extends AbstractCommand implements UserAwareInterface | ||
{ | ||
public const ANONYMIZE = 'anonymize'; | ||
public const DELETE = 'delete'; | ||
|
||
/** @var string */ | ||
private $userId; | ||
/** @var string */ | ||
private $method; | ||
|
||
private function __construct(string $userId, string $method) | ||
{ | ||
$this->setUserId($userId); | ||
$this->setForgetMethod($method); | ||
} | ||
|
||
/** | ||
* Anonymize all user data in Matej. | ||
*/ | ||
public static function anonymize(string $userId): self | ||
{ | ||
return new static($userId, self::ANONYMIZE); | ||
} | ||
|
||
/** | ||
* Completely wipe all user data from Matej. | ||
*/ | ||
public static function delete(string $userId): self | ||
{ | ||
return new static($userId, self::DELETE); | ||
} | ||
|
||
public function getUserId(): string | ||
{ | ||
return $this->userId; | ||
} | ||
|
||
public function getForgetMethod(): string | ||
{ | ||
return $this->method; | ||
} | ||
|
||
protected function setUserId(string $userId): void | ||
{ | ||
Assertion::typeIdentifier($userId); | ||
|
||
$this->userId = $userId; | ||
} | ||
|
||
protected function setForgetMethod(string $method): void | ||
{ | ||
Assertion::choice($method, [self::ANONYMIZE, self::DELETE]); | ||
|
||
$this->method = $method; | ||
} | ||
|
||
protected function getCommandType(): string | ||
{ | ||
return 'user-forget'; | ||
} | ||
|
||
protected function getCommandParameters(): array | ||
{ | ||
return [ | ||
'user_id' => $this->userId, | ||
'method' => $this->method, | ||
]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace Lmc\Matej\RequestBuilder; | ||
|
||
use Fig\Http\Message\RequestMethodInterface; | ||
use Lmc\Matej\Exception\LogicException; | ||
use Lmc\Matej\Model\Assertion; | ||
use Lmc\Matej\Model\Command\UserForget; | ||
use Lmc\Matej\Model\Request; | ||
|
||
class ForgetRequestBuilder extends AbstractRequestBuilder | ||
{ | ||
protected const ENDPOINT_PATH = '/forget'; | ||
|
||
/** @var UserForget[] */ | ||
protected $users = []; | ||
|
||
/** @return $this */ | ||
public function addUser(UserForget $user): self | ||
{ | ||
$this->users[] = $user; | ||
|
||
return $this; | ||
} | ||
|
||
/** | ||
* @param UserForget[] $users | ||
* @return $this | ||
*/ | ||
public function addUsers(array $users): self | ||
{ | ||
foreach ($users as $user) { | ||
$this->addUser($user); | ||
} | ||
|
||
return $this; | ||
} | ||
|
||
public function build(): Request | ||
{ | ||
if (empty($this->users)) { | ||
throw new LogicException( | ||
'At least one UserForget command must be added to the builder before sending the request' | ||
); | ||
} | ||
Assertion::batchSize($this->users); | ||
|
||
return new Request(static::ENDPOINT_PATH, RequestMethodInterface::METHOD_POST, $this->users, $this->requestId); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
29 changes: 29 additions & 0 deletions
29
tests/integration/RequestBuilder/ForgetRequestBuilderTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace Lmc\Matej\IntegrationTests\RequestBuilder; | ||
|
||
use Lmc\Matej\IntegrationTests\IntegrationTestCase; | ||
use Lmc\Matej\Model\Command\UserForget; | ||
|
||
/** | ||
* @covers \Lmc\Matej\RequestBuilder\ForgetRequestBuilder | ||
*/ | ||
class ForgetRequestBuilderTest extends IntegrationTestCase | ||
{ | ||
/** @test */ | ||
public function shouldExecuteForgetRequest(): void | ||
{ | ||
$response = static::createMatejInstance() | ||
->request() | ||
->forget() | ||
->addUser(UserForget::delete('user-a')) | ||
->addUser(UserForget::anonymize('user-b')) | ||
->addUsers([ | ||
UserForget::delete('user-c'), | ||
UserForget::anonymize('user-d'), | ||
]) | ||
->send(); | ||
|
||
$this->assertResponseCommandStatuses($response, ...$this->generateOkStatuses(4)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace Lmc\Matej\Model\Command; | ||
|
||
use Lmc\Matej\UnitTestCase; | ||
|
||
class UserForgetTest extends UnitTestCase | ||
{ | ||
/** @test */ | ||
public function shouldBeInstantiableViaNamedConstructor(): void | ||
{ | ||
$userId = 'user-id'; | ||
|
||
$command = UserForget::anonymize($userId); | ||
$this->assertForgetCommand($command, $userId, UserForget::ANONYMIZE); | ||
|
||
$command = UserForget::delete($userId); | ||
$this->assertForgetCommand($command, $userId, UserForget::DELETE); | ||
} | ||
|
||
/** | ||
* Execute asserts against UserForget command | ||
* | ||
* @param UserForget $command | ||
*/ | ||
private function assertForgetCommand($command, string $userId, string $method): void | ||
{ | ||
$this->assertInstanceOf(UserForget::class, $command); | ||
$this->assertSame( | ||
[ | ||
'type' => 'user-forget', | ||
'parameters' => [ | ||
'user_id' => $userId, | ||
'method' => $method, | ||
], | ||
], | ||
$command->jsonSerialize() | ||
); | ||
$this->assertSame($userId, $command->getUserId()); | ||
$this->assertSame($method, $command->getForgetMethod()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace Lmc\Matej\RequestBuilder; | ||
|
||
use Fig\Http\Message\RequestMethodInterface; | ||
use Lmc\Matej\Exception\DomainException; | ||
use Lmc\Matej\Exception\LogicException; | ||
use Lmc\Matej\Http\RequestManager; | ||
use Lmc\Matej\Model\Command\UserForget; | ||
use Lmc\Matej\Model\Request; | ||
use Lmc\Matej\Model\Response; | ||
use PHPUnit\Framework\TestCase; | ||
|
||
/** | ||
* @covers \Lmc\Matej\RequestBuilder\ForgetRequestBuilder | ||
* @covers \Lmc\Matej\RequestBuilder\AbstractRequestBuilder | ||
*/ | ||
class ForgetRequestBuilderTest extends TestCase | ||
{ | ||
/** @test */ | ||
public function shouldBuildRequestWithCommands(): void | ||
{ | ||
$builder = new ForgetRequestBuilder(); | ||
|
||
$anonymizeUserA = UserForget::anonymize('user-anonymize-a'); | ||
$anonymizeUserB = UserForget::anonymize('user-anonymize-b'); | ||
$deleteUserA = UserForget::delete('user-delete-a'); | ||
$deleteUserB = UserForget::delete('user-delete-b'); | ||
|
||
$builder->addUser($anonymizeUserA); | ||
$builder->addUser($deleteUserA); | ||
|
||
$builder->addUsers([$anonymizeUserB, $deleteUserB]); | ||
|
||
$builder->setRequestId('custom-request-id-foo'); | ||
|
||
$request = $builder->build(); | ||
|
||
$this->assertInstanceOf(Request::class, $request); | ||
$this->assertSame(RequestMethodInterface::METHOD_POST, $request->getMethod()); | ||
$this->assertSame('/forget', $request->getPath()); | ||
|
||
$requestData = $request->getData(); | ||
$this->assertCount(4, $requestData); | ||
$this->assertContains($anonymizeUserA, $requestData); | ||
$this->assertContains($anonymizeUserB, $requestData); | ||
$this->assertContains($deleteUserA, $requestData); | ||
$this->assertContains($deleteUserB, $requestData); | ||
|
||
$this->assertSame('custom-request-id-foo', $request->getRequestId()); | ||
} | ||
|
||
/** @test */ | ||
public function shouldThrowExceptionWhenBuildingEmptyCommands(): void | ||
{ | ||
$builder = new ForgetRequestBuilder(); | ||
|
||
$this->expectException(LogicException::class); | ||
$this->expectExceptionMessage('At least one UserForget command must be added to the builder before sending the request'); | ||
$builder->build(); | ||
} | ||
|
||
/** @test */ | ||
public function shouldThrowExceptionWhenBatchSizeIsTooBig(): void | ||
{ | ||
$builder = new ForgetRequestBuilder(); | ||
|
||
for ($i = 0; $i < 501; $i++) { | ||
$builder->addUser(UserForget::delete('userid-delete-' . $i)); | ||
$builder->addUser(UserForget::anonymize('userid-anonymize-' . $i)); | ||
} | ||
|
||
$this->expectException(DomainException::class); | ||
$this->expectExceptionMessage('Request contains 1002 commands, but at most 1000 is allowed in one request.'); | ||
$builder->build(); | ||
} | ||
|
||
/** @test */ | ||
public function shouldThrowExceptionWhenSendingCommandsWithoutRequestManager(): void | ||
{ | ||
$builder = new ForgetRequestBuilder(); | ||
|
||
$builder->addUser(UserForget::delete('user-delete-a')); | ||
|
||
$this->expectException(LogicException::class); | ||
$this->expectExceptionMessage('Instance of RequestManager must be set to request builder'); | ||
$builder->send(); | ||
} | ||
|
||
/** @test */ | ||
public function shouldSendRequestViaRequestManager(): void | ||
{ | ||
$requestManagerMock = $this->createMock(RequestManager::class); | ||
$requestManagerMock->expects($this->once()) | ||
->method('sendRequest') | ||
->with($this->isInstanceOf(Request::class)) | ||
->willReturn(new Response(0, 0, 0, 0)); | ||
|
||
$builder = new ForgetRequestBuilder(); | ||
$builder->setRequestManager($requestManagerMock); | ||
|
||
$builder->addUser(UserForget::delete('user-delete-a')); | ||
|
||
$builder->send(); | ||
} | ||
} |
Oops, something went wrong.