Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ jobs:
- php: '8.1'
symfony-versions: '^5.4'
coverage: 'none'
- description: 'Symfony 8 + Behat 4.x-dev'
php: '8.4'
symfony-versions: '8.0.*'
coverage: 'none'
- description: 'Log Code Coverage'
php: '8.4'
coverage: 'xdebug'
Expand Down Expand Up @@ -60,6 +64,10 @@ jobs:
key: ${{ runner.os }}-${{ matrix.php }}-${{ matrix.symfony-versions }}-composer-${{ hashFiles('composer.json') }}
restore-keys: ${{ runner.os }}-${{ matrix.php }}-${{ matrix.symfony-versions }}-composer

- name: Use Behat 4.x-dev with Symfony 8
if: matrix.symfony-versions == '8.0.*'
run: composer require behat/behat:4.x-dev --no-update --no-scripts

- name: Update Symfony version
if: matrix.symfony-versions != ''
run: |
Expand All @@ -86,4 +94,22 @@ jobs:
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: './coverage.xml'
fail_ci_if_error: true
fail_ci_if_error: false

prefer-lowest:
name: PHPUnit (composer --prefer-lowest)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'

- name: Install lowest compatible dependencies
run: composer update --prefer-lowest --no-interaction --no-progress

- name: Run PHPUnit tests
run: composer phpunit
19 changes: 19 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.PHONY: cs-fix rector phpunit phpstan phpcs dev-checks

cs-fix:
composer run code-style-fix

rector:
@if [ -x vendor/bin/rector ]; then vendor/bin/rector --dry-run; else echo "rector not installed; skipping"; fi

phpunit:
composer run phpunit

phpstan:
composer run phpstan

phpcs:
composer run code-style

dev-checks:
composer run dev-checks
23 changes: 13 additions & 10 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,26 @@
}
],
"license": "MIT",
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"ext-json": "*",
"php": "^8.1",
"behat/behat": "^3.0",
"symfony/config": "^5.4 || ^6.4 || ^7.0",
"symfony/dependency-injection": "^5.4.34 || ^6.4 || ^7.1",
"symfony/http-client": "^5.4 || ^6.4 || ^7.1",
"symfony/http-kernel": "^5.4 || ^6.4 || ^7.1",
"symfony/routing": "^5.4 || ^6.4 || ^7.1",
"behat/behat": "^3.0 || 4.x-dev@dev",
"symfony/config": "^5.4 || ^6.4 || ^7.0 || ^8.0",
"symfony/dependency-injection": "^5.4.34 || ^6.4 || ^7.1 || ^8.0",
"symfony/http-client": "^5.4 || ^6.4 || ^7.1 || ^8.0",
"symfony/http-kernel": "^5.4 || ^6.4 || ^7.1 || ^8.0",
"symfony/routing": "^5.4 || ^6.4 || ^7.1 || ^8.0",
"macpaw/similar-arrays": "^1.0"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.0",
"slevomat/coding-standard": "^7.0",
"squizlabs/php_codesniffer": "^3.6",
"doctrine/orm": "^2.0"
"phpunit/phpunit": "^10.5 || ^11.0 || ^12.0",
"slevomat/coding-standard": "^8.0",
"squizlabs/php_codesniffer": "^4.0",
"doctrine/orm": "^2.0 || ^3.0",
"dealerdirect/phpcodesniffer-composer-installer": "^1.0"
},
"autoload": {
"psr-4": {
Expand Down
14 changes: 13 additions & 1 deletion phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,19 @@

<rule ref="Generic.PHP.ForbiddenFunctions">
<properties>
<property name="forbiddenFunctions" type="array" value="eval=>NULL,dd=>NULL,die=>NULL,var_dump=>NULL,dump=>NULL,sizeof=>count,delete=>unset,print=>echo,echo=>NULL,print_r=>NULL,create_function=>NULL"/>
<property name="forbiddenFunctions" type="array">
<element key="eval" value="null"/>
<element key="dd" value="null"/>
<element key="die" value="null"/>
<element key="var_dump" value="null"/>
<element key="dump" value="null"/>
<element key="sizeof" value="count"/>
<element key="delete" value="unset"/>
<element key="print" value="echo"/>
<element key="echo" value="null"/>
<element key="print_r" value="null"/>
<element key="create_function" value="null"/>
</property>
</properties>
</rule>
<rule ref="Squiz.WhiteSpace.FunctionSpacing">
Expand Down
13 changes: 2 additions & 11 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,5 @@ parameters:
- src
level: max
checkExplicitMixed: false
ignoreErrors:
-
message: '#Parameter \#1 \$json of function json_decode expects string, string\|false given.*#'
count: 3
path: ./src/Context/ApiContext.php
-
message: '#Parameter \#3 \$actualJSON of method BehatApiContext\\Context\\ApiContext::compareStructureResponse\(\) expects string, string\|false given.*#'
count: 1
path: ./src/Context/ApiContext.php
-
identifier: missingType.iterableValue
# Aligns Symfony Config generics with runtime checks (e.g. ArrayNodeDefinition) across Symfony versions.
treatPhpDocTypesAsCertain: false
13 changes: 6 additions & 7 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"
backupGlobals="false"
backupStaticAttributes="false"
backupStaticProperties="false"
colors="true"
verbose="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
Comment thread
alekseytupichenkov marked this conversation as resolved.
processIsolation="false"
stopOnFailure="false">
stopOnFailure="false"
failOnPhpunitWarning="false">
<testsuites>
<testsuite name="Behat Api Context Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<coverage>
<source>
<include>
<directory>./src</directory>
</include>
</source>
<coverage>
<report>
<clover outputFile="clover.xml"/>
</report>
Expand Down
31 changes: 25 additions & 6 deletions src/Context/ApiContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -246,10 +246,11 @@ public function responseStatusCodeShouldBe(string $httpStatus): void
public function responseIsJson(): void
{
$response = $this->getResponse();
$data = json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR);
$body = $this->getResponseBody($response);
$data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);

if (empty($data)) {
throw new RuntimeException("Response was not JSON\n" . $response->getContent());
throw new RuntimeException("Response was not JSON\n" . $body);
}
}

Expand All @@ -258,7 +259,7 @@ public function responseIsJson(): void
*/
public function responseEmpty(): void
{
if (!empty($this->getResponse()->getContent())) {
if ($this->getResponseBody($this->getResponse()) !== '') {
throw new RuntimeException('Content not empty');
}
}
Expand All @@ -271,7 +272,7 @@ public function responseEmpty(): void
public function responseShouldBeJson(PyStringNode $string): void
{
$expectedResponse = json_decode(trim($string->getRaw()), true, 512, JSON_THROW_ON_ERROR);
$actualResponse = json_decode($this->getResponse()->getContent(), true, 512, JSON_THROW_ON_ERROR);
$actualResponse = json_decode($this->getResponseBody($this->getResponse()), true, 512, JSON_THROW_ON_ERROR);

if ($expectedResponse !== $actualResponse) {
$prettyJSON = json_encode($actualResponse, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT, 512);
Expand All @@ -286,7 +287,7 @@ public function responseShouldBeJson(PyStringNode $string): void
*/
public function iGetParamFromJsonResponse(string $paramPath, string $valueKey): void
{
$actualResponse = json_decode($this->getResponse()->getContent(), true, 512, JSON_THROW_ON_ERROR);
$actualResponse = json_decode($this->getResponseBody($this->getResponse()), true, 512, JSON_THROW_ON_ERROR);
$pathKeys = explode('.', $paramPath);

foreach ($pathKeys as $key) {
Expand All @@ -307,7 +308,7 @@ public function iGetParamFromJsonResponse(string $paramPath, string $valueKey):
*/
public function responseShouldBeJsonWithVariableFields(string $variableFields, PyStringNode $string): void
{
$this->compareStructureResponse($variableFields, $string, $this->getResponse()->getContent());
$this->compareStructureResponse($variableFields, $string, $this->getResponseBody($this->getResponse()));
}

protected function compareStructureResponse(
Expand Down Expand Up @@ -388,6 +389,11 @@ protected function checkResponseHeader(string $headerName, string $headerValue):
}
}

/**
* @param array<string, mixed> $requestParams
*
* @return array<string, mixed>
*/
protected function convertRunnableCodeParams(array $requestParams): array
{
foreach ($requestParams as $key => $value) {
Expand Down Expand Up @@ -449,8 +455,21 @@ protected function getResponse(): Response
return $this->response;
}

/**
* @return array<string, mixed>
*/
public function geRequestParams(): array
{
return $this->requestParams;
}

private function getResponseBody(Response $response): string
{
$content = $response->getContent();
if ($content === false) {
throw new RuntimeException('The response body is not available.');
}

return $content;
}
}
13 changes: 8 additions & 5 deletions src/DependencyInjection/BehatApiContextExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;

class BehatApiContextExtension extends Extension
{
/**
* @param array<string, mixed> $configs
*/
public function load(array $configs, ContainerBuilder $container): void
{
$configuration = new Configuration();
/** @var array<string, mixed> $config */
$config = $this->processConfiguration($configuration, $configs);

$loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
$loader = new PhpFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));

$this->loadApiContext($config, $loader, $container);
}
Expand All @@ -28,10 +31,10 @@ public function load(array $configs, ContainerBuilder $container): void
*/
private function loadApiContext(
array $config,
XmlFileLoader $loader,
PhpFileLoader $loader,
ContainerBuilder $container
): void {
$this->safeLoad($loader, 'api_context.xml');
$this->safeLoad($loader, 'api_context.php');

$this->configureKernelResetManagers(
$config,
Expand Down Expand Up @@ -62,7 +65,7 @@ private function configureKernelResetManagers(
}
}

private function safeLoad(XmlFileLoader $loader, string $file): void
private function safeLoad(PhpFileLoader $loader, string $file): void
{
$loader->load($file);
}
Expand Down
21 changes: 15 additions & 6 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace BehatApiContext\DependencyInjection;

use LogicException;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\NodeBuilder;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
Expand All @@ -13,7 +15,15 @@ class Configuration implements ConfigurationInterface
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('behat_api_context');
$root = $treeBuilder->getRootNode()->children();
$rootNode = $treeBuilder->getRootNode();
// Symfony TreeBuilder root is always ArrayNodeDefinition; kept for static analysis / defensive parity.
// @codeCoverageIgnoreStart
if (!$rootNode instanceof ArrayNodeDefinition) {
throw new LogicException('Expected configuration root to be an array node.');
}
// @codeCoverageIgnoreEnd

$root = $rootNode->children();

$this->addKernelResetManagersSection($root);

Expand All @@ -22,10 +32,9 @@ public function getConfigTreeBuilder(): TreeBuilder

private function addKernelResetManagersSection(NodeBuilder $builder): void
{
$builder
->arrayNode('kernel_reset_managers')
->scalarPrototype()->end()
->end()
->end();
$kernelResetManagers = $builder->arrayNode('kernel_reset_managers');
$kernelResetManagers->scalarPrototype()->end();
$kernelResetManagers->end();
$builder->end();
Comment thread
alekseytupichenkov marked this conversation as resolved.
}
}
20 changes: 20 additions & 0 deletions src/Resources/config/api_context.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

use BehatApiContext\Context\ApiContext;
use BehatApiContext\Service\ResetManager\DoctrineResetManager;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $container): void {
Comment thread
alekseytupichenkov marked this conversation as resolved.
$services = $container->services();

$services->set(ApiContext::class)
->public()
->autowire()
->autoconfigure();

$services->set(DoctrineResetManager::class)
->autowire(false)
->autoconfigure(false);
};
10 changes: 0 additions & 10 deletions src/Resources/config/api_context.xml

This file was deleted.

26 changes: 26 additions & 0 deletions tests/Unit/Context/Api/ApiContextResponseBodyTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace BehatApiContext\Tests\Unit\Context\Api;

use BehatApiContext\Context\ApiContext;
use ReflectionMethod;
use RuntimeException;
use Symfony\Component\HttpFoundation\Response;

final class ApiContextResponseBodyTest extends ApiContextTestCase
{
public function testGetResponseBodyThrowsWhenResponseContentNotAvailable(): void
Comment thread
alekseytupichenkov marked this conversation as resolved.
{
$response = $this->createMock(Response::class);
$response->method('getContent')->willReturn(false);

$method = new ReflectionMethod(ApiContext::class, 'getResponseBody');
$method->setAccessible(true);

$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('The response body is not available.');
$method->invoke($this->apiContext, $response);
}
}
Loading
Loading