diff --git a/.gitignore b/.gitignore index a9893ed..0f35a13 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /node_modules/ /.idea/ /package-lock.json +.DS_Store diff --git a/README.md b/README.md index 792ac5d..0d1980f 100644 --- a/README.md +++ b/README.md @@ -62,12 +62,44 @@ Step 2: Configure Behat Go to `behat.yml` ```yaml -... +# ... contexts: - BehatApiContext\Context\ApiContext -... +# ... ``` +Usage +============= + +Runnable request parameters +---------------------------------- +Main use case when tests need to use current date. +Instead of static data in some `testCaseName.feature`, like this: +```feature +""" +{ + "dateTo": 1680360081, + "dateFrom": 1680532881, +} +""" +``` +Can use, for example: +```feature +""" +{ + "dateTo": "<(new DateTimeImmutable())->add(new DateInterval('P6D'))->getTimeStamp()>", + "dateFrom": "<(new DateTimeImmutable())->add(new DateInterval('P2D'))->getTimeStamp()>", +} +""" +``` + +#### To accomplish this, several conditions must be met: +- Runnable code must be a string and placed in `<>` +- Should not add `return` keyword at the beginning, otherwise will get RuntimeException +- Should not add `;` keyword at the end, otherwise will get RuntimeException +- Should not use the code that returns `null`, otherwise will get RuntimeException + + [master Build Status]: https://github.com/macpaw/behat-api-context/actions?query=workflow%3ACI+branch%3Amaster [master Build Status Image]: https://github.com/macpaw/behat-api-context/workflows/CI/badge.svg?branch=master [develop Build Status]: https://github.com/macpaw/behat-api-context/actions?query=workflow%3ACI+branch%3Adevelop diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 537d67f..02a7abf 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -27,6 +27,10 @@ parameters: count: 1 path: ./src/DependencyInjection - - message: '#Cannot call method (.*)? on object|null.*#' - count: 2 + message: '#Call to an undefined method object::clear().*#' + count: 1 + path: ./src/Service/ResetManager/DoctrineResetManager + - + message: '#Call to an undefined method object::getConnection().*#' + count: 1 path: ./src/Service/ResetManager/DoctrineResetManager diff --git a/src/Context/ApiContext.php b/src/Context/ApiContext.php index 02a77e9..0e99d3f 100644 --- a/src/Context/ApiContext.php +++ b/src/Context/ApiContext.php @@ -15,6 +15,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Routing\RouterInterface; +use Throwable; class ApiContext implements Context { @@ -114,6 +115,7 @@ public function theRequestContainsParams(PyStringNode $params): void ); $newRequestParams = (array) json_decode($processedParams, true, 512, JSON_THROW_ON_ERROR); + $newRequestParams = $this->convertRunnableCodeParams($newRequestParams); $this->requestParams = array_merge($this->requestParams, $newRequestParams); $this->savedValues = array_merge($this->savedValues, $newRequestParams); } @@ -299,6 +301,53 @@ protected function compareStructureResponse(string $variableFields, PyStringNode } } + protected function convertRunnableCodeParams(array $requestParams): array + { + foreach ($requestParams as $key => $value) { + if (is_array($value)) { + $requestParams[$key] = $this->convertRunnableCodeParams($value); + continue; + } + + if (!is_string($value)) { + continue; + } + + $pregMatchValue = preg_match('/^<.*>$/', trim($value)); + + if ($pregMatchValue === 0 || $pregMatchValue === false) { + continue; + } + + $command = substr(trim($value), 1, -1); + + try { + $resultValue = eval('return ' . $command . ';'); + } catch (Throwable $exception) { + throw new RuntimeException( + sprintf( + 'Failed run your code %s, error message: %s', + $value, + $exception->getMessage() + ) + ); + } + + if (is_null($resultValue)) { + throw new \RuntimeException( + sprintf( + 'Running code: %s - should not return the null', + $command + ) + ); + } + + $requestParams[$key] = $resultValue; + } + + return $requestParams; + } + private function resetRequestOptions(): void { $this->headers = []; @@ -314,4 +363,9 @@ protected function getResponse(): Response return $this->response; } + + public function geRequestParams(): array + { + return $this->requestParams; + } } diff --git a/tests/Unit/Context/ApiContextTest.php b/tests/Unit/Context/ApiContextTest.php new file mode 100644 index 0000000..99ba9b0 --- /dev/null +++ b/tests/Unit/Context/ApiContextTest.php @@ -0,0 +1,182 @@ +createMock(RouterInterface::class); + $requestStackMock = $this->createMock(RequestStack::class); + $kernelMock = $this->createMock(KernelInterface::class); + + $this->apiContext = new ApiContext($routerMock, $requestStackMock, $kernelMock); + } + + /** + * @param PyStringNode $paramsValues + * @param string $initialParamValue + * + * @dataProvider getTheRequestContainsParamsSuccess + */ + public function testTheRequestContainsParamsSuccess(PyStringNode $paramsValues, string $initialParamValue): void + { + $this->assertTrue(str_contains($paramsValues->getStrings()[3], $initialParamValue)); + $this->assertTrue(str_contains($paramsValues->getStrings()[6], $initialParamValue)); + $this->assertTrue(str_contains($paramsValues->getStrings()[9], $initialParamValue)); + + $this->apiContext->theRequestContainsParams($paramsValues); + + $this->assertIsInt($this->apiContext->geRequestParams()['dateFrom']); + $this->assertIsInt($this->apiContext->geRequestParams()['levelOne']['dateFrom']); + $this->assertIsInt($this->apiContext->geRequestParams()['levelOne']['levelTwo']['dateFrom']); + + $this->assertEquals( + 10, + strlen((string)$this->apiContext->geRequestParams()['dateFrom']) + ); + $this->assertEquals( + 10, + strlen((string)$this->apiContext->geRequestParams()['levelOne']['dateFrom']) + ); + $this->assertEquals( + 10, + strlen((string)$this->apiContext->geRequestParams()['levelOne']['levelTwo']['dateFrom']) + ); + + $this->assertTrue( + str_contains( + $paramsValues->getStrings()[1], + $this->apiContext->geRequestParams()['tripId'] + ) + ); + $this->assertTrue( + str_contains( + $paramsValues->getStrings()[2], + strval($this->apiContext->geRequestParams()['dateTo']) + ) + ); + $this->assertTrue( + str_contains( + $paramsValues->getStrings()[5], + strval($this->apiContext->geRequestParams()['levelOne']['dateTo']) + ) + ); + $this->assertTrue( + str_contains( + $paramsValues->getStrings()[8], + strval($this->apiContext->geRequestParams()['levelOne']['levelTwo']['dateTo']) + ) + ); + } + + /** + * @param PyStringNode $paramsValues + * + * @dataProvider getTheRequestContainsParamsRuntimeException + */ + public function testTheRequestContainsParamsRuntimeException(PyStringNode $paramsValues): void + { + $this->expectException(RuntimeException::class); + $this->apiContext->theRequestContainsParams($paramsValues); + } + + public function getTheRequestContainsParamsSuccess(): array + { + return [ + [ + self::PARAMS_VALUES => new PyStringNode( + [ + '{', + ' "tripId": "26e185b9-a233-470e-b2d4-2818908a075f",', + ' "dateTo": 1680361181,', + ' "dateFrom": "<(new DateTimeImmutable())->getTimestamp()>",', + ' "levelOne": {', + ' "dateTo": 1680343281,', + ' "dateFrom": "<(new DateTimeImmutable())->getTimestamp()>",', + ' "levelTwo": {', + ' "dateTo": 1680343281,', + ' "dateFrom": "<(new DateTimeImmutable())->getTimestamp()>"', + ' }', + ' }', + '}', + ], + 12 + ), + self::INITIAL_PARAM_VALUE => '<(new DateTimeImmutable())->getTimestamp()>', + ], + ]; + } + + public function getTheRequestContainsParamsRuntimeException(): array + { + return [ + [ + self::PARAMS_VALUES => new PyStringNode( + [ + '{', + ' "tripId": "26e185b9-a233-470e-b2d4-2818908a075f",', + ' "dateTo": 1680361181,', + ' "dateFrom": "<(new DateTimeImutable())->getTimestamp()>"', + '}', + ], + 12 + ) + ], + [ + self::PARAMS_VALUES => new PyStringNode( + [ + '{', + ' "tripId": "26e185b9-a233-470e-b2d4-2818908a075f",', + ' "dateTo": 1680361181,', + ' "dateFrom": "<(DateTimeImmutable)->getTimestamp()>"', + '}', + ], + 12 + ) + ], + [ + self::PARAMS_VALUES => new PyStringNode( + [ + '{', + ' "tripId": "26e185b9-a233-470e-b2d4-2818908a075f",', + ' "levelOne": {', + ' "levelTwo": {', + ' "dateTo": 1680343281,', + ' "dateFrom": "<(ne DateTimeImmutable())->getTimestamp()>"', + ' }', + ' }', + '}', + ], + 12 + ), + ], + [ + self::PARAMS_VALUES => new PyStringNode( + [ + '{', + ' "tripId": "26e185b9-a233-470e-b2d4-2818908a075f",', + ' "dateTo": 1680361181,', + ' "dateFrom": "<>"', + '}', + ], + 12 + ) + ], + ]; + } +}