From c4b27cd6a8fc55a675e20eaeba777b46064cc5a6 Mon Sep 17 00:00:00 2001 From: Daniel Maier Date: Wed, 15 Jan 2025 12:09:44 +0100 Subject: [PATCH 1/4] Implement possibility for explicit ContentType declaration in command executions --- src/Http/Command.php | 14 +++++++++++ src/Http/CommandExecutor.php | 31 ++++++++++++------------- src/Http/ContentType.php | 14 +++++++++++ tests/Unit/Http/CommandExecutorTest.php | 3 +++ 4 files changed, 46 insertions(+), 16 deletions(-) create mode 100644 src/Http/ContentType.php diff --git a/src/Http/Command.php b/src/Http/Command.php index c053f10..849c269 100644 --- a/src/Http/Command.php +++ b/src/Http/Command.php @@ -20,6 +20,7 @@ public function __construct( /** @var Representation|Collection|array|null */ private readonly Representation|Collection|array|null $payload = null, private readonly ?Criteria $criteria = null, + private readonly ?ContentType $contentType = null, ) {} public function getMethod(): Method @@ -27,6 +28,19 @@ public function getMethod(): Method return $this->method; } + public function getContentType(): ContentType + { + if ($this->contentType !== null) { + return $this->contentType; + } + + if (is_array($this->payload)) { + return ContentType::FORM_DATA; + } + + return ContentType::JSON; + } + public function getPath(): string { $placeholders = array_map( diff --git a/src/Http/CommandExecutor.php b/src/Http/CommandExecutor.php index baf4159..b5c9b7a 100644 --- a/src/Http/CommandExecutor.php +++ b/src/Http/CommandExecutor.php @@ -20,29 +20,28 @@ public function __construct( public function executeCommand(Command $command): void { + $contentType = $command->getContentType(); $payload = $command->getPayload(); - if (is_array($payload)) { - $this->client->request( - $command->getMethod()->value, - $command->getPath(), - [ - 'form_params' => $payload, - ], - ); - - return; + $options = [ + 'headers' => [ + 'Content-Type' => $contentType->value, + ], + ]; + + switch ($contentType) { + case ContentType::JSON: + $options['body'] = $this->serializer->serialize($payload); + break; + case ContentType::FORM_DATA: + $options['form_params'] = $payload; + break; } $this->client->request( $command->getMethod()->value, $command->getPath(), - [ - 'body' => $this->serializer->serialize($command->getPayload()), - 'headers' => [ - 'Content-Type' => 'application/json', - ], - ], + $options, ); } } diff --git a/src/Http/ContentType.php b/src/Http/ContentType.php new file mode 100644 index 0000000..0564133 --- /dev/null +++ b/src/Http/ContentType.php @@ -0,0 +1,14 @@ + $payload, + 'headers' => [ + 'Content-Type' => 'application/x-www-form-urlencoded', + ], ], ); From ba4f7e307a68b435f5d069da6454ee71a6d580a1 Mon Sep 17 00:00:00 2001 From: Daniel Maier Date: Wed, 15 Jan 2025 12:10:10 +0100 Subject: [PATCH 2/4] Utilize explicit content type declaration inside executeActionsEmail endpoint to comply with keycloak api --- src/Resource/Users.php | 2 ++ tests/Unit/Resource/UsersTest.php | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/Resource/Users.php b/src/Resource/Users.php index ee42b99..57496fb 100644 --- a/src/Resource/Users.php +++ b/src/Resource/Users.php @@ -9,6 +9,7 @@ use Fschmtt\Keycloak\Collection\RoleCollection; use Fschmtt\Keycloak\Collection\UserCollection; use Fschmtt\Keycloak\Http\Command; +use Fschmtt\Keycloak\Http\ContentType; use Fschmtt\Keycloak\Http\Criteria; use Fschmtt\Keycloak\Http\Method; use Fschmtt\Keycloak\Http\Query; @@ -219,6 +220,7 @@ public function executeActionsEmail(string $realm, string $userId, ?array $actio ], $actions, $criteria, + ContentType::JSON, ), ); } diff --git a/tests/Unit/Resource/UsersTest.php b/tests/Unit/Resource/UsersTest.php index c2ff178..7834d01 100644 --- a/tests/Unit/Resource/UsersTest.php +++ b/tests/Unit/Resource/UsersTest.php @@ -10,6 +10,7 @@ use Fschmtt\Keycloak\Collection\UserCollection; use Fschmtt\Keycloak\Http\Command; use Fschmtt\Keycloak\Http\CommandExecutor; +use Fschmtt\Keycloak\Http\ContentType; use Fschmtt\Keycloak\Http\Criteria; use Fschmtt\Keycloak\Http\Method; use Fschmtt\Keycloak\Http\Query; @@ -410,6 +411,7 @@ public function testExecuteActionsEmail(): void 'realm' => 'test-realm', 'userId' => 'test-user-id', ], + contentType: ContentType::JSON, ); $commandExecutor = $this->createMock(CommandExecutor::class); From c6bcf3082ea14670c355a545fc74117b3b67ff2c Mon Sep 17 00:00:00 2001 From: fschmtt Date: Fri, 17 Jan 2025 11:34:19 +0100 Subject: [PATCH 3/4] feat: allow setting content type on command --- src/Http/Command.php | 20 ++++++------------- src/Http/CommandExecutor.php | 24 ++++++++--------------- src/Http/ContentType.php | 2 +- src/Resource/Organizations.php | 2 ++ src/Resource/Users.php | 1 - tests/Integration/Resource/UsersTest.php | 20 +++++++++++++++++++ tests/Unit/Http/CommandExecutorTest.php | 11 +++++------ tests/Unit/Http/CommandTest.php | 15 ++++++++++++++ tests/Unit/Resource/OrganizationsTest.php | 2 ++ tests/Unit/Resource/UsersTest.php | 1 - 10 files changed, 59 insertions(+), 39 deletions(-) diff --git a/src/Http/Command.php b/src/Http/Command.php index 849c269..c4451b8 100644 --- a/src/Http/Command.php +++ b/src/Http/Command.php @@ -20,7 +20,7 @@ public function __construct( /** @var Representation|Collection|array|null */ private readonly Representation|Collection|array|null $payload = null, private readonly ?Criteria $criteria = null, - private readonly ?ContentType $contentType = null, + private readonly ContentType $contentType = ContentType::JSON, ) {} public function getMethod(): Method @@ -28,19 +28,6 @@ public function getMethod(): Method return $this->method; } - public function getContentType(): ContentType - { - if ($this->contentType !== null) { - return $this->contentType; - } - - if (is_array($this->payload)) { - return ContentType::FORM_DATA; - } - - return ContentType::JSON; - } - public function getPath(): string { $placeholders = array_map( @@ -75,4 +62,9 @@ public function getQuery(): string return '?' . http_build_query($this->criteria->jsonSerialize()); } + + public function getContentType(): ContentType + { + return $this->contentType; + } } diff --git a/src/Http/CommandExecutor.php b/src/Http/CommandExecutor.php index b5c9b7a..a1198eb 100644 --- a/src/Http/CommandExecutor.php +++ b/src/Http/CommandExecutor.php @@ -20,23 +20,15 @@ public function __construct( public function executeCommand(Command $command): void { - $contentType = $command->getContentType(); - $payload = $command->getPayload(); - - $options = [ - 'headers' => [ - 'Content-Type' => $contentType->value, + $options = match ($command->getContentType()) { + ContentType::JSON => [ + 'body' => $this->serializer->serialize($command->getPayload()), + 'headers' => [ + 'Content-Type' => $command->getContentType()->value, + ] ], - ]; - - switch ($contentType) { - case ContentType::JSON: - $options['body'] = $this->serializer->serialize($payload); - break; - case ContentType::FORM_DATA: - $options['form_params'] = $payload; - break; - } + ContentType::FORM_PARAMS => ['form_params' => $command->getPayload()], + }; $this->client->request( $command->getMethod()->value, diff --git a/src/Http/ContentType.php b/src/Http/ContentType.php index 0564133..fdb83e1 100644 --- a/src/Http/ContentType.php +++ b/src/Http/ContentType.php @@ -10,5 +10,5 @@ enum ContentType: string { case JSON = 'application/json'; - case FORM_DATA = 'application/x-www-form-urlencoded'; + case FORM_PARAMS = 'application/x-www-form-urlencoded'; } diff --git a/src/Resource/Organizations.php b/src/Resource/Organizations.php index 42519f8..03d3247 100644 --- a/src/Resource/Organizations.php +++ b/src/Resource/Organizations.php @@ -6,6 +6,7 @@ use Fschmtt\Keycloak\Collection\OrganizationCollection; use Fschmtt\Keycloak\Http\Command; +use Fschmtt\Keycloak\Http\ContentType; use Fschmtt\Keycloak\Http\Criteria; use Fschmtt\Keycloak\Http\Method; use Fschmtt\Keycloak\Http\Query; @@ -71,6 +72,7 @@ public function inviteUser(string $realm, string $id, string $email, string $fir 'firstName' => $firstName, 'lastName' => $lastName, ], + contentType: ContentType::FORM_PARAMS, ), ); } diff --git a/src/Resource/Users.php b/src/Resource/Users.php index 57496fb..bd278ac 100644 --- a/src/Resource/Users.php +++ b/src/Resource/Users.php @@ -220,7 +220,6 @@ public function executeActionsEmail(string $realm, string $userId, ?array $actio ], $actions, $criteria, - ContentType::JSON, ), ); } diff --git a/tests/Integration/Resource/UsersTest.php b/tests/Integration/Resource/UsersTest.php index a9b4e05..4de6d3b 100644 --- a/tests/Integration/Resource/UsersTest.php +++ b/tests/Integration/Resource/UsersTest.php @@ -189,6 +189,26 @@ public function testGetUserCredentials(): void static::assertNull($user); } + public function testExecuteActionsEmail(): void + { + $users = $this->getKeycloak()->users(); + $username = Uuid::uuid4()->toString(); + + $users->create('master', new User( + username: $username, + )); + + $user = $this->searchUserByUsername($username); + static::assertInstanceOf(User::class, $user); + + $users->executeActionsEmail('master', $user->getId(), ['UPDATE_PASSWORD']); + + $users->delete('master', $user->getId()); + + $user = $this->searchUserByUsername($username); + static::assertNull($user); + } + private function searchUserByUsername(string $username, string $realm = 'master'): ?User { /** @var User|null $user */ diff --git a/tests/Unit/Http/CommandExecutorTest.php b/tests/Unit/Http/CommandExecutorTest.php index 883ac9a..9492b51 100644 --- a/tests/Unit/Http/CommandExecutorTest.php +++ b/tests/Unit/Http/CommandExecutorTest.php @@ -7,6 +7,7 @@ use Fschmtt\Keycloak\Http\Client; use Fschmtt\Keycloak\Http\Command; use Fschmtt\Keycloak\Http\CommandExecutor; +use Fschmtt\Keycloak\Http\ContentType; use Fschmtt\Keycloak\Http\Method; use Fschmtt\Keycloak\Json\JsonEncoder; use Fschmtt\Keycloak\Serializer\Serializer; @@ -30,7 +31,7 @@ public function testCallsClientWithoutBodyIfCommandHasNoRepresentation(): void 'body' => null, 'headers' => [ 'Content-Type' => 'application/json', - ], + ] ], ); @@ -43,7 +44,7 @@ public function testCallsClientWithoutBodyIfCommandHasNoRepresentation(): void ); } - public function testCallsClientWithBodyIfCommandHasRepresentation(): void + public function testCallsClientWithJsonIfCommandHasRepresentation(): void { $command = new Command( '/path/to/resource', @@ -101,13 +102,14 @@ public function testCallsClientWithBodyIfCommandHasCollection(): void $executor->executeCommand($command); } - public function testCallsClientWithFormParamsIfCommandHasArrayPayload(): void + public function testCallsClientWithFormParamsIfCommandFormParamContentType(): void { $command = new Command( '/path/to/resource', Method::PUT, [], $payload = ['UPDATE_PASSWORD', 'VERIFY_EMAIL'], + contentType: ContentType::FORM_PARAMS, ); $client = $this->createMock(Client::class); @@ -118,9 +120,6 @@ public function testCallsClientWithFormParamsIfCommandHasArrayPayload(): void '/path/to/resource', [ 'form_params' => $payload, - 'headers' => [ - 'Content-Type' => 'application/x-www-form-urlencoded', - ], ], ); diff --git a/tests/Unit/Http/CommandTest.php b/tests/Unit/Http/CommandTest.php index 7bae2ee..8513c6a 100644 --- a/tests/Unit/Http/CommandTest.php +++ b/tests/Unit/Http/CommandTest.php @@ -5,6 +5,7 @@ namespace Fschmtt\Keycloak\Test\Unit\Http; use Fschmtt\Keycloak\Http\Command; +use Fschmtt\Keycloak\Http\ContentType; use Fschmtt\Keycloak\Http\Criteria; use Fschmtt\Keycloak\Http\Method; use Fschmtt\Keycloak\Test\Unit\Stub\Collection; @@ -84,4 +85,18 @@ public function testBuildsPathWithQueryIfCriteriaIsProvided(): void ))->getPath(), ); } + + public function testContentTypeDefaultsToJson(): void + { + $command = new Command('/path', Method::GET); + + static::assertSame(ContentType::JSON, $command->getContentType()); + } + + public function testContentTypeCanBeSetToFormParams(): void + { + $command = new Command('/path', Method::GET, contentType: ContentType::FORM_PARAMS); + + static::assertSame(ContentType::FORM_PARAMS, $command->getContentType()); + } } diff --git a/tests/Unit/Resource/OrganizationsTest.php b/tests/Unit/Resource/OrganizationsTest.php index 82a53a2..4d18ac8 100644 --- a/tests/Unit/Resource/OrganizationsTest.php +++ b/tests/Unit/Resource/OrganizationsTest.php @@ -7,6 +7,7 @@ use Fschmtt\Keycloak\Collection\OrganizationCollection; use Fschmtt\Keycloak\Http\Command; use Fschmtt\Keycloak\Http\CommandExecutor; +use Fschmtt\Keycloak\Http\ContentType; use Fschmtt\Keycloak\Http\Method; use Fschmtt\Keycloak\Http\Query; use Fschmtt\Keycloak\Http\QueryExecutor; @@ -148,6 +149,7 @@ public function testInviteUser(): void 'firstName' => 'first name', 'lastName' => 'last name', ], + contentType: ContentType::FORM_PARAMS, ); $commandExecutor = $this->createMock(CommandExecutor::class); diff --git a/tests/Unit/Resource/UsersTest.php b/tests/Unit/Resource/UsersTest.php index 7834d01..a5f396f 100644 --- a/tests/Unit/Resource/UsersTest.php +++ b/tests/Unit/Resource/UsersTest.php @@ -411,7 +411,6 @@ public function testExecuteActionsEmail(): void 'realm' => 'test-realm', 'userId' => 'test-user-id', ], - contentType: ContentType::JSON, ); $commandExecutor = $this->createMock(CommandExecutor::class); From f8cd4fe9cf9165c4eebc059be858729deaf48352 Mon Sep 17 00:00:00 2001 From: fschmtt Date: Thu, 23 Jan 2025 21:50:31 +0100 Subject: [PATCH 4/4] chore(test): add integration test for Users::executeActionsEmail() --- src/Http/CommandExecutor.php | 2 +- tests/Integration/Resource/UsersTest.php | 10 +++++++++- tests/Unit/Http/CommandExecutorTest.php | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Http/CommandExecutor.php b/src/Http/CommandExecutor.php index a1198eb..07e84c3 100644 --- a/src/Http/CommandExecutor.php +++ b/src/Http/CommandExecutor.php @@ -25,7 +25,7 @@ public function executeCommand(Command $command): void 'body' => $this->serializer->serialize($command->getPayload()), 'headers' => [ 'Content-Type' => $command->getContentType()->value, - ] + ], ], ContentType::FORM_PARAMS => ['form_params' => $command->getPayload()], }; diff --git a/tests/Integration/Resource/UsersTest.php b/tests/Integration/Resource/UsersTest.php index 4de6d3b..10d95b5 100644 --- a/tests/Integration/Resource/UsersTest.php +++ b/tests/Integration/Resource/UsersTest.php @@ -13,6 +13,7 @@ use Fschmtt\Keycloak\Representation\Role; use Fschmtt\Keycloak\Representation\User; use Fschmtt\Keycloak\Test\Integration\IntegrationTestBehaviour; +use GuzzleHttp\Exception\ServerException; use PHPUnit\Framework\TestCase; use Ramsey\Uuid\Uuid; @@ -195,13 +196,20 @@ public function testExecuteActionsEmail(): void $username = Uuid::uuid4()->toString(); $users->create('master', new User( + email: 'john.doe@example.com', + enabled: true, username: $username, )); $user = $this->searchUserByUsername($username); static::assertInstanceOf(User::class, $user); - $users->executeActionsEmail('master', $user->getId(), ['UPDATE_PASSWORD']); + try { + $users->executeActionsEmail('master', $user->getId(), ['UPDATE_PASSWORD']); + } catch (ServerException $e) { + static::assertSame(500, $e->getResponse()->getStatusCode()); + static::assertStringContainsString('Failed to send execute actions email', $e->getResponse()->getBody()->getContents()); + } $users->delete('master', $user->getId()); diff --git a/tests/Unit/Http/CommandExecutorTest.php b/tests/Unit/Http/CommandExecutorTest.php index 9492b51..13986bb 100644 --- a/tests/Unit/Http/CommandExecutorTest.php +++ b/tests/Unit/Http/CommandExecutorTest.php @@ -31,7 +31,7 @@ public function testCallsClientWithoutBodyIfCommandHasNoRepresentation(): void 'body' => null, 'headers' => [ 'Content-Type' => 'application/json', - ] + ], ], );