From 43ba5485374b802e8ad7b18a3ebb8f2ddf916864 Mon Sep 17 00:00:00 2001 From: "amelie.haladjian" Date: Mon, 4 Nov 2024 15:46:38 +0100 Subject: [PATCH 1/2] feat(api tester): enable specific user authentication for given endpoint --- src/Authenticator/Authenticator.php | 1 + src/Config/Auth.php | 12 ++++ src/Config/Filters.php | 52 +++++++++++++++++ src/Definition/Example/OperationExample.php | 12 ++-- src/Definition/Token.php | 8 +++ src/Test/Suite.php | 64 +-------------------- src/Test/TestCase.php | 4 +- tests/Test/TestCaseTest.php | 4 +- 8 files changed, 85 insertions(+), 72 deletions(-) diff --git a/src/Authenticator/Authenticator.php b/src/Authenticator/Authenticator.php index 1b190f2..2a98a8f 100644 --- a/src/Authenticator/Authenticator.php +++ b/src/Authenticator/Authenticator.php @@ -47,6 +47,7 @@ public function authenticate(Auth $config, Api $api, Requester $requester): Toke $body['access_token'], explode(' ', $config->getBody()['scope'] ?? ''), $body['refresh_token'] ?? null, + $config->getFilters(), $body['token_type'] ?? null, $body['expires_in'] ?? null, ); diff --git a/src/Config/Auth.php b/src/Config/Auth.php index 7d4f08a..cd97a89 100644 --- a/src/Config/Auth.php +++ b/src/Config/Auth.php @@ -16,6 +16,8 @@ final class Auth */ private array $body = []; + private ?Filters $filters = null; + public function __construct( private readonly string $name ) { @@ -57,4 +59,14 @@ public function setBody(array $body): void { $this->body = $body; } + + public function getFilters(): ?Filters + { + return $this->filters; + } + + public function setFilters(Filters $filters): void + { + $this->filters = $filters; + } } diff --git a/src/Config/Filters.php b/src/Config/Filters.php index 7ac0ae5..5d7cc3d 100644 --- a/src/Config/Filters.php +++ b/src/Config/Filters.php @@ -4,6 +4,8 @@ namespace APITester\Config; +use APITester\Util\Filterable; +use Symfony\Component\Yaml\Tag\TaggedValue; use Symfony\Component\Yaml\Yaml; final class Filters @@ -108,6 +110,39 @@ public function writeBaseline(array $exclude): void ); } + public function includes(Filterable $object): bool + { + $include = true; + foreach ($this->getInclude() as $item) { + $include = true; + foreach ($item as $key => $value) { + [$operator, $value] = $this->handleTags($value); + if (!$object->has($key, $value, $operator)) { + $include = false; + continue 2; + } + } + break; + } + + if (!$include) { + return false; + } + + foreach ($this->getExclude() as $item) { + foreach ($item as $key => $value) { + [$operator, $value] = $this->handleTags($value); + if (!$object->has($key, $value, $operator)) { + continue 2; + } + } + $include = false; + break; + } + + return $include; + } + /** * @return array{'exclude': ?array>} */ @@ -116,4 +151,21 @@ private function getBaseLineData(): array /** @var array{'exclude': ?array>} */ return Yaml::parseFile($this->getBaseline()); } + + /** + * @return array{0: string, 1: string|int} + */ + private function handleTags(string|int|TaggedValue $value): array + { + $operator = '='; + + if ($value instanceof TaggedValue) { + if ($value->getTag() === 'NOT') { + $operator = '!='; + } + $value = (string) $value->getValue(); + } + + return [$operator, $value]; + } } diff --git a/src/Definition/Example/OperationExample.php b/src/Definition/Example/OperationExample.php index b90c273..3730234 100644 --- a/src/Definition/Example/OperationExample.php +++ b/src/Definition/Example/OperationExample.php @@ -96,6 +96,7 @@ public function setParameter( foreach ($value as $attribute => $attributeValue) { $this->{$paramProp}[$name][$attribute] = (string) $attributeValue; } + return $this; } @@ -388,11 +389,12 @@ public function setAuthenticationHeaders(Tokens $tokens, bool $ignoreScope = fal $token = $tokens->first(); } else { /** @var Token|null $token */ - $token = $tokens->where( - 'scopes', - 'includes', - $scopes - )->first(); + $token = $tokens->filter( + static fn (Token $token) => $token->getFilters()?->includes($operation) ?? false + ) + ->first() ?? $tokens->where('scopes', 'includes', $scopes) + ->first() + ; } if ($token !== null) { diff --git a/src/Definition/Token.php b/src/Definition/Token.php index 046a16a..91cc597 100644 --- a/src/Definition/Token.php +++ b/src/Definition/Token.php @@ -4,6 +4,8 @@ namespace APITester\Definition; +use APITester\Config\Filters; + final class Token { private readonly string $type; @@ -19,6 +21,7 @@ public function __construct( private readonly string $accessToken, private readonly array $scopes = [], private readonly ?string $refreshToken = null, + private readonly ?Filters $filters = null, ?string $type = null, ?int $expiresIn = null ) { @@ -26,6 +29,11 @@ public function __construct( $this->expiresIn = $expiresIn ?? 3600; } + public function getFilters(): ?Filters + { + return $this->filters; + } + public function getAccessToken(): string { return $this->accessToken; diff --git a/src/Test/Suite.php b/src/Test/Suite.php index 97e6edd..998882f 100644 --- a/src/Test/Suite.php +++ b/src/Test/Suite.php @@ -11,14 +11,12 @@ use APITester\Preparator\Exception\PreparatorLoadingException; use APITester\Preparator\TestCasesPreparator; use APITester\Requester\Requester; -use APITester\Util\Filterable; use APITester\Util\Traits\TimeBoundTrait; use Illuminate\Support\Collection; use PHPUnit\Framework\TestResult; use PHPUnit\Framework\TestSuite; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; -use Symfony\Component\Yaml\Tag\TaggedValue; /** * @internal @@ -87,43 +85,6 @@ public function setLogger(LoggerInterface $logger): void $this->logger = $logger; } - /** - * @param array> $includeFilters - * @param array> $excludeFilters - */ - public function includes(Filterable $object, array $includeFilters = [], array $excludeFilters = []): bool - { - $include = true; - foreach ($includeFilters as $item) { - $include = true; - foreach ($item as $key => $value) { - [$operator, $value] = $this->handleTags($value); - if (!$object->has($key, $value, $operator)) { - $include = false; - continue 2; - } - } - break; - } - - if (!$include) { - return false; - } - - foreach ($excludeFilters as $item) { - foreach ($item as $key => $value) { - [$operator, $value] = $this->handleTags($value); - if (!$object->has($key, $value, $operator)) { - continue 2; - } - } - $include = false; - break; - } - - return $include; - } - /** * @param array> $filter * @@ -233,13 +194,7 @@ private function prepareTestCases(): void private function filterOperation(Operations $operations): Operations { - return $operations->filter( - fn (Operation $operation) => $this->includes( - $operation, - $this->filters->getInclude(), - $this->filters->getExclude(), - ) - ); + return $operations->filter(fn (Operation $operation) => $this->filters->includes($operation)); } /** @@ -301,21 +256,4 @@ private function indexInPart(?string $part, int $index, int $total): bool return false; } - - /** - * @return array{0: string, 1: string|int} - */ - private function handleTags(string|int|\Symfony\Component\Yaml\Tag\TaggedValue $value): array - { - $operator = '='; - - if ($value instanceof TaggedValue) { - if ($value->getTag() === 'NOT') { - $operator = '!='; - } - $value = (string) $value->getValue(); - } - - return [$operator, $value]; - } } diff --git a/src/Test/TestCase.php b/src/Test/TestCase.php index c6bb7f9..92c26c9 100644 --- a/src/Test/TestCase.php +++ b/src/Test/TestCase.php @@ -72,7 +72,7 @@ final class TestCase implements \JsonSerializable, Filterable private ?string $operation; - private ?string $preparator; + private string $preparator; private Validator $validator; @@ -238,7 +238,7 @@ public function setOperation(string $operation): void $this->operation = $operation; } - public function getPreparator(): ?string + public function getPreparator(): string { return $this->preparator; } diff --git a/tests/Test/TestCaseTest.php b/tests/Test/TestCaseTest.php index ab7d185..a10c7b7 100644 --- a/tests/Test/TestCaseTest.php +++ b/tests/Test/TestCaseTest.php @@ -15,7 +15,7 @@ final class TestCaseTest extends UnitTestCase { - private ?TestCase $testCase; + private TestCase $testCase; public function testGivenValidResponseRegardingSchemaAndShouldValidateSchemaResponseOptionIsDisabledWhenAssertThenNoErrorIsThrown( ): void { @@ -134,6 +134,6 @@ private function givenTestCase( private function whenAssert(): void { - $this->testCase?->assert(); + $this->testCase->assert(); } } From 614c0e1137f7bd676d4eec40dd8ef3df9e713a3f Mon Sep 17 00:00:00 2001 From: "amelie.haladjian" Date: Mon, 18 Nov 2024 17:35:50 +0100 Subject: [PATCH 2/2] feat(api tester): add test on filtered tokens --- tests/Preparator/ExamplesPreparatorTest.php | 104 ++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/tests/Preparator/ExamplesPreparatorTest.php b/tests/Preparator/ExamplesPreparatorTest.php index b5b6697..0d7be7c 100644 --- a/tests/Preparator/ExamplesPreparatorTest.php +++ b/tests/Preparator/ExamplesPreparatorTest.php @@ -4,14 +4,18 @@ namespace APITester\Tests\Preparator; +use APITester\Config\Filters; use APITester\Definition\Api; use APITester\Definition\Body; +use APITester\Definition\Collection\Scopes; use APITester\Definition\Example\BodyExample; use APITester\Definition\Example\OperationExample; use APITester\Definition\Example\ResponseExample; use APITester\Definition\Operation; use APITester\Definition\Parameter; use APITester\Definition\Response as DefinitionResponse; +use APITester\Definition\Security\HttpSecurity; +use APITester\Definition\Token; use APITester\Preparator\Config\ExamplesPreparatorConfig; use APITester\Preparator\ExamplesPreparator; use APITester\Test\TestCase; @@ -43,6 +47,7 @@ public function testPrepare(Api $api, array $expected): void { $preparator = new ExamplesPreparator(); + $this->addTokens($preparator); $preparator->configure([]); Assert::objectsEqual( $expected, @@ -376,5 +381,104 @@ public function getExpectedTestSuites(): iterable ), ], ]; + + yield 'with filtered tokens' => [ + Api::create()->addOperation( + Operation::create('filtered_token_test', '/tokens') + ->addSecurity( + HttpSecurity::create( + 'bearer_test', + 'bearer', + scopes: Scopes::fromNames(['scope5']) + ) + ) + ->addResponse(DefinitionResponse::create(200)) + ->addExample( + OperationExample::create('200.default') + ->setResponse(new ResponseExample()) + ) + ), + [ + new TestCase( + ExamplesPreparator::getName() . ' - filtered_token_test - 200.default', + OperationExample::create('filtered_token_test') + ->setPath('/tokens') + ->setHeaders([ + 'Authorization' => ['Bearer 3333'], + ]) + ->setResponse(ResponseExample::create('200')), + ), + ], + ]; + + yield 'without filtered tokens but multiple options' => [ + Api::create()->addOperation( + Operation::create('unfiltered_token_test', '/tokens') + ->addSecurity( + HttpSecurity::create( + 'bearer_test', + 'bearer', + scopes: Scopes::fromNames(['scope5']) + ) + ) + ->addResponse(DefinitionResponse::create(200)) + ->addExample( + OperationExample::create('200.default') + ->setResponse(new ResponseExample()) + ) + ), + [ + new TestCase( + ExamplesPreparator::getName() . ' - unfiltered_token_test - 200.default', + OperationExample::create('unfiltered_token_test') + ->setPath('/tokens') + ->setHeaders([ + 'Authorization' => ['Bearer 1111'], + ]) + ->setResponse(ResponseExample::create('200')), + ), + ], + ]; + } + + private function addTokens(ExamplesPreparator $preparator): void + { + $preparator->addToken( + new Token( + 'token1', + 'oauth2_implicit', + '1111', + [ + 'scope1', + 'scope2', + 'scope5', + ], + ) + ) + ->addToken( + new Token( + 'token2', + 'oauth2_implicit', + '2222', + [ + 'scope3', + 'scope4', + ], + ) + ) + ->addToken( + new Token( + 'token3', + 'oauth2_implicit', + '3333', + [ + 'scope5', + ], + filters: new Filters(include: [[ + 'id' => 'filtered_token_test', + ]]) + ) + ) + ; } }