diff --git a/src/Codeception/Module/Symfony.php b/src/Codeception/Module/Symfony.php index 2f342598..60e27023 100644 --- a/src/Codeception/Module/Symfony.php +++ b/src/Codeception/Module/Symfony.php @@ -12,40 +12,33 @@ use Codeception\Lib\Framework; use Codeception\Lib\Interfaces\DoctrineProvider; use Codeception\Lib\Interfaces\PartedModule; +use Codeception\Module\Symfony\BrowserAssertionsTrait; +use Codeception\Module\Symfony\ConsoleAssertionsTrait; +use Codeception\Module\Symfony\DoctrineAssertionsTrait; +use Codeception\Module\Symfony\EventsAssertionsTrait; +use Codeception\Module\Symfony\FormAssertionsTrait; +use Codeception\Module\Symfony\MailerAssertionsTrait; +use Codeception\Module\Symfony\ParameterAssertionsTrait; +use Codeception\Module\Symfony\RouterAssertionsTrait; +use Codeception\Module\Symfony\SecurityAssertionsTrait; +use Codeception\Module\Symfony\ServicesAssertionsTrait; +use Codeception\Module\Symfony\SessionAssertionsTrait; use Codeception\TestInterface; use Exception; use ReflectionClass; use ReflectionException; -use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\SecurityBundle\DataCollector\SecurityDataCollector; -use Symfony\Component\BrowserKit\Cookie; -use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\Finder\Finder; -use Symfony\Component\Form\Extension\DataCollector\FormDataCollector; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface; -use Symfony\Component\HttpKernel\DataCollector\EventDataCollector; use Symfony\Component\HttpKernel\DataCollector\TimeDataCollector; use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\HttpKernel\Profiler\Profile; use Symfony\Component\HttpKernel\Profiler\Profiler; -use Symfony\Component\Routing\Exception\ResourceNotFoundException; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouterInterface; -use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; -use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; -use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter; -use Symfony\Component\Security\Core\Security; -use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken; use Symfony\Component\VarDumper\Cloner\Data; -use function array_intersect_assoc; -use function array_key_exists; use function array_keys; use function array_map; use function array_merge; @@ -54,27 +47,14 @@ use function class_exists; use function codecept_root_dir; use function count; -use function explode; use function file_exists; -use function get_class; use function implode; -use function in_array; use function ini_get; use function ini_set; -use function interface_exists; -use function is_array; -use function is_int; use function is_null; -use function is_object; use function is_string; -use function is_subclass_of; use function iterator_to_array; -use function json_encode; -use function serialize; use function sprintf; -use function strlen; -use function strpos; -use function substr_compare; /** * This module uses Symfony Crawler and HttpKernel to emulate requests and test response. @@ -160,6 +140,20 @@ */ class Symfony extends Framework implements DoctrineProvider, PartedModule { + use + BrowserAssertionsTrait, + ConsoleAssertionsTrait, + DoctrineAssertionsTrait, + EventsAssertionsTrait, + FormAssertionsTrait, + MailerAssertionsTrait, + ParameterAssertionsTrait, + RouterAssertionsTrait, + SecurityAssertionsTrait, + ServicesAssertionsTrait, + SessionAssertionsTrait + ; + private const SWIFTMAILER = 'swiftmailer'; private const SYMFONY_MAILER = 'symfony_mailer'; @@ -380,302 +374,6 @@ protected function getKernelClass(): string ); } - /** - * Get service $serviceName and add it to the lists of persistent services. - * - * @param string $serviceName - */ - public function persistService(string $serviceName): void - { - $service = $this->grabService($serviceName); - $this->persistentServices[$serviceName] = $service; - if ($this->client instanceof SymfonyConnector) { - $this->client->persistentServices[$serviceName] = $service; - } - } - - /** - * Get service $serviceName and add it to the lists of persistent services, - * making that service persistent between tests. - * - * @param string $serviceName - */ - public function persistPermanentService(string $serviceName): void - { - $service = $this->grabService($serviceName); - $this->persistentServices[$serviceName] = $service; - $this->permanentServices[$serviceName] = $service; - if ($this->client instanceof SymfonyConnector) { - $this->client->persistentServices[$serviceName] = $service; - } - } - - /** - * Remove service $serviceName from the lists of persistent services. - * - * @param string $serviceName - */ - 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]); - } - } - - /** - * Invalidate previously cached routes. - */ - public function invalidateCachedRouter(): void - { - $this->unpersistService('router'); - } - - /** - * Opens web page using route name and parameters. - * - * ``` php - * amOnRoute('posts.create'); - * $I->amOnRoute('posts.show', array('id' => 34)); - * ``` - * - * @param string $routeName - * @param array $params - */ - public function amOnRoute(string $routeName, array $params = []): void - { - /** @var RouterInterface $router */ - $router = $this->grabService('router'); - 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); - } - - /** - * Checks that current url matches route. - * - * ``` php - * seeCurrentRouteIs('posts.index'); - * $I->seeCurrentRouteIs('posts.show', array('id' => 8)); - * ``` - * - * @param string $routeName - * @param array $params - */ - public function seeCurrentRouteIs(string $routeName, array $params = []): void - { - /** @var RouterInterface $router */ - $router = $this->grabService('router'); - if ($router->getRouteCollection()->get($routeName) === null) { - $this->fail(sprintf('Route with name "%s" does not exists.', $routeName)); - } - - $uri = explode('?', $this->grabFromCurrentUrl())[0]; - 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); - - $this->assertEquals($expected, $intersection); - } - - /** - * Checks that current url matches route. - * Unlike seeCurrentRouteIs, this can matches without exact route parameters - * - * ``` php - * seeInCurrentRoute('my_blog_pages'); - * ``` - * - * @param string $routeName - */ - public function seeInCurrentRoute(string $routeName): void - { - /** @var RouterInterface $router */ - $router = $this->grabService('router'); - if ($router->getRouteCollection()->get($routeName) === null) { - $this->fail(sprintf('Route with name "%s" does not exists.', $routeName)); - } - - $uri = explode('?', $this->grabFromCurrentUrl())[0]; - try { - $matchedRouteName = $router->match($uri)['_route']; - } catch (ResourceNotFoundException $e) { - $this->fail(sprintf('The "%s" url does not match with any route', $uri)); - } - - $this->assertEquals($matchedRouteName, $routeName); - } - - /** - * Goes to a page and check that it can be accessed. - * - * ```php - * seePageIsAvailable('/dashboard'); - * ``` - * - * @param string $url - */ - public function seePageIsAvailable(string $url): void - { - $this->amOnPage($url); - $this->seeResponseCodeIsSuccessful(); - $this->seeInCurrentUrl($url); - } - - /** - * Goes to a page and check that it redirects to another. - * - * ```php - * seePageRedirectsTo('/admin', '/login'); - * ``` - * - * @param string $page - * @param string $redirectsTo - */ - public function seePageRedirectsTo(string $page, string $redirectsTo): void - { - $this->client->followRedirects(false); - $this->amOnPage($page); - /** @var Response $response */ - $response = $this->client->getResponse(); - $this->assertTrue( - $response->isRedirection() - ); - $this->client->followRedirect(); - $this->seeInCurrentUrl($redirectsTo); - } - - /** - * Checks if the desired number of emails was sent. - * If no argument is provided then at least one email must be sent to satisfy the check. - * The email is checked using Symfony's profiler, which means: - * * If your app performs a redirect after sending the email, you need to suppress this using REST Module's [stopFollowingRedirects](https://codeception.com/docs/modules/REST#stopFollowingRedirects) - * * If the email is sent by a Symfony Console Command, Codeception cannot detect it yet. - * - * ``` php - * seeEmailIsSent(2); - * ``` - * - * @param int|null $expectedCount - */ - public function seeEmailIsSent(?int $expectedCount = null): void - { - $realCount = 0; - $mailer = $this->config['mailer']; - if ($mailer === self::SWIFTMAILER) { - $mailCollector = $this->grabCollector('swiftmailer', __FUNCTION__); - $realCount = $mailCollector->getMessageCount(); - } elseif ($mailer === self::SYMFONY_MAILER) { - $mailCollector = $this->grabCollector('mailer', __FUNCTION__); - $realCount = count($mailCollector->getEvents()->getMessages()); - } else { - $this->fail( - "Emails can't be tested without Mailer service connector. - Set your mailer service in `functional.suite.yml`: `mailer: swiftmailer` - (Or `mailer: symfony_mailer` for Symfony Mailer)." - ); - } - - if ($expectedCount !== null) { - $this->assertEquals($expectedCount, $realCount, sprintf( - 'Expected number of sent emails was %d, but in reality %d %s sent.', - $expectedCount, $realCount, $realCount === 1 ? 'was' : 'were' - )); - return; - } - $this->assertGreaterThan(0, $realCount); - } - - /** - * Checks that no email was sent. This is an alias for seeEmailIsSent(0). - * - * @part email - */ - public function dontSeeEmailIsSent(): void - { - $this->seeEmailIsSent(0); - } - - /** - * Grabs a service from the Symfony dependency injection container (DIC). - * In "test" environment, Symfony uses a special `test.service_container`, see https://symfony.com/doc/current/testing.html#accessing-the-container - * Services that aren't injected somewhere into your app, need to be defined as `public` to be accessible by Codeception. - * - * ``` php - * grabService('doctrine'); - * ``` - * - * @param string $service - * @return mixed - * @part services - */ - public function grabService(string $service) - { - $container = $this->_getContainer(); - if (!$container->has($service)) { - $this->fail("Service $service is not available in container. - If the service isn't injected anywhere in your app, you need to set it to `public` in your `config/services_test.php`/`.yaml`, - see https://symfony.com/doc/current/testing.html#accessing-the-container"); - } - return $container->get($service); - } - - /** - * Run Symfony console command, grab response and return as string. - * Recommended to use for integration or functional testing. - * - * ``` php - * runSymfonyConsoleCommand('hello:world', ['arg' => 'argValue', 'opt1' => 'optValue'], ['input']); - * ``` - * - * @param string $command The console command to execute - * @param array $parameters Parameters (arguments and options) to pass to the command - * @param array $consoleInputs Console inputs (e.g. used for interactive questions) - * @param int $expectedExitCode The expected exit code of the command - * - * @return string Returns the console output of the command - */ - public function runSymfonyConsoleCommand(string $command, array $parameters = [], array $consoleInputs = [], int $expectedExitCode = 0): string - { - $kernel = $this->grabService('kernel'); - $application = new Application($kernel); - $consoleCommand = $application->find($command); - $commandTester = new CommandTester($consoleCommand); - $commandTester->setInputs($consoleInputs); - - $parameters = ['command' => $command] + $parameters; - $exitCode = $commandTester->execute($parameters); - $output = $commandTester->getDisplay(); - - $this->assertEquals( - $expectedExitCode, - $exitCode, - 'Command did not exit with code '.$expectedExitCode - .' but with '.$exitCode.': '.$output - ); - - return $output; - } - /** * @return Profile|null */ @@ -801,29 +499,6 @@ protected function getInternalDomains(): array return array_unique($internalDomains); } - /** - * Reboot client's kernel. - * Can be used to manually reboot kernel when 'rebootable_client' => false - * - * ``` php - * rebootClientKernel(); - * - * // Perform other requests - * - * ``` - * - */ - public function rebootClientKernel(): void - { - if ($this->client instanceof SymfonyConnector) { - $this->client->rebootKernel(); - } - } - /** * Returns list of the possible kernel classes based on the module configuration * @@ -845,785 +520,4 @@ private function getPossibleKernelClasses(): array return [$this->config['kernel_class']]; } - - /** - * Checks that number of given records were found in database. - * 'id' is the default search parameter. - * - * ```php - * seeNumRecords(1, User::class, ['name' => 'davert']); - * $I->seeNumRecords(80, User::class); - * ``` - * - * @param int $expectedNum Expected number of records - * @param string $className A doctrine entity - * @param array $criteria Optional query criteria - */ - public function seeNumRecords(int $expectedNum, string $className, array $criteria = []): void - { - $currentNum = $this->grabNumRecords($className, $criteria); - - $this->assertEquals( - $expectedNum, - $currentNum, - sprintf( - 'The number of found %s (%d) does not match expected number %d with %s', - $className, $currentNum, $expectedNum, json_encode($criteria) - ) - ); - } - - /** - * Retrieves number of records from database - * 'id' is the default search parameter. - * - * ```php - * grabNumRecords('User::class', ['name' => 'davert']); - * ``` - * - * @param string $entityClass The entity class - * @param array $criteria Optional query criteria - * @return int - */ - public function grabNumRecords(string $entityClass, array $criteria = []): int - { - $em = $this->_getEntityManager(); - $repository = $em->getRepository($entityClass); - - if (empty($criteria)) { - return (int)$repository->createQueryBuilder('a') - ->select('count(a.id)') - ->getQuery() - ->getSingleScalarResult(); - } - return $repository->count($criteria); - } - - /** - * Invalidate the current session. - * ```php - * logout(); - * ``` - */ - public function logout(): void - { - $container = $this->_getContainer(); - if ($container->has('security.token_storage')) { - /** @var TokenStorageInterface $tokenStorage */ - $tokenStorage = $this->grabService('security.token_storage'); - $tokenStorage->setToken(); - } - - /** @var SessionInterface $session */ - $session = $this->grabService('session'); - - $sessionName = $session->getName(); - $session->invalidate(); - - $cookieJar = $this->client->getCookieJar(); - foreach ($cookieJar->all() as $cookie) { - $cookieName = $cookie->getName(); - if ($cookieName === 'MOCKSESSID' || - $cookieName === 'REMEMBERME' || - $cookieName === $sessionName - ) { - $cookieJar->expire($cookieName); - } - } - $cookieJar->flushExpiredCookies(); - } - - /** - * Assert that the session has a given list of values. - * - * ``` php - * seeSessionHasValues(['key1', 'key2']); - * $I->seeSessionHasValues(['key1' => 'value1', 'key2' => 'value2']); - * ``` - * - * @param array $bindings - */ - public function seeSessionHasValues(array $bindings): void - { - foreach ($bindings as $key => $value) { - if (is_int($key)) { - $this->seeInSession($value); - } else { - $this->seeInSession($key, $value); - } - } - } - - /** - * Assert that a session attribute exists. - * - * ```php - * seeInSession('attribute'); - * $I->seeInSession('attribute', 'value'); - * ``` - * - * @param string $attribute - * @param mixed|null $value - */ - - public function seeInSession(string $attribute, $value = null): void - { - /** @var SessionInterface $session */ - $session = $this->grabService('session'); - - if (!$session->has($attribute)) { - $this->fail("No session attribute with name '$attribute'"); - } - - if (null !== $value) { - $this->assertEquals($value, $session->get($attribute)); - } - } - - /** - * Assert that a session attribute does not exist, or is not equal to the passed value. - * - * ```php - * dontSeeInSession('attribute'); - * $I->dontSeeInSession('attribute', 'value'); - * ``` - * - * @param string $attribute - * @param mixed|null $value - */ - public function dontSeeInSession(string $attribute, $value = null): void - { - /** @var SessionInterface $session */ - $session = $this->grabService('session'); - - if (null === $value) { - if ($session->has($attribute)) { - $this->fail("Session attribute with name '$attribute' does exist"); - } - } - else { - $this->assertNotEquals($value, $session->get($attribute)); - } - } - - /** - * Opens web page by action name - * - * ``` php - * amOnAction('PostController::index'); - * $I->amOnAction('HomeController'); - * $I->amOnAction('ArticleController', ['slug' => 'lorem-ipsum']); - * ``` - * - * @param string $action - * @param array $params - */ - public function amOnAction(string $action, array $params = []): void - { - /** @var RouterInterface $router */ - $router = $this->grabService('router'); - - $routes = $router->getRouteCollection()->getIterator(); - - foreach ($routes as $route) { - $controller = $route->getDefault('_controller'); - if (substr_compare($controller, $action, -strlen($action)) === 0) { - $resource = $router->match($route->getPath()); - $url = $router->generate( - $resource['_route'], - $params, - UrlGeneratorInterface::ABSOLUTE_PATH - ); - $this->amOnPage($url); - return; - } - } - } - - /** - * Checks that a user is authenticated. - * - * ```php - * seeAuthentication(); - * ``` - */ - public function seeAuthentication(): void - { - /** @var Security $security */ - $security = $this->grabService('security.helper'); - - $user = $security->getUser(); - - if ($user === null) { - $this->fail('There is no user in session'); - } - - $this->assertTrue( - $security->isGranted(AuthenticatedVoter::IS_AUTHENTICATED_FULLY), - 'There is no authenticated user' - ); - } - - /** - * Submit a form specifying the form name only once. - * - * Use this function instead of $I->submitForm() to avoid repeating the form name in the field selectors. - * If you customized the names of the field selectors use $I->submitForm() for full control. - * - * ```php - * submitSymfonyForm('login_form', [ - * '[email]' => 'john_doe@gmail.com', - * '[password]' => 'secretForest' - * ]); - * ``` - * - * @param string $name - * @param string[] $fields - */ - public function submitSymfonyForm(string $name, array $fields): void - { - $selector = sprintf('form[name=%s]', $name); - - $params = []; - foreach ($fields as $key => $value) { - $fixedKey = sprintf('%s%s', $name, $key); - $params[$fixedKey] = $value; - } - $button = sprintf('%s_submit', $name); - - $this->submitForm($selector, $params, $button); - } - - /** - * Checks that a user is authenticated with the 'remember me' option. - * - * ```php - * seeRememberedAuthentication(); - * ``` - */ - public function seeRememberedAuthentication(): void - { - /** @var Security $security */ - $security = $this->grabService('security.helper'); - - $user = $security->getUser(); - - if ($user === null) { - $this->fail('There is no user in session'); - } - - $hasRememberMeCookie = $this->client->getCookieJar()->get('REMEMBERME'); - $hasRememberMeRole = $security->isGranted(AuthenticatedVoter::IS_AUTHENTICATED_REMEMBERED); - - $isRemembered = $hasRememberMeCookie && $hasRememberMeRole; - $this->assertTrue( - $isRemembered, - 'User does not have remembered authentication' - ); - } - - /** - * Check that user is not authenticated with the 'remember me' option. - * - * ```php - * dontSeeRememberedAuthentication(); - * ``` - */ - public function dontSeeRememberedAuthentication(): void - { - /** @var Security $security */ - $security = $this->grabService('security.helper'); - - $hasRememberMeCookie = $this->client->getCookieJar()->get('REMEMBERME'); - $hasRememberMeRole = $security->isGranted(AuthenticatedVoter::IS_AUTHENTICATED_REMEMBERED); - - $isRemembered = $hasRememberMeCookie && $hasRememberMeRole; - $this->assertFalse( - $isRemembered, - 'User does have remembered authentication' - ); - } - - /** - * Verifies that the current user has multiple roles - * - * ``` php - * seeUserHasRoles(['ROLE_USER', 'ROLE_ADMIN']); - * ``` - * - * @param string[] $roles - */ - public function seeUserHasRoles(array $roles): void - { - foreach ($roles as $role) { - $this->seeUserHasRole($role); - } - } - - /** - * Check that the current user has a role - * - * ```php - * seeUserHasRole('ROLE_ADMIN'); - * ``` - * - * @param string $role - */ - public function seeUserHasRole(string $role): void - { - /** @var Security $security */ - $security = $this->grabService('security.helper'); - - $user = $security->getUser(); - - if ($user === null) { - $this->fail('There is no user in session'); - } - - $this->assertTrue( - $security->isGranted($role), - sprintf( - 'User %s has no role %s', - $user->getUsername(), - $role - ) - ); - } - - /** - * Check that user is not authenticated. - * - * ```php - * dontSeeAuthentication(); - * ``` - */ - public function dontSeeAuthentication(): void - { - /** @var Security $security */ - $security = $this->grabService('security.helper'); - - $this->assertFalse( - $security->isGranted(AuthenticatedVoter::IS_AUTHENTICATED_FULLY), - 'There is an user authenticated' - ); - } - - /** - * Grabs a Symfony parameter - * - * ```php - * grabParameter('app.business_name'); - * ``` - * - * @param string $name - * @return mixed|null - */ - public function grabParameter(string $name) - { - /** @var ParameterBagInterface $parameterBag */ - $parameterBag = $this->grabService('parameter_bag'); - return $parameterBag->get($name); - } - - /** - * Make sure events fired during the test. - * - * ``` php - * seeEventTriggered('App\MyEvent'); - * $I->seeEventTriggered(new App\Events\MyEvent()); - * $I->seeEventTriggered(['App\MyEvent', 'App\MyOtherEvent']); - * ``` - * @param string|object|string[] $expected - */ - public function seeEventTriggered($expected): void - { - /** @var EventDataCollector $eventCollector */ - $eventCollector = $this->grabCollector('events', __FUNCTION__); - - /** @var Data $data */ - $data = $eventCollector->getCalledListeners(); - - if ($data->count() === 0) { - $this->fail('No event was triggered'); - } - - $actual = $data->getValue(true); - $expected = is_array($expected) ? $expected : [$expected]; - - foreach ($expected as $expectedEvent) { - $triggered = false; - $expectedEvent = is_object($expectedEvent) ? get_class($expectedEvent) : $expectedEvent; - - foreach ($actual as $actualEvent) { - if (strpos($actualEvent['pretty'], $expectedEvent) === 0) { - $triggered = true; - } - } - $this->assertTrue($triggered, "The '$expectedEvent' event did not trigger"); - } - } - - /** - * Make sure events did not fire during the test. - * - * ``` php - * dontSeeEventTriggered('App\MyEvent'); - * $I->dontSeeEventTriggered(new App\Events\MyEvent()); - * $I->dontSeeEventTriggered(['App\MyEvent', 'App\MyOtherEvent']); - * ``` - * @param string|object|string[] $expected - */ - public function dontSeeEventTriggered($expected): void - { - /** @var EventDataCollector $eventCollector */ - $eventCollector = $this->grabCollector('events', __FUNCTION__); - - /** @var Data $data */ - $data = $eventCollector->getNotCalledListeners(); - - $actual = $data->getValue(true); - $expected = is_array($expected) ? $expected : [$expected]; - - foreach ($expected as $expectedEvent) { - $notTriggered = false; - $expectedEvent = is_object($expectedEvent) ? get_class($expectedEvent) : $expectedEvent; - - foreach ($actual as $actualEvent) { - if (strpos($actualEvent['pretty'], $expectedEvent) === 0) { - $notTriggered = true; - } - } - $this->assertTrue($notTriggered, "The '$expectedEvent' event triggered"); - } - } - - /** - * Checks that current page matches action - * - * ``` php - * seeCurrentActionIs('PostController::index'); - * $I->seeCurrentActionIs('HomeController'); - * ``` - * - * @param string $action - */ - public function seeCurrentActionIs(string $action): void - { - /** @var RouterInterface $router */ - $router = $this->grabService('router'); - - $routes = $router->getRouteCollection()->getIterator(); - - foreach ($routes as $route) { - $controller = $route->getDefault('_controller'); - if (substr_compare($controller, $action, -strlen($action)) === 0) { - /** @var Request $request */ - $request = $this->client->getRequest(); - $currentActionFqcn = $request->attributes->get('_controller'); - - $this->assertStringEndsWith($action, $currentActionFqcn, "Current action is '$currentActionFqcn'."); - return; - } - } - $this->fail("Action '$action' does not exist"); - } - - /** - * Verifies that multiple fields on a form have errors. - * - * If you only specify the name of the fields, this method will - * verify that the field contains at least one error of any type: - * - * ``` php - * seeFormErrorMessages(['telephone', 'address']); - * ``` - * - * If you want to specify the error messages, you can do so - * by sending an associative array instead, with the key being - * the name of the field and the error message the value. - * - * This method will validate that the expected error message - * is contained in the actual error message, that is, - * you can specify either the entire error message or just a part of it: - * - * ``` php - * seeFormErrorMessages([ - * 'address' => 'The address is too long' - * 'telephone' => 'too short', // the full error message is 'The telephone is too short' - * ]); - * ``` - * - * If you don't want to specify the error message for some fields, - * you can pass `null` as value instead of the message string, - * or you can directly omit the value of that field. If that is the case, - * it will be validated that that field has at least one error of any type: - * - * ``` php - * seeFormErrorMessages([ - * 'telephone' => 'too short', - * 'address' => null, - * 'postal code', - * ]); - * ``` - * - * @param string[] $expectedErrors - */ - public function seeFormErrorMessages(array $expectedErrors): void - { - foreach ($expectedErrors as $field => $message) { - if (is_int($field)) { - $this->seeFormErrorMessage($message); - } else { - $this->seeFormErrorMessage($field, $message); - } - } - } - - /** - * Verifies that a form field has an error. - * You can specify the expected error message as second parameter. - * - * ``` php - * seeFormErrorMessage('username'); - * $I->seeFormErrorMessage('username', 'Username is empty'); - * ``` - * @param string $field - * @param string|null $message - */ - public function seeFormErrorMessage(string $field, ?string $message = null): void - { - /** @var FormDataCollector $formCollector */ - $formCollector = $this->grabCollector('form', __FUNCTION__); - - if (!$forms = $formCollector->getData()->getValue('forms')['forms']) { - $this->fail('There are no forms on the current page.'); - } - - $fields = []; - $errors = []; - - foreach ($forms as $form) { - foreach ($form['children'] as $child) { - $fieldName = $child['name']; - $fields[] = $fieldName; - - if (!array_key_exists('errors', $child)) { - continue; - } - foreach ($child['errors'] as $error) { - $errors[$fieldName] = $error['message']; - } - } - } - - if (!in_array($field, $fields)) { - $this->fail("the field '$field' does not exist in the form."); - } - - if (!array_key_exists($field, $errors)) { - $this->fail("No form error message for field '$field'."); - } - - if (!$message) { - return; - } - - $this->assertStringContainsString( - $message, - $errors[$field], - sprintf( - "There is an error message for the field '%s', but it does not match the expected message.", - $field - ) - ); - } - - /** - * Checks that the user's password would not benefit from rehashing. - * If the user is not provided it is taken from the current session. - * - * You might use this function after performing tasks like registering a user or submitting a password update form. - * - * ```php - * seeUserPasswordDoesNotNeedRehash(); - * $I->seeUserPasswordDoesNotNeedRehash($user); - * ``` - * - * @param UserInterface|null $user - */ - public function seeUserPasswordDoesNotNeedRehash(UserInterface $user = null): void - { - if ($user === null) { - /** @var Security $security */ - $security = $this->grabService('security.helper'); - $user = $security->getUser(); - if ($user === null) { - $this->fail('No user found to validate'); - } - } - $encoder = $this->grabService('security.user_password_encoder.generic'); - - $this->assertFalse($encoder->needsRehash($user), 'User password needs rehash'); - } - - /** - * Verifies that there are no errors bound to the submitted form. - * - * ``` php - * dontSeeFormErrors(); - * ``` - */ - public function dontSeeFormErrors(): void - { - /** @var FormDataCollector $formCollector */ - $formCollector = $this->grabCollector('form', __FUNCTION__); - - $this->assertEquals( - 0, - $formCollector->getData()->offsetGet('nb_errors'), - 'Expecting that the form does not have errors, but there were!' - ); - } - - /** - * Login with the given user object. - * The `$user` object must have a persistent identifier. - * If you have more than one firewall or firewall context, you can specify the desired one as a parameter. - * - * ```php - * grabEntityFromRepository(User::class, [ - * 'email' => 'john_doe@gmail.com' - * ]); - * $I->amLoggedInAs($user); - * ``` - * - * @param UserInterface $user - * @param string $firewallName - * @param null $firewallContext - */ - public function amLoggedInAs(UserInterface $user, string $firewallName = 'main', $firewallContext = null): void - { - /** @var SessionInterface $session */ - $session = $this->grabService('session'); - - if ($this->config['guard']) { - $token = new PostAuthenticationGuardToken($user, $firewallName, $user->getRoles()); - } else { - $token = new UsernamePasswordToken($user, null, $firewallName, $user->getRoles()); - } - - if ($firewallContext) { - $session->set('_security_'.$firewallContext, serialize($token)); - } else { - $session->set('_security_'.$firewallName, serialize($token)); - } - - $session->save(); - - $cookie = new Cookie($session->getName(), $session->getId()); - $this->client->getCookieJar()->set($cookie); - } - - /** - * Verifies that there are one or more errors bound to the submitted form. - * - * ``` php - * seeFormHasErrors(); - * ``` - */ - public function seeFormHasErrors(): void - { - /** @var FormDataCollector $formCollector */ - $formCollector = $this->grabCollector('form', __FUNCTION__); - - $this->assertGreaterThan( - 0, - $formCollector->getData()->offsetGet('nb_errors'), - 'Expecting that the form has errors, but there were none!' - ); - } - - /** - * Grab a Doctrine entity repository. - * Works with objects, entities, repositories, and repository interfaces. - * - * ```php - * grabRepository($user); - * $I->grabRepository(User::class); - * $I->grabRepository(UserRepository::class); - * $I->grabRepository(UserRepositoryInterface::class); - * ``` - * - * @param object|string $mixed - * @return \Doctrine\ORM\EntityRepository|null - */ - public function grabRepository($mixed) - { - $entityRepoClass = '\Doctrine\ORM\EntityRepository'; - $isNotARepo = function () use ($mixed): void { - $this->fail( - sprintf("'%s' is not an entity repository", $mixed) - ); - }; - $getRepo = function () use ($mixed, $entityRepoClass, $isNotARepo) { - if (!$repo = $this->grabService($mixed)) return null; - if (!$repo instanceof $entityRepoClass) { - $isNotARepo(); - return null; - } - return $repo; - }; - - if (is_object($mixed)) { - $mixed = get_class($mixed); - } - - if (interface_exists($mixed)) { - return $getRepo(); - } - - if (!is_string($mixed) || !class_exists($mixed) ) { - $isNotARepo(); - return null; - } - - if (is_subclass_of($mixed, $entityRepoClass)){ - return $getRepo(); - } - - $em = $this->_getEntityManager(); - if ($em->getMetadataFactory()->isTransient($mixed)) { - $isNotARepo(); - return null; - } - - return $em->getRepository($mixed); - } } diff --git a/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php new file mode 100644 index 00000000..4f8dc566 --- /dev/null +++ b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php @@ -0,0 +1,107 @@ + false + * + * ``` php + * rebootClientKernel(); + * + * // Perform other requests + * + * ``` + * + */ + public function rebootClientKernel(): void + { + if ($this->client instanceof SymfonyConnector) { + $this->client->rebootKernel(); + } + } + + /** + * Goes to a page and check that it can be accessed. + * + * ```php + * seePageIsAvailable('/dashboard'); + * ``` + * + * @param string $url + */ + public function seePageIsAvailable(string $url): void + { + $this->amOnPage($url); + $this->seeResponseCodeIsSuccessful(); + $this->seeInCurrentUrl($url); + } + + /** + * Goes to a page and check that it redirects to another. + * + * ```php + * seePageRedirectsTo('/admin', '/login'); + * ``` + * + * @param string $page + * @param string $redirectsTo + */ + public function seePageRedirectsTo(string $page, string $redirectsTo): void + { + $this->client->followRedirects(false); + $this->amOnPage($page); + /** @var Response $response */ + $response = $this->client->getResponse(); + $this->assertTrue( + $response->isRedirection() + ); + $this->client->followRedirect(); + $this->seeInCurrentUrl($redirectsTo); + } + + /** + * Submit a form specifying the form name only once. + * + * Use this function instead of $I->submitForm() to avoid repeating the form name in the field selectors. + * If you customized the names of the field selectors use $I->submitForm() for full control. + * + * ```php + * submitSymfonyForm('login_form', [ + * '[email]' => 'john_doe@gmail.com', + * '[password]' => 'secretForest' + * ]); + * ``` + * + * @param string $name + * @param string[] $fields + */ + public function submitSymfonyForm(string $name, array $fields): void + { + $selector = sprintf('form[name=%s]', $name); + + $params = []; + foreach ($fields as $key => $value) { + $fixedKey = sprintf('%s%s', $name, $key); + $params[$fixedKey] = $value; + } + $button = sprintf('%s_submit', $name); + + $this->submitForm($selector, $params, $button); + } +} \ No newline at end of file diff --git a/src/Codeception/Module/Symfony/ConsoleAssertionsTrait.php b/src/Codeception/Module/Symfony/ConsoleAssertionsTrait.php new file mode 100644 index 00000000..85f62806 --- /dev/null +++ b/src/Codeception/Module/Symfony/ConsoleAssertionsTrait.php @@ -0,0 +1,49 @@ +runSymfonyConsoleCommand('hello:world', ['arg' => 'argValue', 'opt1' => 'optValue'], ['input']); + * ``` + * + * @param string $command The console command to execute + * @param array $parameters Parameters (arguments and options) to pass to the command + * @param array $consoleInputs Console inputs (e.g. used for interactive questions) + * @param int $expectedExitCode The expected exit code of the command + * + * @return string Returns the console output of the command + */ + public function runSymfonyConsoleCommand(string $command, array $parameters = [], array $consoleInputs = [], int $expectedExitCode = 0): string + { + $kernel = $this->grabService('kernel'); + $application = new Application($kernel); + $consoleCommand = $application->find($command); + $commandTester = new CommandTester($consoleCommand); + $commandTester->setInputs($consoleInputs); + + $parameters = ['command' => $command] + $parameters; + $exitCode = $commandTester->execute($parameters); + $output = $commandTester->getDisplay(); + + $this->assertEquals( + $expectedExitCode, + $exitCode, + 'Command did not exit with code '.$expectedExitCode + .' but with '.$exitCode.': '.$output + ); + + return $output; + } +} \ No newline at end of file diff --git a/src/Codeception/Module/Symfony/DoctrineAssertionsTrait.php b/src/Codeception/Module/Symfony/DoctrineAssertionsTrait.php new file mode 100644 index 00000000..db9295f8 --- /dev/null +++ b/src/Codeception/Module/Symfony/DoctrineAssertionsTrait.php @@ -0,0 +1,129 @@ +grabNumRecords('User::class', ['name' => 'davert']); + * ``` + * + * @param string $entityClass The entity class + * @param array $criteria Optional query criteria + * @return int + */ + public function grabNumRecords(string $entityClass, array $criteria = []): int + { + $em = $this->_getEntityManager(); + $repository = $em->getRepository($entityClass); + + if (empty($criteria)) { + return (int)$repository->createQueryBuilder('a') + ->select('count(a.id)') + ->getQuery() + ->getSingleScalarResult(); + } + return $repository->count($criteria); + } + + /** + * Grab a Doctrine entity repository. + * Works with objects, entities, repositories, and repository interfaces. + * + * ```php + * grabRepository($user); + * $I->grabRepository(User::class); + * $I->grabRepository(UserRepository::class); + * $I->grabRepository(UserRepositoryInterface::class); + * ``` + * + * @param object|string $mixed + * @return \Doctrine\ORM\EntityRepository|null + */ + public function grabRepository($mixed) + { + $entityRepoClass = '\Doctrine\ORM\EntityRepository'; + $isNotARepo = function () use ($mixed): void { + $this->fail( + sprintf("'%s' is not an entity repository", $mixed) + ); + }; + $getRepo = function () use ($mixed, $entityRepoClass, $isNotARepo) { + if (!$repo = $this->grabService($mixed)) return null; + if (!$repo instanceof $entityRepoClass) { + $isNotARepo(); + return null; + } + return $repo; + }; + + if (is_object($mixed)) { + $mixed = get_class($mixed); + } + + if (interface_exists($mixed)) { + return $getRepo(); + } + + if (!is_string($mixed) || !class_exists($mixed) ) { + $isNotARepo(); + return null; + } + + if (is_subclass_of($mixed, $entityRepoClass)){ + return $getRepo(); + } + + $em = $this->_getEntityManager(); + if ($em->getMetadataFactory()->isTransient($mixed)) { + $isNotARepo(); + return null; + } + + return $em->getRepository($mixed); + } + + /** + * Checks that number of given records were found in database. + * 'id' is the default search parameter. + * + * ```php + * seeNumRecords(1, User::class, ['name' => 'davert']); + * $I->seeNumRecords(80, User::class); + * ``` + * + * @param int $expectedNum Expected number of records + * @param string $className A doctrine entity + * @param array $criteria Optional query criteria + */ + public function seeNumRecords(int $expectedNum, string $className, array $criteria = []): void + { + $currentNum = $this->grabNumRecords($className, $criteria); + + $this->assertEquals( + $expectedNum, + $currentNum, + sprintf( + 'The number of found %s (%d) does not match expected number %d with %s', + $className, $currentNum, $expectedNum, json_encode($criteria) + ) + ); + } +} \ No newline at end of file diff --git a/src/Codeception/Module/Symfony/EventsAssertionsTrait.php b/src/Codeception/Module/Symfony/EventsAssertionsTrait.php new file mode 100644 index 00000000..8193a068 --- /dev/null +++ b/src/Codeception/Module/Symfony/EventsAssertionsTrait.php @@ -0,0 +1,89 @@ +dontSeeEventTriggered('App\MyEvent'); + * $I->dontSeeEventTriggered(new App\Events\MyEvent()); + * $I->dontSeeEventTriggered(['App\MyEvent', 'App\MyOtherEvent']); + * ``` + * @param string|object|string[] $expected + */ + public function dontSeeEventTriggered($expected): void + { + /** @var EventDataCollector $eventCollector */ + $eventCollector = $this->grabCollector('events', __FUNCTION__); + + /** @var Data $data */ + $data = $eventCollector->getNotCalledListeners(); + + $actual = $data->getValue(true); + $expected = is_array($expected) ? $expected : [$expected]; + + foreach ($expected as $expectedEvent) { + $notTriggered = false; + $expectedEvent = is_object($expectedEvent) ? get_class($expectedEvent) : $expectedEvent; + + foreach ($actual as $actualEvent) { + if (strpos($actualEvent['pretty'], $expectedEvent) === 0) { + $notTriggered = true; + } + } + $this->assertTrue($notTriggered, "The '$expectedEvent' event triggered"); + } + } + + /** + * Make sure events fired during the test. + * + * ``` php + * seeEventTriggered('App\MyEvent'); + * $I->seeEventTriggered(new App\Events\MyEvent()); + * $I->seeEventTriggered(['App\MyEvent', 'App\MyOtherEvent']); + * ``` + * @param string|object|string[] $expected + */ + public function seeEventTriggered($expected): void + { + /** @var EventDataCollector $eventCollector */ + $eventCollector = $this->grabCollector('events', __FUNCTION__); + + /** @var Data $data */ + $data = $eventCollector->getCalledListeners(); + + if ($data->count() === 0) { + $this->fail('No event was triggered'); + } + + $actual = $data->getValue(true); + $expected = is_array($expected) ? $expected : [$expected]; + + foreach ($expected as $expectedEvent) { + $triggered = false; + $expectedEvent = is_object($expectedEvent) ? get_class($expectedEvent) : $expectedEvent; + + foreach ($actual as $actualEvent) { + if (strpos($actualEvent['pretty'], $expectedEvent) === 0) { + $triggered = true; + } + } + $this->assertTrue($triggered, "The '$expectedEvent' event did not trigger"); + } + } +} \ No newline at end of file diff --git a/src/Codeception/Module/Symfony/FormAssertionsTrait.php b/src/Codeception/Module/Symfony/FormAssertionsTrait.php new file mode 100644 index 00000000..c9e1544f --- /dev/null +++ b/src/Codeception/Module/Symfony/FormAssertionsTrait.php @@ -0,0 +1,168 @@ +dontSeeFormErrors(); + * ``` + */ + public function dontSeeFormErrors(): void + { + /** @var FormDataCollector $formCollector */ + $formCollector = $this->grabCollector('form', __FUNCTION__); + + $this->assertEquals( + 0, + $formCollector->getData()->offsetGet('nb_errors'), + 'Expecting that the form does not have errors, but there were!' + ); + } + + /** + * Verifies that a form field has an error. + * You can specify the expected error message as second parameter. + * + * ``` php + * seeFormErrorMessage('username'); + * $I->seeFormErrorMessage('username', 'Username is empty'); + * ``` + * @param string $field + * @param string|null $message + */ + public function seeFormErrorMessage(string $field, ?string $message = null): void + { + /** @var FormDataCollector $formCollector */ + $formCollector = $this->grabCollector('form', __FUNCTION__); + + if (!$forms = $formCollector->getData()->getValue('forms')['forms']) { + $this->fail('There are no forms on the current page.'); + } + + $fields = []; + $errors = []; + + foreach ($forms as $form) { + foreach ($form['children'] as $child) { + $fieldName = $child['name']; + $fields[] = $fieldName; + + if (!array_key_exists('errors', $child)) { + continue; + } + foreach ($child['errors'] as $error) { + $errors[$fieldName] = $error['message']; + } + } + } + + if (!in_array($field, $fields)) { + $this->fail("the field '$field' does not exist in the form."); + } + + if (!array_key_exists($field, $errors)) { + $this->fail("No form error message for field '$field'."); + } + + if (!$message) { + return; + } + + $this->assertStringContainsString( + $message, + $errors[$field], + sprintf( + "There is an error message for the field '%s', but it does not match the expected message.", + $field + ) + ); + } + + /** + * Verifies that multiple fields on a form have errors. + * + * If you only specify the name of the fields, this method will + * verify that the field contains at least one error of any type: + * + * ``` php + * seeFormErrorMessages(['telephone', 'address']); + * ``` + * + * If you want to specify the error messages, you can do so + * by sending an associative array instead, with the key being + * the name of the field and the error message the value. + * + * This method will validate that the expected error message + * is contained in the actual error message, that is, + * you can specify either the entire error message or just a part of it: + * + * ``` php + * seeFormErrorMessages([ + * 'address' => 'The address is too long' + * 'telephone' => 'too short', // the full error message is 'The telephone is too short' + * ]); + * ``` + * + * If you don't want to specify the error message for some fields, + * you can pass `null` as value instead of the message string, + * or you can directly omit the value of that field. If that is the case, + * it will be validated that that field has at least one error of any type: + * + * ``` php + * seeFormErrorMessages([ + * 'telephone' => 'too short', + * 'address' => null, + * 'postal code', + * ]); + * ``` + * + * @param string[] $expectedErrors + */ + public function seeFormErrorMessages(array $expectedErrors): void + { + foreach ($expectedErrors as $field => $message) { + if (is_int($field)) { + $this->seeFormErrorMessage($message); + } else { + $this->seeFormErrorMessage($field, $message); + } + } + } + + /** + * Verifies that there are one or more errors bound to the submitted form. + * + * ``` php + * seeFormHasErrors(); + * ``` + */ + public function seeFormHasErrors(): void + { + /** @var FormDataCollector $formCollector */ + $formCollector = $this->grabCollector('form', __FUNCTION__); + + $this->assertGreaterThan( + 0, + $formCollector->getData()->offsetGet('nb_errors'), + 'Expecting that the form has errors, but there were none!' + ); + } +} \ No newline at end of file diff --git a/src/Codeception/Module/Symfony/MailerAssertionsTrait.php b/src/Codeception/Module/Symfony/MailerAssertionsTrait.php new file mode 100644 index 00000000..2ff971b5 --- /dev/null +++ b/src/Codeception/Module/Symfony/MailerAssertionsTrait.php @@ -0,0 +1,62 @@ +seeEmailIsSent(0); + } + + /** + * Checks if the desired number of emails was sent. + * If no argument is provided then at least one email must be sent to satisfy the check. + * The email is checked using Symfony's profiler, which means: + * * If your app performs a redirect after sending the email, you need to suppress this using REST Module's [stopFollowingRedirects](https://codeception.com/docs/modules/REST#stopFollowingRedirects) + * * If the email is sent by a Symfony Console Command, Codeception cannot detect it yet. + * + * ``` php + * seeEmailIsSent(2); + * ``` + * + * @param int|null $expectedCount + */ + public function seeEmailIsSent(?int $expectedCount = null): void + { + $realCount = 0; + $mailer = $this->config['mailer']; + if ($mailer === self::SWIFTMAILER) { + $mailCollector = $this->grabCollector('swiftmailer', __FUNCTION__); + $realCount = $mailCollector->getMessageCount(); + } elseif ($mailer === self::SYMFONY_MAILER) { + $mailCollector = $this->grabCollector('mailer', __FUNCTION__); + $realCount = count($mailCollector->getEvents()->getMessages()); + } else { + $this->fail( + "Emails can't be tested without Mailer service connector. + Set your mailer service in `functional.suite.yml`: `mailer: swiftmailer` + (Or `mailer: symfony_mailer` for Symfony Mailer)." + ); + } + + if ($expectedCount !== null) { + $this->assertEquals($expectedCount, $realCount, sprintf( + 'Expected number of sent emails was %d, but in reality %d %s sent.', + $expectedCount, $realCount, $realCount === 1 ? 'was' : 'were' + )); + return; + } + $this->assertGreaterThan(0, $realCount); + } +} \ No newline at end of file diff --git a/src/Codeception/Module/Symfony/ParameterAssertionsTrait.php b/src/Codeception/Module/Symfony/ParameterAssertionsTrait.php new file mode 100644 index 00000000..85193959 --- /dev/null +++ b/src/Codeception/Module/Symfony/ParameterAssertionsTrait.php @@ -0,0 +1,28 @@ +grabParameter('app.business_name'); + * ``` + * + * @param string $name + * @return mixed|null + */ + public function grabParameter(string $name) + { + /** @var ParameterBagInterface $parameterBag */ + $parameterBag = $this->grabService('parameter_bag'); + return $parameterBag->get($name); + } +} \ No newline at end of file diff --git a/src/Codeception/Module/Symfony/RouterAssertionsTrait.php b/src/Codeception/Module/Symfony/RouterAssertionsTrait.php new file mode 100644 index 00000000..b84c551c --- /dev/null +++ b/src/Codeception/Module/Symfony/RouterAssertionsTrait.php @@ -0,0 +1,178 @@ +amOnAction('PostController::index'); + * $I->amOnAction('HomeController'); + * $I->amOnAction('ArticleController', ['slug' => 'lorem-ipsum']); + * ``` + * + * @param string $action + * @param array $params + */ + public function amOnAction(string $action, array $params = []): void + { + /** @var RouterInterface $router */ + $router = $this->grabService('router'); + + $routes = $router->getRouteCollection()->getIterator(); + + foreach ($routes as $route) { + $controller = $route->getDefault('_controller'); + if (substr_compare($controller, $action, -strlen($action)) === 0) { + $resource = $router->match($route->getPath()); + $url = $router->generate( + $resource['_route'], + $params, + UrlGeneratorInterface::ABSOLUTE_PATH + ); + $this->amOnPage($url); + return; + } + } + } + + /** + * Opens web page using route name and parameters. + * + * ``` php + * amOnRoute('posts.create'); + * $I->amOnRoute('posts.show', array('id' => 34)); + * ``` + * + * @param string $routeName + * @param array $params + */ + public function amOnRoute(string $routeName, array $params = []): void + { + /** @var RouterInterface $router */ + $router = $this->grabService('router'); + 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); + } + + /** + * Invalidate previously cached routes. + */ + public function invalidateCachedRouter(): void + { + $this->unpersistService('router'); + } + + /** + * Checks that current page matches action + * + * ``` php + * seeCurrentActionIs('PostController::index'); + * $I->seeCurrentActionIs('HomeController'); + * ``` + * + * @param string $action + */ + public function seeCurrentActionIs(string $action): void + { + /** @var RouterInterface $router */ + $router = $this->grabService('router'); + + $routes = $router->getRouteCollection()->getIterator(); + + foreach ($routes as $route) { + $controller = $route->getDefault('_controller'); + if (substr_compare($controller, $action, -strlen($action)) === 0) { + /** @var Request $request */ + $request = $this->client->getRequest(); + $currentActionFqcn = $request->attributes->get('_controller'); + + $this->assertStringEndsWith($action, $currentActionFqcn, "Current action is '$currentActionFqcn'."); + return; + } + } + $this->fail("Action '$action' does not exist"); + } + + /** + * Checks that current url matches route. + * + * ``` php + * seeCurrentRouteIs('posts.index'); + * $I->seeCurrentRouteIs('posts.show', array('id' => 8)); + * ``` + * + * @param string $routeName + * @param array $params + */ + public function seeCurrentRouteIs(string $routeName, array $params = []): void + { + /** @var RouterInterface $router */ + $router = $this->grabService('router'); + if ($router->getRouteCollection()->get($routeName) === null) { + $this->fail(sprintf('Route with name "%s" does not exists.', $routeName)); + } + + $uri = explode('?', $this->grabFromCurrentUrl())[0]; + 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); + + $this->assertEquals($expected, $intersection); + } + + /** + * Checks that current url matches route. + * Unlike seeCurrentRouteIs, this can matches without exact route parameters + * + * ``` php + * seeInCurrentRoute('my_blog_pages'); + * ``` + * + * @param string $routeName + */ + public function seeInCurrentRoute(string $routeName): void + { + /** @var RouterInterface $router */ + $router = $this->grabService('router'); + if ($router->getRouteCollection()->get($routeName) === null) { + $this->fail(sprintf('Route with name "%s" does not exists.', $routeName)); + } + + $uri = explode('?', $this->grabFromCurrentUrl())[0]; + try { + $matchedRouteName = $router->match($uri)['_route']; + } catch (ResourceNotFoundException $e) { + $this->fail(sprintf('The "%s" url does not match with any route', $uri)); + } + + $this->assertEquals($matchedRouteName, $routeName); + } +} \ No newline at end of file diff --git a/src/Codeception/Module/Symfony/SecurityAssertionsTrait.php b/src/Codeception/Module/Symfony/SecurityAssertionsTrait.php new file mode 100644 index 00000000..1bac156e --- /dev/null +++ b/src/Codeception/Module/Symfony/SecurityAssertionsTrait.php @@ -0,0 +1,186 @@ +dontSeeAuthentication(); + * ``` + */ + public function dontSeeAuthentication(): void + { + /** @var Security $security */ + $security = $this->grabService('security.helper'); + + $this->assertFalse( + $security->isGranted(AuthenticatedVoter::IS_AUTHENTICATED_FULLY), + 'There is an user authenticated' + ); + } + + /** + * Check that user is not authenticated with the 'remember me' option. + * + * ```php + * dontSeeRememberedAuthentication(); + * ``` + */ + public function dontSeeRememberedAuthentication(): void + { + /** @var Security $security */ + $security = $this->grabService('security.helper'); + + $hasRememberMeCookie = $this->client->getCookieJar()->get('REMEMBERME'); + $hasRememberMeRole = $security->isGranted(AuthenticatedVoter::IS_AUTHENTICATED_REMEMBERED); + + $isRemembered = $hasRememberMeCookie && $hasRememberMeRole; + $this->assertFalse( + $isRemembered, + 'User does have remembered authentication' + ); + } + + /** + * Checks that a user is authenticated. + * + * ```php + * seeAuthentication(); + * ``` + */ + public function seeAuthentication(): void + { + /** @var Security $security */ + $security = $this->grabService('security.helper'); + + $user = $security->getUser(); + + if ($user === null) { + $this->fail('There is no user in session'); + } + + $this->assertTrue( + $security->isGranted(AuthenticatedVoter::IS_AUTHENTICATED_FULLY), + 'There is no authenticated user' + ); + } + + /** + * Checks that a user is authenticated with the 'remember me' option. + * + * ```php + * seeRememberedAuthentication(); + * ``` + */ + public function seeRememberedAuthentication(): void + { + /** @var Security $security */ + $security = $this->grabService('security.helper'); + + $user = $security->getUser(); + + if ($user === null) { + $this->fail('There is no user in session'); + } + + $hasRememberMeCookie = $this->client->getCookieJar()->get('REMEMBERME'); + $hasRememberMeRole = $security->isGranted(AuthenticatedVoter::IS_AUTHENTICATED_REMEMBERED); + + $isRemembered = $hasRememberMeCookie && $hasRememberMeRole; + $this->assertTrue( + $isRemembered, + 'User does not have remembered authentication' + ); + } + + /** + * Check that the current user has a role + * + * ```php + * seeUserHasRole('ROLE_ADMIN'); + * ``` + * + * @param string $role + */ + public function seeUserHasRole(string $role): void + { + /** @var Security $security */ + $security = $this->grabService('security.helper'); + + $user = $security->getUser(); + + if ($user === null) { + $this->fail('There is no user in session'); + } + + $this->assertTrue( + $security->isGranted($role), + sprintf( + 'User %s has no role %s', + $user->getUsername(), + $role + ) + ); + } + + /** + * Verifies that the current user has multiple roles + * + * ``` php + * seeUserHasRoles(['ROLE_USER', 'ROLE_ADMIN']); + * ``` + * + * @param string[] $roles + */ + public function seeUserHasRoles(array $roles): void + { + foreach ($roles as $role) { + $this->seeUserHasRole($role); + } + } + + /** + * Checks that the user's password would not benefit from rehashing. + * If the user is not provided it is taken from the current session. + * + * You might use this function after performing tasks like registering a user or submitting a password update form. + * + * ```php + * seeUserPasswordDoesNotNeedRehash(); + * $I->seeUserPasswordDoesNotNeedRehash($user); + * ``` + * + * @param UserInterface|null $user + */ + public function seeUserPasswordDoesNotNeedRehash(UserInterface $user = null): void + { + if ($user === null) { + /** @var Security $security */ + $security = $this->grabService('security.helper'); + $user = $security->getUser(); + if ($user === null) { + $this->fail('No user found to validate'); + } + } + $encoder = $this->grabService('security.user_password_encoder.generic'); + + $this->assertFalse($encoder->needsRehash($user), 'User password needs rehash'); + } +} \ No newline at end of file diff --git a/src/Codeception/Module/Symfony/ServicesAssertionsTrait.php b/src/Codeception/Module/Symfony/ServicesAssertionsTrait.php new file mode 100644 index 00000000..5b0e4df6 --- /dev/null +++ b/src/Codeception/Module/Symfony/ServicesAssertionsTrait.php @@ -0,0 +1,83 @@ +grabService('doctrine'); + * ``` + * + * @param string $service + * @return mixed + * @part services + */ + public function grabService(string $service) + { + $container = $this->_getContainer(); + if (!$container->has($service)) { + $this->fail("Service $service is not available in container. + If the service isn't injected anywhere in your app, you need to set it to `public` in your `config/services_test.php`/`.yaml`, + see https://symfony.com/doc/current/testing.html#accessing-the-container"); + } + return $container->get($service); + } + + /** + * Get service $serviceName and add it to the lists of persistent services. + * + * @param string $serviceName + */ + public function persistService(string $serviceName): void + { + $service = $this->grabService($serviceName); + $this->persistentServices[$serviceName] = $service; + if ($this->client instanceof SymfonyConnector) { + $this->client->persistentServices[$serviceName] = $service; + } + } + + /** + * Get service $serviceName and add it to the lists of persistent services, + * making that service persistent between tests. + * + * @param string $serviceName + */ + public function persistPermanentService(string $serviceName): void + { + $service = $this->grabService($serviceName); + $this->persistentServices[$serviceName] = $service; + $this->permanentServices[$serviceName] = $service; + if ($this->client instanceof SymfonyConnector) { + $this->client->persistentServices[$serviceName] = $service; + } + } + + /** + * Remove service $serviceName from the lists of persistent services. + * + * @param string $serviceName + */ + 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]); + } + } +} \ No newline at end of file diff --git a/src/Codeception/Module/Symfony/SessionAssertionsTrait.php b/src/Codeception/Module/Symfony/SessionAssertionsTrait.php new file mode 100644 index 00000000..b6296d49 --- /dev/null +++ b/src/Codeception/Module/Symfony/SessionAssertionsTrait.php @@ -0,0 +1,167 @@ +grabEntityFromRepository(User::class, [ + * 'email' => 'john_doe@gmail.com' + * ]); + * $I->amLoggedInAs($user); + * ``` + * + * @param UserInterface $user + * @param string $firewallName + * @param null $firewallContext + */ + public function amLoggedInAs(UserInterface $user, string $firewallName = 'main', $firewallContext = null): void + { + /** @var SessionInterface $session */ + $session = $this->grabService('session'); + + if ($this->config['guard']) { + $token = new PostAuthenticationGuardToken($user, $firewallName, $user->getRoles()); + } else { + $token = new UsernamePasswordToken($user, null, $firewallName, $user->getRoles()); + } + + if ($firewallContext) { + $session->set('_security_'.$firewallContext, serialize($token)); + } else { + $session->set('_security_'.$firewallName, serialize($token)); + } + + $session->save(); + + $cookie = new Cookie($session->getName(), $session->getId()); + $this->client->getCookieJar()->set($cookie); + } + + /** + * Assert that a session attribute does not exist, or is not equal to the passed value. + * + * ```php + * dontSeeInSession('attribute'); + * $I->dontSeeInSession('attribute', 'value'); + * ``` + * + * @param string $attribute + * @param mixed|null $value + */ + public function dontSeeInSession(string $attribute, $value = null): void + { + /** @var SessionInterface $session */ + $session = $this->grabService('session'); + + if (null === $value) { + if ($session->has($attribute)) { + $this->fail("Session attribute with name '$attribute' does exist"); + } + } + else { + $this->assertNotEquals($value, $session->get($attribute)); + } + } + + /** + * Invalidate the current session. + * ```php + * logout(); + * ``` + */ + public function logout(): void + { + $container = $this->_getContainer(); + if ($container->has('security.token_storage')) { + /** @var TokenStorageInterface $tokenStorage */ + $tokenStorage = $this->grabService('security.token_storage'); + $tokenStorage->setToken(); + } + + /** @var SessionInterface $session */ + $session = $this->grabService('session'); + + $sessionName = $session->getName(); + $session->invalidate(); + + $cookieJar = $this->client->getCookieJar(); + foreach ($cookieJar->all() as $cookie) { + $cookieName = $cookie->getName(); + if ($cookieName === 'MOCKSESSID' || + $cookieName === 'REMEMBERME' || + $cookieName === $sessionName + ) { + $cookieJar->expire($cookieName); + } + } + $cookieJar->flushExpiredCookies(); + } + + /** + * Assert that a session attribute exists. + * + * ```php + * seeInSession('attribute'); + * $I->seeInSession('attribute', 'value'); + * ``` + * + * @param string $attribute + * @param mixed|null $value + */ + public function seeInSession(string $attribute, $value = null): void + { + /** @var SessionInterface $session */ + $session = $this->grabService('session'); + + if (!$session->has($attribute)) { + $this->fail("No session attribute with name '$attribute'"); + } + + if (null !== $value) { + $this->assertEquals($value, $session->get($attribute)); + } + } + + /** + * Assert that the session has a given list of values. + * + * ``` php + * seeSessionHasValues(['key1', 'key2']); + * $I->seeSessionHasValues(['key1' => 'value1', 'key2' => 'value2']); + * ``` + * + * @param array $bindings + */ + public function seeSessionHasValues(array $bindings): void + { + foreach ($bindings as $key => $value) { + if (is_int($key)) { + $this->seeInSession($value); + } else { + $this->seeInSession($key, $value); + } + } + } +} \ No newline at end of file pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy