diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b4fc29ef..2bab7b32 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: - php: [7.3, 7.4, 8.0] + php: [7.4, 8.0, 8.1] symfony: [4.4, 5.3] steps: @@ -59,7 +59,10 @@ jobs: composer require symfony/css-selector=${{ matrix.symfony }} --ignore-platform-req=php --no-update composer require symfony/dom-crawler=${{ matrix.symfony }} --ignore-platform-req=php --no-update composer require symfony/browser-kit=${{ matrix.symfony }} --ignore-platform-req=php --no-update - composer install --prefer-dist --no-progress --ignore-platform-req=php + composer require vlucas/phpdotenv --ignore-platform-req=php --no-update + composer require codeception/module-asserts --ignore-platform-req=php --no-update + composer require codeception/module-doctrine2 --ignore-platform-req=php --no-update + composer install --prefer-dist --no-progress --ignore-platform-req=php --no-dev - name: Validate composer.json and composer.lock run: composer validate diff --git a/composer.json b/composer.json index 168f4e76..82aeb1d5 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ ], "minimum-stability": "RC", "require": { - "php": "^7.3 | ^8.0", + "php": "^7.4 | ^8.0", "ext-json": "*", "codeception/lib-innerbrowser": "^1.4", "codeception/codeception": "^4.0" @@ -24,6 +24,14 @@ "require-dev": { "codeception/module-asserts": "^1.3", "codeception/module-doctrine2": "^1.1", + "doctrine/orm": "^2.10", + "symfony/form": "^4.4 | ^5.0", + "symfony/framework-bundle": "^4.4 | ^5.0", + "symfony/http-kernel": "^4.4 | ^5.0", + "symfony/mailer": "^4.4 | ^5.0", + "symfony/routing": "^4.4 | ^5.0", + "symfony/security-bundle": "^4.4 | ^5.0", + "symfony/twig-bundle": "^4.4 | ^5.0", "vlucas/phpdotenv": "^4.2 | ^5.3" }, "suggest": { diff --git a/src/Codeception/Lib/Connector/Symfony.php b/src/Codeception/Lib/Connector/Symfony.php index 174b1607..c71f1e60 100644 --- a/src/Codeception/Lib/Connector/Symfony.php +++ b/src/Codeception/Lib/Connector/Symfony.php @@ -17,25 +17,13 @@ class Symfony extends HttpKernelBrowser { - /** - * @var bool - */ - private $rebootable; + private bool $rebootable; - /** - * @var bool - */ - private $hasPerformedRequest = false; + private bool $hasPerformedRequest = false; - /** - * @var ContainerInterface - */ - private $container; + private ?ContainerInterface $container; - /** - * @var array - */ - public $persistentServices = []; + public array $persistentServices = []; /** * Constructor. @@ -67,6 +55,7 @@ protected function doRequest($request): Response $this->hasPerformedRequest = true; } } + return parent::doRequest($request); } @@ -113,6 +102,7 @@ private function getContainer(): ?ContainerInterface if ($container->has('test.service_container')) { $container = $container->get('test.service_container'); } + return $container; } @@ -123,6 +113,7 @@ private function getProfiler(): ?Profiler $profiler = $this->container->get('profiler'); return $profiler; } + return null; } @@ -131,10 +122,11 @@ private function getService(string $serviceName): ?object if ($this->container->has($serviceName)) { return $this->container->get($serviceName); } + return null; } - private function persistDoctrineConnections() + private function persistDoctrineConnections(): void { if (!$this->container->hasParameter('doctrine.connections')) { return; diff --git a/src/Codeception/Module/Symfony.php b/src/Codeception/Module/Symfony.php index 745cf16a..78ecd898 100644 --- a/src/Codeception/Module/Symfony.php +++ b/src/Codeception/Module/Symfony.php @@ -143,16 +143,16 @@ class Symfony extends Framework implements DoctrineProvider, PartedModule use TimeAssertionsTrait; use TwigAssertionsTrait; - /** - * @var Kernel - */ - public $kernel; + public Kernel $kernel; /** * @var SymfonyConnector */ public $client; + /** + * @var array + */ public $config = [ 'app_path' => 'app', 'kernel_class' => 'App\Kernel', @@ -172,10 +172,7 @@ public function _parts(): array return ['services']; } - /** - * @var string|null - */ - protected $kernelClass; + protected ?string $kernelClass = null; /** * Services that should be persistent permanently for all tests @@ -229,6 +226,7 @@ public function _after(TestInterface $test): void foreach (array_keys($this->permanentServices) as $serviceName) { $this->permanentServices[$serviceName] = $this->grabService($serviceName); } + parent::_after($test); } @@ -250,6 +248,7 @@ public function _getEntityManager() if ($this->kernel === null) { $this->fail('Symfony module is not loaded'); } + $emService = $this->config['em_service']; if (!isset($this->permanentServices[$emService])) { // Try to persist configured entity manager @@ -258,13 +257,16 @@ public function _getEntityManager() if ($container->has('doctrine')) { $this->persistPermanentService('doctrine'); } + if ($container->has('doctrine.orm.default_entity_manager')) { $this->persistPermanentService('doctrine.orm.default_entity_manager'); } + if ($container->has('doctrine.dbal.default_connection')) { $this->persistPermanentService('doctrine.dbal.default_connection'); } } + return $this->permanentServices[$emService]; } @@ -277,9 +279,11 @@ public function _getContainer(): ContainerInterface if (!$container instanceof ContainerInterface) { $this->fail('Could not get Symfony container'); } + if ($container->has('test.service_container')) { $container = $container->get('test.service_container'); } + return $container; } @@ -348,6 +352,7 @@ protected function getProfile(): ?Profile if (!$profiler = $this->getService('profiler')) { return null; } + try { $response = $this->getClient()->getResponse(); return $profiler->loadProfileFromResponse($response); @@ -356,6 +361,7 @@ protected function getProfile(): ?Profile } catch (Exception $e) { $this->fail($e->getMessage()); } + return null; } @@ -379,6 +385,7 @@ protected function grabCollector(string $collector, string $function, ?string $m if ($message) { $this->fail($message); } + $this->fail( sprintf("The '%s' collector is needed to use the '%s' function.", $collector, $function) ); @@ -419,12 +426,14 @@ protected function debugResponse($url): void $this->debugSection('User', 'Anonymous'); } } + if ($profile->hasCollector('mailer')) { /** @var MessageDataCollector $mailerCollector */ $mailerCollector = $profile->getCollector('mailer'); $emails = count($mailerCollector->getEvents()->getMessages()); $this->debugSection('Emails', $emails . ' sent'); } + if ($profile->hasCollector('time')) { /** @var TimeDataCollector $timeCollector */ $timeCollector = $profile->getCollector('time'); diff --git a/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php index f0c53237..cabd34f2 100644 --- a/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php @@ -50,6 +50,7 @@ public function seePageIsAvailable(string $url = null): void $this->amOnPage($url); $this->seeInCurrentUrl($url); } + $this->assertThat($this->getClient()->getResponse(), new ResponseIsSuccessful()); } @@ -102,6 +103,7 @@ public function submitSymfonyForm(string $name, array $fields): void $fixedKey = sprintf('%s%s', $name, $key); $params[$fixedKey] = $value; } + $button = sprintf('%s_submit', $name); $this->submitForm($selector, $params, $button); diff --git a/src/Codeception/Module/Symfony/DoctrineAssertionsTrait.php b/src/Codeception/Module/Symfony/DoctrineAssertionsTrait.php index c67653ff..45b770de 100644 --- a/src/Codeception/Module/Symfony/DoctrineAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/DoctrineAssertionsTrait.php @@ -4,6 +4,7 @@ namespace Codeception\Module\Symfony; +use Doctrine\ORM\EntityRepository; use function class_exists; use function get_class; use function interface_exists; @@ -39,6 +40,7 @@ public function grabNumRecords(string $entityClass, array $criteria = []): int ->getQuery() ->getSingleScalarResult(); } + return $repository->count($criteria); } @@ -59,18 +61,20 @@ public function grabNumRecords(string $entityClass, array $criteria = []): int */ public function grabRepository($mixed) { - $entityRepoClass = '\Doctrine\ORM\EntityRepository'; + $entityRepoClass = EntityRepository::class; $isNotARepo = function () use ($mixed): void { $this->fail( sprintf("'%s' is not an entity repository", $mixed) ); }; - $getRepo = function () use ($mixed, $entityRepoClass, $isNotARepo) { + $getRepo = function () use ($mixed, $entityRepoClass, $isNotARepo): ?EntityRepository { if (!$repo = $this->grabService($mixed)) return null; + if (!$repo instanceof $entityRepoClass) { $isNotARepo(); return null; } + return $repo; }; @@ -123,7 +127,7 @@ public function seeNumRecords(int $expectedNum, string $className, array $criter $currentNum, sprintf( 'The number of found %s (%d) does not match expected number %d with %s', - $className, $currentNum, $expectedNum, json_encode($criteria) + $className, $currentNum, $expectedNum, json_encode($criteria, JSON_THROW_ON_ERROR) ) ); } diff --git a/src/Codeception/Module/Symfony/FormAssertionsTrait.php b/src/Codeception/Module/Symfony/FormAssertionsTrait.php index 4a14af3c..919d18a7 100644 --- a/src/Codeception/Module/Symfony/FormAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/FormAssertionsTrait.php @@ -65,6 +65,7 @@ public function seeFormErrorMessage(string $field, ?string $message = null): voi if (!array_key_exists('errors', $child)) { continue; } + foreach ($child['errors'] as $error) { $errors[$fieldName] = $error['message']; } diff --git a/src/Codeception/Module/Symfony/MailerAssertionsTrait.php b/src/Codeception/Module/Symfony/MailerAssertionsTrait.php index 670d2b69..9ead87ff 100644 --- a/src/Codeception/Module/Symfony/MailerAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/MailerAssertionsTrait.php @@ -63,6 +63,7 @@ public function grabLastSentEmail(): ?Email if ($lastEmail = end($emails)) { return $lastEmail; } + return null; } diff --git a/src/Codeception/Module/Symfony/RouterAssertionsTrait.php b/src/Codeception/Module/Symfony/RouterAssertionsTrait.php index 043d4b57..e477b82c 100644 --- a/src/Codeception/Module/Symfony/RouterAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/RouterAssertionsTrait.php @@ -68,6 +68,7 @@ public function amOnRoute(string $routeName, array $params = []): void if ($router->getRouteCollection()->get($routeName) === null) { $this->fail(sprintf('Route with name "%s" does not exists.', $routeName)); } + $url = $router->generate($routeName, $params); $this->amOnPage($url); } @@ -107,6 +108,7 @@ public function seeCurrentActionIs(string $action): void return; } } + $this->fail("Action '{$action}' does not exist"); } @@ -130,11 +132,13 @@ public function seeCurrentRouteIs(string $routeName, array $params = []): void } $uri = explode('?', $this->grabFromCurrentUrl())[0]; + $match = []; try { $match = $router->match($uri); } catch (ResourceNotFoundException $e) { $this->fail(sprintf('The "%s" url does not match with any route', $uri)); } + $expected = array_merge(['_route' => $routeName], $params); $intersection = array_intersect_assoc($expected, $match); @@ -160,6 +164,7 @@ public function seeInCurrentRoute(string $routeName): void } $uri = explode('?', $this->grabFromCurrentUrl())[0]; + $matchedRouteName = ''; try { $matchedRouteName = (string) $router->match($uri)['_route']; } catch (ResourceNotFoundException $e) { diff --git a/src/Codeception/Module/Symfony/SecurityAssertionsTrait.php b/src/Codeception/Module/Symfony/SecurityAssertionsTrait.php index 674c3153..a8ad24b6 100644 --- a/src/Codeception/Module/Symfony/SecurityAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/SecurityAssertionsTrait.php @@ -172,6 +172,7 @@ public function seeUserPasswordDoesNotNeedRehash(UserInterface $user = null): vo $this->fail('No user found to validate'); } } + $hasher = $this->grabPasswordHasherService(); $this->assertFalse($hasher->needsRehash($user), 'User password needs rehash'); diff --git a/src/Codeception/Module/Symfony/ServicesAssertionsTrait.php b/src/Codeception/Module/Symfony/ServicesAssertionsTrait.php index 1fe2124d..c21289c5 100644 --- a/src/Codeception/Module/Symfony/ServicesAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/ServicesAssertionsTrait.php @@ -29,6 +29,7 @@ public function grabService(string $serviceId): object $this->fail("Service `{$serviceId}` is required by Codeception, but not loaded by Symfony since you're not using it anywhere in your app.\n Recommended solution: Set it to `public` in your `config/services_test.php`/`.yaml`, see https://symfony.com/doc/current/service_container/alias_private.html#marking-services-as-public-private"); } + return $service; } @@ -75,9 +76,11 @@ public function unpersistService(string $serviceName): void if (isset($this->persistentServices[$serviceName])) { unset($this->persistentServices[$serviceName]); } + if (isset($this->permanentServices[$serviceName])) { unset($this->permanentServices[$serviceName]); } + if ($this->client instanceof SymfonyConnector && isset($this->client->persistentServices[$serviceName])) { unset($this->client->persistentServices[$serviceName]); } @@ -89,6 +92,7 @@ protected function getService(string $serviceId): ?object if ($container->has($serviceId)) { return $container->get($serviceId); } + return null; } } diff --git a/src/Codeception/Module/Symfony/SessionAssertionsTrait.php b/src/Codeception/Module/Symfony/SessionAssertionsTrait.php index ee4bc56e..8fdadbb8 100644 --- a/src/Codeception/Module/Symfony/SessionAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/SessionAssertionsTrait.php @@ -137,6 +137,7 @@ public function logoutProgrammatically(): void $cookieJar->expire($cookieName); } } + $cookieJar->flushExpiredCookies(); }