From 70e5e06b0de5a4e7520e303ea6bac096e1f62ea9 Mon Sep 17 00:00:00 2001 From: Aaron Gustavo Nieves <64917965+TavoNiievez@users.noreply.github.com> Date: Sun, 23 Nov 2025 10:11:04 -0500 Subject: [PATCH 1/3] Simplify the module code (#222) --- composer.json | 1 + src/Codeception/Module/Symfony.php | 79 +++++++------------ .../Module/Symfony/BrowserAssertionsTrait.php | 3 +- .../Module/Symfony/MailerAssertionsTrait.php | 13 ++- .../Symfony/NotifierAssertionsTrait.php | 13 ++- 5 files changed, 42 insertions(+), 67 deletions(-) diff --git a/composer.json b/composer.json index 03ca498..810b0b3 100644 --- a/composer.json +++ b/composer.json @@ -33,6 +33,7 @@ "doctrine/orm": "^3.5", "friendsofphp/php-cs-fixer": "^3.85", "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^10.0", "symfony/browser-kit": "^5.4 | ^6.4 | ^7.3", "symfony/cache": "^5.4 | ^6.4 | ^7.3", "symfony/config": "^5.4 | ^6.4 | ^7.3", diff --git a/src/Codeception/Module/Symfony.php b/src/Codeception/Module/Symfony.php index ff11a36..2c87f52 100644 --- a/src/Codeception/Module/Symfony.php +++ b/src/Codeception/Module/Symfony.php @@ -334,6 +334,14 @@ protected function getClient(): SymfonyConnector */ protected function getKernelClass(): string { + /** @var class-string $kernelClass */ + $kernelClass = $this->config['kernel_class']; + $this->requireAdditionalAutoloader(); + + if (class_exists($kernelClass)) { + return $kernelClass; + } + /** @var string $rootDir */ $rootDir = codecept_root_dir(); $path = $rootDir . $this->config['app_path']; @@ -346,40 +354,21 @@ protected function getKernelClass(): string ); } - $this->requireAdditionalAutoloader(); - - $finder = new Finder(); - $results = iterator_to_array($finder->name('*Kernel.php')->depth('0')->in($path)); - - if ($results === []) { - throw new ModuleRequireException( - self::class, - "File with Kernel class was not found at {$path}.\n" . - 'Specify directory where file with Kernel class for your application is located with `app_path` parameter.' - ); - } - - $kernelClass = $this->config['kernel_class']; - $filesRealPath = []; + $finder = new Finder(); + $finder->name('*Kernel.php')->depth('0')->in($path); - foreach ($results as $file) { + foreach ($finder as $file) { include_once $file->getRealPath(); - $filesRealPath[] = $file->getRealPath(); } - if (class_exists($kernelClass)) { - $ref = new ReflectionClass($kernelClass); - $fileName = $ref->getFileName(); - if ($fileName !== false && in_array($fileName, $filesRealPath, true)) { - /** @var class-string $kernelClass */ - return $kernelClass; - } + if (class_exists($kernelClass, false)) { + return $kernelClass; } throw new ModuleRequireException( self::class, - "Kernel class was not found.\n" . - 'Specify directory where file with Kernel class for your application is located with `kernel_class` parameter.' + "Kernel class was not found at {$path}.\n" . + 'Specify directory where file with Kernel class for your application is located with `app_path` parameter.' ); } @@ -455,31 +444,19 @@ protected function debugResponse(mixed $url): void return; } - if ($profile->hasCollector(DataCollectorName::SECURITY->value)) { - $securityCollector = $profile->getCollector(DataCollectorName::SECURITY->value); - if ($securityCollector instanceof SecurityDataCollector) { - $this->debugSecurityData($securityCollector); - } - } - - if ($profile->hasCollector(DataCollectorName::MAILER->value)) { - $mailerCollector = $profile->getCollector(DataCollectorName::MAILER->value); - if ($mailerCollector instanceof MessageDataCollector) { - $this->debugMailerData($mailerCollector); - } - } - - if ($profile->hasCollector(DataCollectorName::NOTIFIER->value)) { - $notifierCollector = $profile->getCollector(DataCollectorName::NOTIFIER->value); - if ($notifierCollector instanceof NotificationDataCollector) { - $this->debugNotifierData($notifierCollector); - } - } - - if ($profile->hasCollector(DataCollectorName::TIME->value)) { - $timeCollector = $profile->getCollector(DataCollectorName::TIME->value); - if ($timeCollector instanceof TimeDataCollector) { - $this->debugTimeData($timeCollector); + $collectors = [ + DataCollectorName::SECURITY->value => [$this->debugSecurityData(...), SecurityDataCollector::class], + DataCollectorName::MAILER->value => [$this->debugMailerData(...), MessageDataCollector::class], + DataCollectorName::NOTIFIER->value => [$this->debugNotifierData(...), NotificationDataCollector::class], + DataCollectorName::TIME->value => [$this->debugTimeData(...), TimeDataCollector::class], + ]; + + foreach ($collectors as $name => [$callback, $expectedClass]) { + if ($profile->hasCollector($name)) { + $collector = $profile->getCollector($name); + if ($collector instanceof $expectedClass) { + $callback($collector); + } } } } diff --git a/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php index 8bd940b..d04b69e 100644 --- a/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php @@ -359,8 +359,7 @@ public function submitSymfonyForm(string $name, array $fields): void $params = []; foreach ($fields as $key => $value) { - $fixedKey = sprintf('%s%s', $name, $key); - $params[$fixedKey] = $value; + $params[$name . $key] = $value; } $button = sprintf('%s_submit', $name); diff --git a/src/Codeception/Module/Symfony/MailerAssertionsTrait.php b/src/Codeception/Module/Symfony/MailerAssertionsTrait.php index 649bfcd..8da45f5 100644 --- a/src/Codeception/Module/Symfony/MailerAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/MailerAssertionsTrait.php @@ -164,13 +164,12 @@ public function getMailerEvent(int $index = 0, ?string $transport = null): ?Mess protected function getMessageMailerEvents(): MessageEvents { - $mailer = $this->getService('mailer.message_logger_listener'); - if ($mailer instanceof MessageLoggerListener) { - return $mailer->getEvents(); - } - $mailer = $this->getService('mailer.logger_message_listener'); - if ($mailer instanceof MessageLoggerListener) { - return $mailer->getEvents(); + $services = ['mailer.message_logger_listener', 'mailer.logger_message_listener']; + foreach ($services as $serviceId) { + $mailer = $this->getService($serviceId); + if ($mailer instanceof MessageLoggerListener) { + return $mailer->getEvents(); + } } Assert::fail("Emails can't be tested without Symfony Mailer service."); } diff --git a/src/Codeception/Module/Symfony/NotifierAssertionsTrait.php b/src/Codeception/Module/Symfony/NotifierAssertionsTrait.php index 77422c7..765d8d0 100644 --- a/src/Codeception/Module/Symfony/NotifierAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/NotifierAssertionsTrait.php @@ -251,13 +251,12 @@ protected function getNotificationEvents(): NotificationEvents Assert::fail('Notifier assertions require Symfony 6.2 or higher.'); } - $notifier = $this->getService('notifier.notification_logger_listener'); - if ($notifier instanceof NotificationLoggerListener) { - return $notifier->getEvents(); - } - $notifier = $this->getService('notifier.logger_notification_listener'); - if ($notifier instanceof NotificationLoggerListener) { - return $notifier->getEvents(); + $services = ['notifier.notification_logger_listener', 'notifier.logger_notification_listener']; + foreach ($services as $serviceId) { + $notifier = $this->getService($serviceId); + if ($notifier instanceof NotificationLoggerListener) { + return $notifier->getEvents(); + } } Assert::fail("Notifications can't be tested without Symfony Notifier service."); } From 79ac0ff72f1d9a5a90917de1b08b4b4cda053d7c Mon Sep 17 00:00:00 2001 From: Aaron Gustavo Nieves <64917965+TavoNiievez@users.noreply.github.com> Date: Thu, 4 Sep 2025 15:27:35 -0500 Subject: [PATCH 2/3] test: cover session login and add time, translation, twig, validator asserts # Conflicts: # composer.json --- .gitignore | 7 +- codeception.yml | 7 + composer.json | 15 +- readme.md | 4 + src/Codeception/Lib/Connector/Symfony.php | 6 +- .../Module/Symfony/BrowserAssertionsTrait.php | 19 +- .../Module/Symfony/RouterAssertionsTrait.php | 5 +- .../Module/Symfony/SessionAssertionsTrait.php | 2 +- tests/BrowserAssertionsTest.php | 69 ++++++ tests/BrowserKitConnectorTest.php | 26 +++ tests/ConsoleAssertionsTest.php | 38 ++++ tests/DomCrawlerAssertionsTest.php | 52 +++++ tests/FormAssertionsTest.php | 43 ++++ tests/Functional.suite.yml | 8 + tests/Functional/BrowserCest.php | 39 ++++ tests/Functional/ConsoleCest.php | 13 ++ tests/Functional/DomCrawlerCest.php | 24 +++ tests/Functional/ExampleCest.php | 14 ++ tests/Functional/FormCest.php | 15 ++ tests/Functional/LoggerCest.php | 13 ++ tests/Functional/MailerCest.php | 37 ++++ tests/Functional/MimeCest.php | 26 +++ tests/Functional/ParameterCest.php | 12 ++ tests/Functional/RouterCest.php | 19 ++ tests/Functional/SecurityCest.php | 23 ++ tests/Functional/ServicesCest.php | 15 ++ tests/Functional/SessionCest.php | 47 +++++ tests/Functional/TimeCest.php | 13 ++ tests/Functional/TranslationCest.php | 20 ++ tests/Functional/TwigCest.php | 16 ++ tests/Functional/ValidatorCest.php | 23 ++ tests/LoggerAssertionsTest.php | 64 ++++++ tests/MailerAssertionsTest.php | 84 ++++++++ tests/MimeAssertionsTest.php | 71 +++++++ tests/ParameterAssertionsTest.php | 46 ++++ tests/RouterAssertionsTest.php | 59 ++++++ tests/SecurityAssertionsTest.php | 73 +++++++ tests/ServicesAssertionsTest.php | 59 ++++++ tests/SessionAssertionsTest.php | 87 ++++++++ tests/TimeAssertionsTest.php | 64 ++++++ tests/TranslationAssertionsTest.php | 71 +++++++ tests/TwigAssertionsTest.php | 67 ++++++ tests/ValidatorAssertionsTest.php | 63 ++++++ tests/_app/HelloCommand.php | 24 +++ tests/_app/TestKernel.php | 198 ++++++++++++++++++ tests/_app/TestUser.php | 33 +++ tests/_app/ValidEntity.php | 18 ++ tests/_app/templates/home.html.twig | 2 + tests/_app/templates/layout.html.twig | 6 + tests/_app/translations/messages.en.yaml | 1 + tests/_support/.gitkeep | 0 51 files changed, 1745 insertions(+), 15 deletions(-) create mode 100644 codeception.yml create mode 100644 tests/BrowserAssertionsTest.php create mode 100644 tests/BrowserKitConnectorTest.php create mode 100644 tests/ConsoleAssertionsTest.php create mode 100644 tests/DomCrawlerAssertionsTest.php create mode 100644 tests/FormAssertionsTest.php create mode 100644 tests/Functional.suite.yml create mode 100644 tests/Functional/BrowserCest.php create mode 100644 tests/Functional/ConsoleCest.php create mode 100644 tests/Functional/DomCrawlerCest.php create mode 100644 tests/Functional/ExampleCest.php create mode 100644 tests/Functional/FormCest.php create mode 100644 tests/Functional/LoggerCest.php create mode 100644 tests/Functional/MailerCest.php create mode 100644 tests/Functional/MimeCest.php create mode 100644 tests/Functional/ParameterCest.php create mode 100644 tests/Functional/RouterCest.php create mode 100644 tests/Functional/SecurityCest.php create mode 100644 tests/Functional/ServicesCest.php create mode 100644 tests/Functional/SessionCest.php create mode 100644 tests/Functional/TimeCest.php create mode 100644 tests/Functional/TranslationCest.php create mode 100644 tests/Functional/TwigCest.php create mode 100644 tests/Functional/ValidatorCest.php create mode 100644 tests/LoggerAssertionsTest.php create mode 100644 tests/MailerAssertionsTest.php create mode 100644 tests/MimeAssertionsTest.php create mode 100644 tests/ParameterAssertionsTest.php create mode 100644 tests/RouterAssertionsTest.php create mode 100644 tests/SecurityAssertionsTest.php create mode 100644 tests/ServicesAssertionsTest.php create mode 100644 tests/SessionAssertionsTest.php create mode 100644 tests/TimeAssertionsTest.php create mode 100644 tests/TranslationAssertionsTest.php create mode 100644 tests/TwigAssertionsTest.php create mode 100644 tests/ValidatorAssertionsTest.php create mode 100644 tests/_app/HelloCommand.php create mode 100644 tests/_app/TestKernel.php create mode 100644 tests/_app/TestUser.php create mode 100644 tests/_app/ValidEntity.php create mode 100644 tests/_app/templates/home.html.twig create mode 100644 tests/_app/templates/layout.html.twig create mode 100644 tests/_app/translations/messages.en.yaml create mode 100644 tests/_support/.gitkeep diff --git a/.gitignore b/.gitignore index a816930..763daa1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,9 @@ /vendor/ /composer.lock /framework-tests -/.php-cs-fixer.cache \ No newline at end of file +/.php-cs-fixer.cache +tests/_output/ +tests/_support/_generated/ +tests/_support/FunctionalTester.php +var/ + diff --git a/codeception.yml b/codeception.yml new file mode 100644 index 0000000..aa0f0cb --- /dev/null +++ b/codeception.yml @@ -0,0 +1,7 @@ +namespace: Tests +actor_suffix: Tester +paths: + tests: tests + output: tests/_output + data: tests/_data + support: tests/_support diff --git a/composer.json b/composer.json index 810b0b3..76790d8 100644 --- a/composer.json +++ b/composer.json @@ -24,16 +24,16 @@ "require": { "php": "^8.2", "ext-json": "*", - "codeception/codeception": "^5.3", "codeception/lib-innerbrowser": "^3.1 | ^4.0" }, "require-dev": { + "codeception/codeception": "^5.3", "codeception/module-asserts": "^3.0", "codeception/module-doctrine": "^3.1", "doctrine/orm": "^3.5", "friendsofphp/php-cs-fixer": "^3.85", "phpstan/phpstan": "^2.1", - "phpunit/phpunit": "^10.0", + "phpunit/phpunit": "^10.5", "symfony/browser-kit": "^5.4 | ^6.4 | ^7.3", "symfony/cache": "^5.4 | ^6.4 | ^7.3", "symfony/config": "^5.4 | ^6.4 | ^7.3", @@ -73,6 +73,17 @@ "Codeception\\": "src/Codeception/" } }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + }, + "files": [ + "tests/_app/TestKernel.php", + "tests/_app/HelloCommand.php", + "tests/_app/TestUser.php", + "tests/_app/ValidEntity.php" + ] + }, "config": { "sort-packages": true }, diff --git a/readme.md b/readme.md index 34cac75..25af797 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,7 @@ # Codeception Module Symfony A Codeception module for Symfony framework. +It can be used with Codeception or as a standalone Symfony BrowserKit client. [![Actions Status](https://github.com/Codeception/module-symfony/workflows/CI/badge.svg)](https://github.com/Codeception/module-symfony/actions) [![Latest Stable Version](https://poser.pugx.org/codeception/module-symfony/v/stable)](https://github.com/Codeception/module-symfony/releases) @@ -18,6 +19,9 @@ A Codeception module for Symfony framework. composer require "codeception/module-symfony" --dev ``` +To use the connector without Codeception, require the package and instantiate +`Codeception\\Lib\\Connector\\Symfony` with your kernel. + ## Documentation See [the module documentation](https://codeception.com/docs/modules/Symfony). diff --git a/src/Codeception/Lib/Connector/Symfony.php b/src/Codeception/Lib/Connector/Symfony.php index bf18eb2..e0aff60 100644 --- a/src/Codeception/Lib/Connector/Symfony.php +++ b/src/Codeception/Lib/Connector/Symfony.php @@ -17,7 +17,7 @@ use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\HttpKernel\Profiler\Profiler; -use function codecept_debug; +use function function_exists; /** * @property KernelInterface $kernel @@ -73,7 +73,9 @@ public function rebootKernel(): void try { $this->container->set($name, $service); } catch (InvalidArgumentException $e) { - codecept_debug("[Symfony] Can't set persistent service {$name}: {$e->getMessage()}"); + if (function_exists('codecept_debug')) { + codecept_debug("[Symfony] Can't set persistent service {$name}: {$e->getMessage()}"); + } } } diff --git a/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php index d04b69e..10c2165 100644 --- a/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php @@ -309,8 +309,8 @@ public function rebootClientKernel(): void public function seePageIsAvailable(?string $url = null): void { if ($url !== null) { - $this->amOnPage($url); - $this->seeInCurrentUrl($url); + $this->getClient()->request('GET', $url); + $this->assertStringContainsString($url, $this->getClient()->getRequest()->getRequestUri()); } $this->assertResponseIsSuccessful(); @@ -328,12 +328,12 @@ public function seePageRedirectsTo(string $page, string $redirectsTo): void { $client = $this->getClient(); $client->followRedirects(false); - $this->amOnPage($page); + $client->request('GET', $page); $this->assertThatForResponse(new ResponseIsRedirected(), 'The response is not a redirection.'); $client->followRedirect(); - $this->seeInCurrentUrl($redirectsTo); + $this->assertStringContainsString($redirectsTo, $client->getRequest()->getRequestUri()); } /** @@ -362,9 +362,16 @@ public function submitSymfonyForm(string $name, array $fields): void $params[$name . $key] = $value; } - $button = sprintf('%s_submit', $name); + if (method_exists($this, 'submitForm')) { // @phpstan-ignore-line + $button = sprintf('%s_submit', $name); + $this->submitForm($selector, $params, $button); + return; + } - $this->submitForm($selector, $params, $button); + $node = $this->getClient()->getCrawler()->filter($selector); + $this->assertNotEmpty($node, sprintf('Form "%s" not found.', $selector)); + $form = $node->form(); + $this->getClient()->submit($form, $params); } protected function assertThatForClient(Constraint $constraint, string $message = ''): void diff --git a/src/Codeception/Module/Symfony/RouterAssertionsTrait.php b/src/Codeception/Module/Symfony/RouterAssertionsTrait.php index cdbd41e..85ec9aa 100644 --- a/src/Codeception/Module/Symfony/RouterAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/RouterAssertionsTrait.php @@ -112,8 +112,7 @@ private function getCurrentRouteMatch(string $routeName): array { $this->assertRouteExists($routeName); - $url = $this->grabFromCurrentUrl(); - Assert::assertIsString($url, 'Unable to obtain current URL.'); + $url = $this->getClient()->getRequest()->getRequestUri(); $path = (string) parse_url($url, PHP_URL_PATH); /** @var array $match */ @@ -143,7 +142,7 @@ private function assertRouteExists(string $routeName): void /** @param array $params */ private function openRoute(string $routeName, array $params = []): void { - $this->amOnPage($this->grabRouterService()->generate($routeName, $params)); + $this->getClient()->request('GET', $this->grabRouterService()->generate($routeName, $params)); } protected function grabRouterService(): RouterInterface diff --git a/src/Codeception/Module/Symfony/SessionAssertionsTrait.php b/src/Codeception/Module/Symfony/SessionAssertionsTrait.php index 7052894..47137f1 100644 --- a/src/Codeception/Module/Symfony/SessionAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/SessionAssertionsTrait.php @@ -82,7 +82,7 @@ public function dontSeeInSession(string $attribute, mixed $value = null): void */ public function goToLogoutPath(): void { - $this->amOnPage($this->getLogoutUrlGenerator()->getLogoutPath()); + $this->getClient()->request('GET', $this->getLogoutUrlGenerator()->getLogoutPath()); } /** diff --git a/tests/BrowserAssertionsTest.php b/tests/BrowserAssertionsTest.php new file mode 100644 index 0000000..2cec5d7 --- /dev/null +++ b/tests/BrowserAssertionsTest.php @@ -0,0 +1,69 @@ +client = new KernelBrowser(self::$kernel); + $this->client->getCookieJar()->set(new Cookie('browser_cookie', 'value')); + } + + protected function tearDown(): void + { + parent::tearDown(); + restore_exception_handler(); + } + + protected function getClient(): KernelBrowser + { + return $this->client; + } + + protected static function getKernelClass(): string + { + return \TestKernel::class; + } + + public function testBrowserAssertions(): void + { + $this->client->request('GET', '/sample'); + + $this->assertBrowserHasCookie('browser_cookie'); + $this->assertBrowserCookieValueSame('browser_cookie', 'value'); + $this->assertBrowserNotHasCookie('missing_cookie'); + + $this->assertRequestAttributeValueSame('foo', 'bar'); + + $this->assertResponseHasCookie('response_cookie'); + $this->assertResponseCookieValueSame('response_cookie', 'yum'); + $this->assertResponseNotHasCookie('other_cookie'); + + $this->assertResponseHasHeader('X-Test'); + $this->assertResponseHeaderSame('X-Test', '1'); + $this->assertResponseHeaderNotSame('X-Test', '2'); + $this->assertResponseNotHasHeader('X-None'); + + $this->assertResponseFormatSame('html'); + $this->assertResponseIsSuccessful(); + $this->assertResponseStatusCodeSame(200); + $this->assertRouteSame('sample'); + + $this->seePageIsAvailable('/sample'); + $this->seePageRedirectsTo('/redirect', '/sample'); + + $this->client->request('GET', '/unprocessable'); + $this->assertResponseIsUnprocessable(); + } +} diff --git a/tests/BrowserKitConnectorTest.php b/tests/BrowserKitConnectorTest.php new file mode 100644 index 0000000..163f634 --- /dev/null +++ b/tests/BrowserKitConnectorTest.php @@ -0,0 +1,26 @@ +boot(); + $browser = new SymfonyConnector($kernel); + + $browser->request('GET', '/'); + + $this->assertSame(200, $browser->getResponse()->getStatusCode()); + $this->assertSame('OK', $browser->getResponse()->getContent()); + } + + protected function tearDown(): void + { + restore_exception_handler(); + parent::tearDown(); + } +} diff --git a/tests/ConsoleAssertionsTest.php b/tests/ConsoleAssertionsTest.php new file mode 100644 index 0000000..f3d2c97 --- /dev/null +++ b/tests/ConsoleAssertionsTest.php @@ -0,0 +1,38 @@ +get($serviceId); + } + + protected function unpersistService(string $serviceName): void + { + // no-op for tests + } + + public function testRunSymfonyConsoleCommand(): void + { + $output = $this->runSymfonyConsoleCommand('app:hello', ['name' => 'Codeception']); + $this->assertStringContainsString('Hello Codeception', $output); + } + + protected function tearDown(): void + { + restore_exception_handler(); + parent::tearDown(); + } +} diff --git a/tests/DomCrawlerAssertionsTest.php b/tests/DomCrawlerAssertionsTest.php new file mode 100644 index 0000000..de50d8c --- /dev/null +++ b/tests/DomCrawlerAssertionsTest.php @@ -0,0 +1,52 @@ +client = new KernelBrowser(self::$kernel); + $this->client->request('GET', '/sample'); + } + + protected function tearDown(): void + { + parent::tearDown(); + restore_exception_handler(); + } + + protected function getClient(): KernelBrowser + { + return $this->client; + } + + protected static function getKernelClass(): string + { + return \TestKernel::class; + } + + public function testDomCrawlerAssertions(): void + { + $this->assertCheckboxChecked('agree'); + $this->assertCheckboxNotChecked('subscribe'); + $this->assertInputValueSame('username', 'john'); + $this->assertInputValueNotSame('username', 'doe'); + $this->assertPageTitleContains('Test'); + $this->assertPageTitleSame('Test Page'); + $this->assertSelectorExists('#greeting'); + $this->assertSelectorNotExists('#missing'); + $this->assertSelectorTextContains('#greeting', 'Hello'); + $this->assertSelectorTextNotContains('#greeting', 'Bye'); + $this->assertSelectorTextSame('#greeting', 'Hello World'); + } +} diff --git a/tests/FormAssertionsTest.php b/tests/FormAssertionsTest.php new file mode 100644 index 0000000..cf31d11 --- /dev/null +++ b/tests/FormAssertionsTest.php @@ -0,0 +1,43 @@ +client = new KernelBrowser(self::$kernel); + $this->client->request('GET', '/sample'); + } + + protected function tearDown(): void + { + parent::tearDown(); + restore_exception_handler(); + } + + protected function getClient(): KernelBrowser + { + return $this->client; + } + + protected static function getKernelClass(): string + { + return \TestKernel::class; + } + + public function testFormAssertions(): void + { + $this->assertFormValue('#testForm', 'field1', 'value1'); + $this->assertNoFormValue('#testForm', 'missing_field'); + } +} diff --git a/tests/Functional.suite.yml b/tests/Functional.suite.yml new file mode 100644 index 0000000..e010b19 --- /dev/null +++ b/tests/Functional.suite.yml @@ -0,0 +1,8 @@ +actor: FunctionalTester +modules: + enabled: + - Symfony: + app_path: tests/_app + environment: 'test' + kernel_class: TestKernel + - Asserts diff --git a/tests/Functional/BrowserCest.php b/tests/Functional/BrowserCest.php new file mode 100644 index 0000000..76437f2 --- /dev/null +++ b/tests/Functional/BrowserCest.php @@ -0,0 +1,39 @@ +setCookie('browser_cookie', 'value'); + $I->amOnPage('/sample'); + + $I->assertBrowserHasCookie('browser_cookie'); + $I->assertBrowserCookieValueSame('browser_cookie', 'value'); + $I->assertBrowserNotHasCookie('missing_cookie'); + + $I->assertRequestAttributeValueSame('foo', 'bar'); + + $I->assertResponseHasCookie('response_cookie'); + $I->assertResponseCookieValueSame('response_cookie', 'yum'); + $I->assertResponseNotHasCookie('other_cookie'); + + $I->assertResponseHasHeader('X-Test'); + $I->assertResponseHeaderSame('X-Test', '1'); + $I->assertResponseHeaderNotSame('X-Test', '2'); + $I->assertResponseNotHasHeader('X-None'); + + $I->assertResponseFormatSame('html'); + $I->assertResponseIsSuccessful(); + $I->assertResponseStatusCodeSame(200); + $I->assertRouteSame('sample'); + + $I->seePageIsAvailable('/sample'); + $I->seePageRedirectsTo('/redirect', '/sample'); + + $I->amOnPage('/unprocessable'); + $I->assertResponseIsUnprocessable(); + } +} diff --git a/tests/Functional/ConsoleCest.php b/tests/Functional/ConsoleCest.php new file mode 100644 index 0000000..1bd0ffa --- /dev/null +++ b/tests/Functional/ConsoleCest.php @@ -0,0 +1,13 @@ +runSymfonyConsoleCommand('app:hello', ['name' => 'Codeception']); + $I->assertStringContainsString('Hello Codeception', $output); + } +} diff --git a/tests/Functional/DomCrawlerCest.php b/tests/Functional/DomCrawlerCest.php new file mode 100644 index 0000000..938499c --- /dev/null +++ b/tests/Functional/DomCrawlerCest.php @@ -0,0 +1,24 @@ +amOnPage('/sample'); + + $I->assertCheckboxChecked('agree'); + $I->assertCheckboxNotChecked('subscribe'); + $I->assertInputValueSame('username', 'john'); + $I->assertInputValueNotSame('username', 'doe'); + $I->assertPageTitleContains('Test'); + $I->assertPageTitleSame('Test Page'); + $I->assertSelectorExists('#greeting'); + $I->assertSelectorNotExists('#missing'); + $I->assertSelectorTextContains('#greeting', 'Hello'); + $I->assertSelectorTextNotContains('#greeting', 'Bye'); + $I->assertSelectorTextSame('#greeting', 'Hello World'); + } +} diff --git a/tests/Functional/ExampleCest.php b/tests/Functional/ExampleCest.php new file mode 100644 index 0000000..ea74f95 --- /dev/null +++ b/tests/Functional/ExampleCest.php @@ -0,0 +1,14 @@ +amOnPage('/'); + $I->seeResponseCodeIs(200); + $I->see('OK'); + } +} diff --git a/tests/Functional/FormCest.php b/tests/Functional/FormCest.php new file mode 100644 index 0000000..0116a03 --- /dev/null +++ b/tests/Functional/FormCest.php @@ -0,0 +1,15 @@ +amOnPage('/sample'); + + $I->assertFormValue('#testForm', 'field1', 'value1'); + $I->assertNoFormValue('#testForm', 'missing_field'); + } +} diff --git a/tests/Functional/LoggerCest.php b/tests/Functional/LoggerCest.php new file mode 100644 index 0000000..8a5d453 --- /dev/null +++ b/tests/Functional/LoggerCest.php @@ -0,0 +1,13 @@ +amOnPage('/sample'); + $I->dontSeeDeprecations(); + } +} diff --git a/tests/Functional/MailerCest.php b/tests/Functional/MailerCest.php new file mode 100644 index 0000000..cfe4140 --- /dev/null +++ b/tests/Functional/MailerCest.php @@ -0,0 +1,37 @@ +grabService('mailer.message_logger_listener'); + $logger->reset(); + $I->dontSeeEmailIsSent(); + + $queuedEmail = (new Email())->from('queued@example.com')->to('queued@example.com'); + $envelope = new Envelope(new Address('queued@example.com'), [new Address('queued@example.com')]); + $queuedEvent = new MessageEvent($queuedEmail, $envelope, 'smtp', true); + $logger->onMessage($queuedEvent); + + $I->assertQueuedEmailCount(1); + $I->assertEmailIsQueued($queuedEvent); + + $I->amOnRoute('send_email'); + + $I->assertEmailCount(1); + $I->seeEmailIsSent(); + $I->grabLastSentEmail(); + $I->grabSentEmails(); + $event = $I->getMailerEvent(1); + $I->assertEmailIsNotQueued($event); + } +} diff --git a/tests/Functional/MimeCest.php b/tests/Functional/MimeCest.php new file mode 100644 index 0000000..ca89fea --- /dev/null +++ b/tests/Functional/MimeCest.php @@ -0,0 +1,26 @@ +grabService('mailer.message_logger_listener'); + $logger->reset(); + + $I->amOnRoute('send_email'); + + $I->assertEmailAddressContains('To', 'jane_doe@example.com'); + $I->assertEmailAttachmentCount(1); + $I->assertEmailHasHeader('To'); + $I->assertEmailHeaderSame('To', 'jane_doe@example.com'); + $I->assertEmailHeaderNotSame('To', 'john_doe@example.com'); + $I->assertEmailHtmlBodyContains('HTML body'); + $I->assertEmailHtmlBodyNotContains('password'); + $I->assertEmailNotHasHeader('Bcc'); + $I->assertEmailTextBodyContains('Example text body'); + $I->assertEmailTextBodyNotContains('Secret'); + } +} diff --git a/tests/Functional/ParameterCest.php b/tests/Functional/ParameterCest.php new file mode 100644 index 0000000..d5ef8eb --- /dev/null +++ b/tests/Functional/ParameterCest.php @@ -0,0 +1,12 @@ +assertSame('value', $I->grabParameter('app.param')); + } +} diff --git a/tests/Functional/RouterCest.php b/tests/Functional/RouterCest.php new file mode 100644 index 0000000..62e2e56 --- /dev/null +++ b/tests/Functional/RouterCest.php @@ -0,0 +1,19 @@ +amOnRoute('sample'); + $I->seeCurrentRouteIs('sample'); + $I->seeInCurrentRoute('sample'); + $I->seeCurrentActionIs('TestKernel::sample'); + + $I->amOnAction('TestKernel::index'); + $I->seeCurrentRouteIs('index'); + $I->invalidateCachedRouter(); + } +} diff --git a/tests/Functional/SecurityCest.php b/tests/Functional/SecurityCest.php new file mode 100644 index 0000000..439162b --- /dev/null +++ b/tests/Functional/SecurityCest.php @@ -0,0 +1,23 @@ +dontSeeAuthentication(); + $hasher = $I->grabService('security.password_hasher'); + $hashed = $hasher->hashPassword(new \TestUser('tmp', ''), 'password'); + $user = new \TestUser('john@example.com', $hashed, ['ROLE_USER', 'ROLE_ADMIN']); + $I->amLoggedInAs($user); + + $I->seeAuthentication(); + + $I->seeUserHasRole('ROLE_ADMIN'); + $I->seeUserHasRoles(['ROLE_USER', 'ROLE_ADMIN']); + $I->seeUserPasswordDoesNotNeedRehash(); + } +} diff --git a/tests/Functional/ServicesCest.php b/tests/Functional/ServicesCest.php new file mode 100644 index 0000000..794b98b --- /dev/null +++ b/tests/Functional/ServicesCest.php @@ -0,0 +1,15 @@ +grabService('router'); + $I->persistService('router'); + $I->persistPermanentService('router'); + $I->unpersistService('router'); + } +} diff --git a/tests/Functional/SessionCest.php b/tests/Functional/SessionCest.php new file mode 100644 index 0000000..450fb00 --- /dev/null +++ b/tests/Functional/SessionCest.php @@ -0,0 +1,47 @@ +grabService('service_container'); + $factory = $I->grabService('session.factory'); + $session = $factory->createSession(); + $container->set('session', $session); + $session->set('key1', 'value1'); + $session->set('key2', 'value2'); + $session->save(); + + $I->seeInSession('key1'); + $I->seeInSession('key1', 'value1'); + $I->dontSeeInSession('missing'); + $I->dontSeeInSession('key1', 'other'); + $I->seeSessionHasValues(['key1', 'key2']); + $I->seeSessionHasValues(['key1' => 'value1', 'key2' => 'value2']); + } + + public function loginAndLogoutAssertions(FunctionalTester $I): void + { + $user = new InMemoryUser('john@example.com', null, ['ROLE_USER']); + + $I->amLoggedInAs($user); + $I->seeInSession('_security_main'); + $I->logout(); + $I->dontSeeInSession('_security_main'); + + $token = new UsernamePasswordToken($user, 'main', ['ROLE_USER']); + $I->amLoggedInWithToken($token); + $I->seeInSession('_security_main'); + $I->goToLogoutPath(); + + $I->amLoggedInAs($user); + $I->seeInSession('_security_main'); + $I->logoutProgrammatically(); + $I->dontSeeInSession('_security_main'); + } +} diff --git a/tests/Functional/TimeCest.php b/tests/Functional/TimeCest.php new file mode 100644 index 0000000..3b901e5 --- /dev/null +++ b/tests/Functional/TimeCest.php @@ -0,0 +1,13 @@ +amOnRoute('sample'); + $I->seeRequestTimeIsLessThan(500); + } +} diff --git a/tests/Functional/TranslationCest.php b/tests/Functional/TranslationCest.php new file mode 100644 index 0000000..63f9cc5 --- /dev/null +++ b/tests/Functional/TranslationCest.php @@ -0,0 +1,20 @@ +amOnRoute('translation'); + $I->dontSeeMissingTranslations(); + $I->dontSeeFallbackTranslations(); + $I->assertGreaterThanOrEqual(0, $I->grabDefinedTranslationsCount()); + $I->seeAllTranslationsDefined(); + $I->seeDefaultLocaleIs('en'); + $I->seeFallbackLocalesAre(['es']); + $I->seeFallbackTranslationsCountLessThan(1); + $I->seeMissingTranslationsCountLessThan(1); + } +} diff --git a/tests/Functional/TwigCest.php b/tests/Functional/TwigCest.php new file mode 100644 index 0000000..74515dd --- /dev/null +++ b/tests/Functional/TwigCest.php @@ -0,0 +1,16 @@ +amOnRoute('twig'); + $I->seeRenderedTemplate('home.html.twig'); + $I->seeRenderedTemplate('layout.html.twig'); + $I->dontSeeRenderedTemplate('other.html.twig'); + $I->seeCurrentTemplateIs('home.html.twig'); + } +} diff --git a/tests/Functional/ValidatorCest.php b/tests/Functional/ValidatorCest.php new file mode 100644 index 0000000..3da2732 --- /dev/null +++ b/tests/Functional/ValidatorCest.php @@ -0,0 +1,23 @@ +seeViolatedConstraint($invalid); + $I->seeViolatedConstraint($invalid, 'name'); + $I->seeViolatedConstraint($invalid, 'short', Assert\Length::class); + $I->seeViolatedConstraintsCount(2, $invalid); + $I->seeViolatedConstraintsCount(1, $invalid, 'name'); + $I->seeViolatedConstraintMessage('too short', $invalid, 'short'); + $I->dontSeeViolatedConstraint($valid); + $I->dontSeeViolatedConstraint($invalid, 'short', Assert\NotBlank::class); + } +} diff --git a/tests/LoggerAssertionsTest.php b/tests/LoggerAssertionsTest.php new file mode 100644 index 0000000..b9c0c1c --- /dev/null +++ b/tests/LoggerAssertionsTest.php @@ -0,0 +1,64 @@ +client = new KernelBrowser(self::$kernel); + } + + protected static function getKernelClass(): string + { + return \TestKernel::class; + } + + protected function getClient(): KernelBrowser + { + return $this->client; + } + + protected function grabService(string $serviceId): object + { + return self::getContainer()->get($serviceId); + } + + protected function unpersistService(string $serviceName): void + { + // no-op for tests + } + + public function testDontSeeDeprecations(): void + { + $this->client->request('GET', '/sample'); + $this->dontSeeDeprecations(); + } + + + protected function tearDown(): void + { + restore_exception_handler(); + parent::tearDown(); + } + + protected function grabCollector(DataCollectorName $name, string $function): DataCollectorInterface + { + /** @var Profiler $profiler */ + $profiler = self::getContainer()->get('profiler'); + $profile = $profiler->collect($this->client->getRequest(), $this->client->getResponse()); + return $profile->getCollector($name->value); + } +} diff --git a/tests/MailerAssertionsTest.php b/tests/MailerAssertionsTest.php new file mode 100644 index 0000000..552413b --- /dev/null +++ b/tests/MailerAssertionsTest.php @@ -0,0 +1,84 @@ +kernel = new \TestKernel('test', true); + $this->kernel->boot(); + $this->client = new KernelBrowser($this->kernel); + $this->getService('mailer.message_logger_listener')->reset(); + } + + protected function tearDown(): void + { + $this->kernel->shutdown(); + restore_exception_handler(); + parent::tearDown(); + } + + protected function getClient(): KernelBrowser + { + return $this->client; + } + + protected function getService(string $serviceId): object + { + $container = $this->kernel->getContainer(); + if ($container->has('test.service_container')) { + $container = $container->get('test.service_container'); + } + return $container->get($serviceId); + } + + public function testMailerAssertions(): void + { + $this->dontSeeEmailIsSent(); + + $queuedEmail = (new Email()) + ->from('queued@example.com') + ->to('queued@example.com'); + $envelope = new Envelope(new Address('queued@example.com'), [new Address('queued@example.com')]); + $queuedEvent = new MessageEvent($queuedEmail, $envelope, 'smtp', true); + /** @var MessageLoggerListener $logger */ + $logger = $this->getService('mailer.message_logger_listener'); + $logger->onMessage($queuedEvent); + + $this->assertQueuedEmailCount(1); + $this->assertEmailIsQueued($queuedEvent); + + $mailer = $this->getService('mailer'); + $mailer->send((new Email()) + ->from('john_doe@example.com') + ->to('jane_doe@example.com') + ->subject('Test') + ->text('Example text body') + ->html('

HTML body

') + ->attach('Attachment content', 'test.txt') + ); + + $this->assertEmailCount(1); + $this->seeEmailIsSent(); + $this->grabLastSentEmail(); + $this->grabSentEmails(); + $event = $this->getMailerEvent(1); + $this->assertEmailIsNotQueued($event); + } +} diff --git a/tests/MimeAssertionsTest.php b/tests/MimeAssertionsTest.php new file mode 100644 index 0000000..8c55086 --- /dev/null +++ b/tests/MimeAssertionsTest.php @@ -0,0 +1,71 @@ +kernel = new \TestKernel('test', true); + $this->kernel->boot(); + $this->client = new KernelBrowser($this->kernel); + $this->getService('mailer.message_logger_listener')->reset(); + + $mailer = $this->getService('mailer'); + $mailer->send((new Email()) + ->from('john_doe@example.com') + ->to('jane_doe@example.com') + ->subject('Test') + ->text('Example text body') + ->html('

HTML body

') + ->attach('Attachment content', 'test.txt') + ); + } + + protected function tearDown(): void + { + $this->kernel->shutdown(); + restore_exception_handler(); + parent::tearDown(); + } + + protected function getClient(): KernelBrowser + { + return $this->client; + } + + protected function getService(string $serviceId): object + { + $container = $this->kernel->getContainer(); + if ($container->has('test.service_container')) { + $container = $container->get('test.service_container'); + } + return $container->get($serviceId); + } + + public function testMimeAssertions(): void + { + $this->assertEmailAddressContains('To', 'jane_doe@example.com'); + $this->assertEmailAttachmentCount(1); + $this->assertEmailHasHeader('To'); + $this->assertEmailHeaderSame('To', 'jane_doe@example.com'); + $this->assertEmailHeaderNotSame('To', 'john_doe@example.com'); + $this->assertEmailHtmlBodyContains('HTML body'); + $this->assertEmailHtmlBodyNotContains('password'); + $this->assertEmailNotHasHeader('Bcc'); + $this->assertEmailTextBodyContains('Example text body'); + $this->assertEmailTextBodyNotContains('Secret'); + } +} diff --git a/tests/ParameterAssertionsTest.php b/tests/ParameterAssertionsTest.php new file mode 100644 index 0000000..398005b --- /dev/null +++ b/tests/ParameterAssertionsTest.php @@ -0,0 +1,46 @@ +client = new KernelBrowser(self::$kernel); + } + + protected static function getKernelClass(): string + { + return \TestKernel::class; + } + + protected function getClient(): KernelBrowser + { + return $this->client; + } + + protected function grabService(string $serviceId): object + { + return self::getContainer()->get($serviceId); + } + + public function testGrabParameter(): void + { + $this->assertSame('value', $this->grabParameter('app.param')); + } + + protected function tearDown(): void + { + restore_exception_handler(); + parent::tearDown(); + } +} diff --git a/tests/RouterAssertionsTest.php b/tests/RouterAssertionsTest.php new file mode 100644 index 0000000..5d68527 --- /dev/null +++ b/tests/RouterAssertionsTest.php @@ -0,0 +1,59 @@ +client = new KernelBrowser(self::$kernel); + } + + protected static function getKernelClass(): string + { + return \TestKernel::class; + } + + protected function getClient(): KernelBrowser + { + return $this->client; + } + + protected function grabService(string $serviceId): object + { + return self::getContainer()->get($serviceId); + } + + protected function unpersistService(string $serviceName): void + { + // no-op for tests + } + + public function testRouterAssertions(): void + { + $this->amOnRoute('sample'); + $this->seeCurrentRouteIs('sample'); + $this->seeInCurrentRoute('sample'); + $this->seeCurrentActionIs('TestKernel::sample'); + + $this->amOnAction('TestKernel::index'); + $this->seeCurrentRouteIs('index'); + + $this->invalidateCachedRouter(); + } + + protected function tearDown(): void + { + restore_exception_handler(); + parent::tearDown(); + } +} diff --git a/tests/SecurityAssertionsTest.php b/tests/SecurityAssertionsTest.php new file mode 100644 index 0000000..072049b --- /dev/null +++ b/tests/SecurityAssertionsTest.php @@ -0,0 +1,73 @@ +client = new KernelBrowser(self::$kernel); + } + + protected static function getKernelClass(): string + { + return \TestKernel::class; + } + + protected function getClient(): KernelBrowser + { + return $this->client; + } + + protected function grabService(string $serviceId): object + { + return self::getContainer()->get($serviceId); + } + + protected function getService(string $serviceId): object + { + return self::getContainer()->get($serviceId); + } + + protected function grabSecurityService() + { + return new Security(self::getContainer()); + } + + public function testSecurityAssertions(): void + { + $this->dontSeeAuthentication(); + $this->dontSeeRememberedAuthentication(); + + $hasher = $this->grabService('security.password_hasher'); + $hashed = $hasher->hashPassword(new \TestUser('tmp', ''), 'password'); + $user = new \TestUser('john@example.com', $hashed, ['ROLE_USER', 'ROLE_ADMIN']); + $this->getClient()->loginUser($user); + + $this->seeAuthentication(); + + $this->getClient()->getCookieJar()->set(new Cookie('REMEMBERME', 'test')); + $this->seeRememberedAuthentication(); + + $this->seeUserHasRole('ROLE_ADMIN'); + $this->seeUserHasRoles(['ROLE_USER', 'ROLE_ADMIN']); + $this->seeUserPasswordDoesNotNeedRehash(); + } + + protected function tearDown(): void + { + restore_exception_handler(); + parent::tearDown(); + } +} diff --git a/tests/ServicesAssertionsTest.php b/tests/ServicesAssertionsTest.php new file mode 100644 index 0000000..3972cfe --- /dev/null +++ b/tests/ServicesAssertionsTest.php @@ -0,0 +1,59 @@ +client = new KernelBrowser(self::$kernel); + } + + protected static function getKernelClass(): string + { + return \TestKernel::class; + } + + protected function _getContainer(): ContainerInterface + { + return self::getContainer(); + } + + protected function getClient(): KernelBrowser + { + return $this->client; + } + + public function testServicesAssertions(): void + { + $this->grabService('router'); + $this->persistService('router'); + $this->assertArrayHasKey('router', $this->persistentServices); + + $this->persistPermanentService('router'); + $this->assertArrayHasKey('router', $this->permanentServices); + + $this->unpersistService('router'); + $this->assertArrayNotHasKey('router', $this->persistentServices); + $this->assertArrayNotHasKey('router', $this->permanentServices); + } + + protected function tearDown(): void + { + restore_exception_handler(); + parent::tearDown(); + } +} diff --git a/tests/SessionAssertionsTest.php b/tests/SessionAssertionsTest.php new file mode 100644 index 0000000..b277762 --- /dev/null +++ b/tests/SessionAssertionsTest.php @@ -0,0 +1,87 @@ +client = new KernelBrowser(self::$kernel); + } + + protected static function getKernelClass(): string + { + return \TestKernel::class; + } + + protected function getClient(): KernelBrowser + { + return $this->client; + } + + protected function grabService(string $serviceId): object + { + return self::getContainer()->get($serviceId); + } + + protected function _getContainer(): ContainerInterface + { + return self::getContainer(); + } + + public function testSessionAssertions(): void + { + $container = self::getContainer(); + $factory = $container->get('session.factory'); + $session = $factory->createSession(); + $container->set('session', $session); + $session->set('key1', 'value1'); + $session->set('key2', 'value2'); + $session->save(); + + $this->seeInSession('key1'); + $this->seeInSession('key1', 'value1'); + $this->dontSeeInSession('missing'); + $this->dontSeeInSession('key1', 'other'); + $this->seeSessionHasValues(['key1', 'key2']); + $this->seeSessionHasValues(['key1' => 'value1', 'key2' => 'value2']); + } + + public function testLoginAndLogoutAssertions(): void + { + $user = new InMemoryUser('john@example.com', null, ['ROLE_USER']); + + $this->amLoggedInAs($user); + $this->seeInSession('_security_main'); + $this->logout(); + $this->dontSeeInSession('_security_main'); + + $token = new UsernamePasswordToken($user, 'main', ['ROLE_USER']); + $this->amLoggedInWithToken($token); + $this->seeInSession('_security_main'); + $this->goToLogoutPath(); + + $this->amLoggedInAs($user); + $this->seeInSession('_security_main'); + $this->logoutProgrammatically(); + $this->dontSeeInSession('_security_main'); + } + + protected function tearDown(): void + { + restore_exception_handler(); + parent::tearDown(); + } +} diff --git a/tests/TimeAssertionsTest.php b/tests/TimeAssertionsTest.php new file mode 100644 index 0000000..12a7c32 --- /dev/null +++ b/tests/TimeAssertionsTest.php @@ -0,0 +1,64 @@ +client = new KernelBrowser(self::$kernel); + $this->client->request('GET', '/sample'); + } + + protected static function getKernelClass(): string + { + return \TestKernel::class; + } + + protected function getClient(): KernelBrowser + { + return $this->client; + } + + protected function grabService(string $serviceId): object + { + return self::getContainer()->get($serviceId); + } + + protected function _getContainer(): ContainerInterface + { + return self::getContainer(); + } + + public function testRequestTime(): void + { + $this->seeRequestTimeIsLessThan(500); + } + + protected function grabCollector(DataCollectorName $name, string $function): DataCollectorInterface + { + /** @var Profiler $profiler */ + $profiler = self::getContainer()->get('profiler'); + $profile = $profiler->collect($this->client->getRequest(), $this->client->getResponse()); + return $profile->getCollector($name->value); + } + + protected function tearDown(): void + { + restore_exception_handler(); + parent::tearDown(); + } +} diff --git a/tests/TranslationAssertionsTest.php b/tests/TranslationAssertionsTest.php new file mode 100644 index 0000000..6a91c28 --- /dev/null +++ b/tests/TranslationAssertionsTest.php @@ -0,0 +1,71 @@ +client = new KernelBrowser(self::$kernel); + $this->client->request('GET', '/translation'); + } + + protected static function getKernelClass(): string + { + return \TestKernel::class; + } + + protected function getClient(): KernelBrowser + { + return $this->client; + } + + protected function grabService(string $serviceId): object + { + return self::getContainer()->get($serviceId); + } + + protected function _getContainer(): ContainerInterface + { + return self::getContainer(); + } + + public function testTranslationAssertions(): void + { + $this->dontSeeMissingTranslations(); + $this->dontSeeFallbackTranslations(); + $this->assertGreaterThanOrEqual(0, $this->grabDefinedTranslationsCount()); + $this->seeAllTranslationsDefined(); + $this->seeDefaultLocaleIs('en'); + $this->seeFallbackLocalesAre(['es']); + $this->seeFallbackTranslationsCountLessThan(1); + $this->seeMissingTranslationsCountLessThan(1); + } + + protected function grabCollector(DataCollectorName $name, string $function): DataCollectorInterface + { + /** @var Profiler $profiler */ + $profiler = self::getContainer()->get('profiler'); + $profile = $profiler->collect($this->client->getRequest(), $this->client->getResponse()); + return $profile->getCollector($name->value); + } + + protected function tearDown(): void + { + restore_exception_handler(); + parent::tearDown(); + } +} diff --git a/tests/TwigAssertionsTest.php b/tests/TwigAssertionsTest.php new file mode 100644 index 0000000..0bdaccd --- /dev/null +++ b/tests/TwigAssertionsTest.php @@ -0,0 +1,67 @@ +client = new KernelBrowser(self::$kernel); + $this->client->request('GET', '/twig'); + } + + protected static function getKernelClass(): string + { + return \TestKernel::class; + } + + protected function getClient(): KernelBrowser + { + return $this->client; + } + + protected function grabService(string $serviceId): object + { + return self::getContainer()->get($serviceId); + } + + protected function _getContainer(): ContainerInterface + { + return self::getContainer(); + } + + public function testTwigAssertions(): void + { + $this->seeRenderedTemplate('home.html.twig'); + $this->seeRenderedTemplate('layout.html.twig'); + $this->dontSeeRenderedTemplate('other.html.twig'); + $this->seeCurrentTemplateIs('home.html.twig'); + } + + protected function grabCollector(DataCollectorName $name, string $function): DataCollectorInterface + { + /** @var Profiler $profiler */ + $profiler = self::getContainer()->get('profiler'); + $profile = $profiler->collect($this->client->getRequest(), $this->client->getResponse()); + return $profile->getCollector($name->value); + } + + protected function tearDown(): void + { + restore_exception_handler(); + parent::tearDown(); + } +} diff --git a/tests/ValidatorAssertionsTest.php b/tests/ValidatorAssertionsTest.php new file mode 100644 index 0000000..5aa2bcc --- /dev/null +++ b/tests/ValidatorAssertionsTest.php @@ -0,0 +1,63 @@ +client = new KernelBrowser(self::$kernel); + } + + protected static function getKernelClass(): string + { + return \TestKernel::class; + } + + protected function getClient(): KernelBrowser + { + return $this->client; + } + + protected function grabService(string $serviceId): object + { + return self::getContainer()->get($serviceId); + } + + protected function _getContainer(): ContainerInterface + { + return self::getContainer(); + } + + public function testValidatorAssertions(): void + { + $invalid = new \ValidEntity(); + $valid = new \ValidEntity('John', 'abcd'); + + $this->seeViolatedConstraint($invalid); + $this->seeViolatedConstraint($invalid, 'name'); + $this->seeViolatedConstraint($invalid, 'short', Assert\Length::class); + $this->seeViolatedConstraintsCount(2, $invalid); + $this->seeViolatedConstraintsCount(1, $invalid, 'name'); + $this->seeViolatedConstraintMessage('too short', $invalid, 'short'); + $this->dontSeeViolatedConstraint($valid); + $this->dontSeeViolatedConstraint($invalid, 'short', Assert\NotBlank::class); + } + + protected function tearDown(): void + { + restore_exception_handler(); + parent::tearDown(); + } +} diff --git a/tests/_app/HelloCommand.php b/tests/_app/HelloCommand.php new file mode 100644 index 0000000..f583d40 --- /dev/null +++ b/tests/_app/HelloCommand.php @@ -0,0 +1,24 @@ +setDescription('Greets the user') + ->addArgument('name', InputArgument::OPTIONAL, 'Name to greet', 'World'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $name = (string) $input->getArgument('name'); + $output->writeln('Hello ' . $name); + return Command::SUCCESS; + } +} diff --git a/tests/_app/TestKernel.php b/tests/_app/TestKernel.php new file mode 100644 index 0000000..d8dc71e --- /dev/null +++ b/tests/_app/TestKernel.php @@ -0,0 +1,198 @@ +extension('framework', [ + 'secret' => 'test', + 'test' => true, + 'profiler' => ['enabled' => true, 'collect' => true, 'collect_serializer_data' => true], + 'property_info' => ['with_constructor_extractor' => false], + 'session' => [ + 'handler_id' => null, + 'storage_factory_id' => 'session.storage.factory.mock_file', + ], + 'mailer' => ['dsn' => 'null://null'], + 'default_locale' => 'en', + 'translator' => [ + 'default_path' => __DIR__ . '/translations', + 'fallbacks' => ['es'], + ], + 'validation' => ['enabled' => true], + ]); + + $container->extension('twig', [ + 'default_path' => __DIR__ . '/templates', + ]); + + $container->extension('security', [ + 'password_hashers' => [ + PasswordAuthenticatedUserInterface::class => 'auto', + ], + 'providers' => [ + 'users_in_memory' => [ + 'memory' => [ + 'users' => [], + ], + ], + ], + 'firewalls' => [ + 'main' => [ + 'lazy' => true, + 'provider' => 'users_in_memory', + 'remember_me' => ['secret' => 'test'], + 'logout' => ['path' => 'logout'], + ], + ], + ]); + + $container->parameters()->set('app.param', 'value'); + + $services = $container->services(); + $services->set(HelloCommand::class, HelloCommand::class) + ->tag('console.command', ['command' => 'app:hello']) + ->public(); + $services->set(Security::class) + ->public() + ->arg('$container', service('test.service_container')); + $services->alias('security.helper', Security::class)->public(); + $services->set('mailer.message_logger_listener', MessageLoggerListener::class) + ->tag('kernel.event_subscriber') + ->public(); + } + + public function registerBundles(): iterable + { + return [ + new FrameworkBundle(), + new SecurityBundle(), + new TwigBundle(), + ]; + } + + protected function configureRoutes(RoutingConfigurator $routes): void + { + $routes->add('index', '/') + ->controller(self::class . '::index'); + $routes->add('sample', '/sample') + ->controller(self::class . '::sample'); + $routes->add('redirect', '/redirect') + ->controller(self::class . '::redirect'); + $routes->add('unprocessable', '/unprocessable') + ->controller(self::class . '::unprocessable'); + $routes->add('session', '/session') + ->controller(self::class . '::session'); + $routes->add('deprecated', '/deprecated') + ->controller(self::class . '::deprecated'); + $routes->add('send_email', '/send-email') + ->controller(self::class . '::sendEmail'); + $routes->add('translation', '/translation') + ->controller(self::class . '::translation'); + $routes->add('twig', '/twig') + ->controller(self::class . '::twig'); + $routes->add('logout', '/logout') + ->controller(self::class . '::index'); + } + + public function index(): Response + { + return new Response('OK'); + } + + public function sample(Request $request): Response + { + $request->attributes->set('foo', 'bar'); + $html = << + Test Page + + + + + +
+ +
+
Hello World
+ + +HTML; + $response = new Response($html, 200, ['X-Test' => '1']); + $response->headers->setCookie(new Cookie('response_cookie', 'yum')); + return $response; + } + + public function redirect(): RedirectResponse + { + return new RedirectResponse('/sample'); + } + + public function unprocessable(): Response + { + return new Response('Unprocessable', 422); + } + + public function session(Request $request): Response + { + $session = $request->getSession(); + $session->set('key1', 'value1'); + $session->set('key2', 'value2'); + return new Response('Session'); + } + + public function deprecated(): Response + { + trigger_error('Deprecated endpoint', E_USER_DEPRECATED); + return new Response('Deprecated'); + } + + public function sendEmail(MailerInterface $mailer): Response + { + $email = (new Email()) + ->from('john_doe@example.com') + ->to('jane_doe@example.com') + ->subject('Test') + ->text('Example text body') + ->html('

HTML body

') + ->attach('Attachment content', 'test.txt'); + + $mailer->send($email); + + return new Response('Email sent'); + } + + public function translation(TranslatorInterface $translator): Response + { + $translator->trans('defined_message'); + return new Response('Translation'); + } + + public function twig(Environment $twig): Response + { + return new Response($twig->render('home.html.twig')); + } +} diff --git a/tests/_app/TestUser.php b/tests/_app/TestUser.php new file mode 100644 index 0000000..69caf2f --- /dev/null +++ b/tests/_app/TestUser.php @@ -0,0 +1,33 @@ +userIdentifier; + } + + public function getRoles(): array + { + return $this->roles; + } + + public function getPassword(): string + { + return $this->password; + } + + public function eraseCredentials(): void + { + } +} diff --git a/tests/_app/ValidEntity.php b/tests/_app/ValidEntity.php new file mode 100644 index 0000000..2729a7d --- /dev/null +++ b/tests/_app/ValidEntity.php @@ -0,0 +1,18 @@ +name = $name; + $this->short = $short; + } +} diff --git a/tests/_app/templates/home.html.twig b/tests/_app/templates/home.html.twig new file mode 100644 index 0000000..d2f72c4 --- /dev/null +++ b/tests/_app/templates/home.html.twig @@ -0,0 +1,2 @@ +{% extends "layout.html.twig" %} +{% block content %}Home{% endblock %} diff --git a/tests/_app/templates/layout.html.twig b/tests/_app/templates/layout.html.twig new file mode 100644 index 0000000..82f27ae --- /dev/null +++ b/tests/_app/templates/layout.html.twig @@ -0,0 +1,6 @@ + + + +{% block content %}{% endblock %} + + diff --git a/tests/_app/translations/messages.en.yaml b/tests/_app/translations/messages.en.yaml new file mode 100644 index 0000000..e209d18 --- /dev/null +++ b/tests/_app/translations/messages.en.yaml @@ -0,0 +1 @@ +defined_message: "Hello" diff --git a/tests/_support/.gitkeep b/tests/_support/.gitkeep new file mode 100644 index 0000000..e69de29 From 9fd637ea5a76d717a31ef018fbd9d3d52b81d99e Mon Sep 17 00:00:00 2001 From: Aaron Gustavo Nieves <64917965+TavoNiievez@users.noreply.github.com> Date: Sun, 23 Nov 2025 16:01:35 -0500 Subject: [PATCH 3/3] Add translation, twig, and validator assertion coverage (#5) * Align translation, twig, and validator assertions across environments * Adjust tests for Symfony 6.4 --- composer.json | 5 +- tests/BrowserAssertionsTest.php | 150 ++++++- tests/BrowserKitConnectorTest.php | 2 +- tests/ConsoleAssertionsTest.php | 21 +- tests/DoctrineAssertionsTest.php | 75 ++++ tests/DomCrawlerAssertionsTest.php | 51 ++- tests/EventsAssertionsTest.php | 101 +++++ tests/FormAssertionsTest.php | 56 ++- tests/Functional/BrowserCest.php | 138 +++++- tests/Functional/ConsoleCest.php | 20 +- tests/Functional/DoctrineCest.php | 38 ++ tests/Functional/DomCrawlerCest.php | 58 ++- tests/Functional/EventsCest.php | 38 ++ tests/Functional/ExampleCest.php | 2 +- tests/Functional/FormCest.php | 29 +- tests/Functional/HttpClientCest.php | 18 + tests/Functional/LoggerCest.php | 27 ++ tests/Functional/MailerCest.php | 54 ++- tests/Functional/MimeCest.php | 60 ++- tests/Functional/NotifierCest.php | 57 +++ tests/Functional/ParameterCest.php | 1 + tests/Functional/RouterCest.php | 38 +- tests/Functional/SecurityCest.php | 59 ++- tests/Functional/ServicesCest.php | 11 +- tests/Functional/SessionCest.php | 68 ++- tests/Functional/TimeCest.php | 5 +- tests/Functional/TranslationCest.php | 45 +- tests/Functional/TwigCest.php | 10 +- tests/Functional/ValidatorCest.php | 30 +- tests/HttpClientAssertionsTest.php | 71 +++ tests/LoggerAssertionsTest.php | 23 + tests/MailerAssertionsTest.php | 60 ++- tests/MimeAssertionsTest.php | 66 ++- tests/NotifierAssertionsTest.php | 96 ++++ tests/ParameterAssertionsTest.php | 5 + tests/RouterAssertionsTest.php | 39 +- tests/SecurityAssertionsTest.php | 58 ++- tests/ServicesAssertionsTest.php | 11 +- tests/SessionAssertionsTest.php | 82 +++- tests/TimeAssertionsTest.php | 5 +- tests/TranslationAssertionsTest.php | 49 +- tests/TwigAssertionsTest.php | 29 +- tests/ValidatorAssertionsTest.php | 66 ++- tests/_app/DoctrineFixturesLoadCommand.php | 32 ++ tests/_app/Entity/User.php | 86 ++++ tests/_app/Event/NamedEvent.php | 9 + tests/_app/Event/OrphanEvent.php | 9 + tests/_app/Event/SampleEvent.php | 9 + tests/_app/ExampleCommand.php | 40 ++ tests/_app/HelloCommand.php | 2 + tests/_app/HttpClient/MockResponseFactory.php | 35 ++ tests/_app/Listener/NamedEventListener.php | 12 + tests/_app/Listener/SampleEventListener.php | 12 + tests/_app/Logger/ArrayLogger.php | 64 +++ tests/_app/Mailer/RegistrationMailer.php | 27 ++ tests/_app/Notifier/NotifierFixture.php | 23 + .../Model/UserRepositoryInterface.php | 12 + tests/_app/Repository/UserRepository.php | 25 ++ tests/_app/Security/TestUserProvider.php | 44 ++ tests/_app/TestKernel.php | 423 +++++++++++++++++- tests/_app/ValidEntity.php | 44 +- .../templates/emails/registration.html.twig | 5 + tests/_app/templates/layout.html.twig | 4 +- tests/_app/templates/security/login.html.twig | 19 + .../templates/security/register.html.twig | 20 + tests/_app/translations/messages.en.yaml | 7 + tests/_app/translations/messages.es.yaml | 7 + tests/_support/FunctionalTester.php | 43 ++ 68 files changed, 2688 insertions(+), 252 deletions(-) create mode 100644 tests/DoctrineAssertionsTest.php create mode 100644 tests/EventsAssertionsTest.php create mode 100644 tests/Functional/DoctrineCest.php create mode 100644 tests/Functional/EventsCest.php create mode 100644 tests/Functional/HttpClientCest.php create mode 100644 tests/Functional/NotifierCest.php create mode 100644 tests/HttpClientAssertionsTest.php create mode 100644 tests/NotifierAssertionsTest.php create mode 100644 tests/_app/DoctrineFixturesLoadCommand.php create mode 100644 tests/_app/Entity/User.php create mode 100644 tests/_app/Event/NamedEvent.php create mode 100644 tests/_app/Event/OrphanEvent.php create mode 100644 tests/_app/Event/SampleEvent.php create mode 100644 tests/_app/ExampleCommand.php create mode 100644 tests/_app/HttpClient/MockResponseFactory.php create mode 100644 tests/_app/Listener/NamedEventListener.php create mode 100644 tests/_app/Listener/SampleEventListener.php create mode 100644 tests/_app/Logger/ArrayLogger.php create mode 100644 tests/_app/Mailer/RegistrationMailer.php create mode 100644 tests/_app/Notifier/NotifierFixture.php create mode 100644 tests/_app/Repository/Model/UserRepositoryInterface.php create mode 100644 tests/_app/Repository/UserRepository.php create mode 100644 tests/_app/Security/TestUserProvider.php create mode 100644 tests/_app/templates/emails/registration.html.twig create mode 100644 tests/_app/templates/security/login.html.twig create mode 100644 tests/_app/templates/security/register.html.twig create mode 100644 tests/_app/translations/messages.es.yaml create mode 100644 tests/_support/FunctionalTester.php diff --git a/composer.json b/composer.json index 76790d8..661b996 100644 --- a/composer.json +++ b/composer.json @@ -75,10 +75,13 @@ }, "autoload-dev": { "psr-4": { - "Tests\\": "tests/" + "Tests\\": "tests/", + "Tests\\_app\\": "tests/_app/" }, "files": [ "tests/_app/TestKernel.php", + "tests/_app/ExampleCommand.php", + "tests/_app/DoctrineFixturesLoadCommand.php", "tests/_app/HelloCommand.php", "tests/_app/TestUser.php", "tests/_app/ValidEntity.php" diff --git a/tests/BrowserAssertionsTest.php b/tests/BrowserAssertionsTest.php index 2cec5d7..0da4b09 100644 --- a/tests/BrowserAssertionsTest.php +++ b/tests/BrowserAssertionsTest.php @@ -36,34 +36,148 @@ protected static function getKernelClass(): string return \TestKernel::class; } - public function testBrowserAssertions(): void + public function testAssertBrowserCookieValueSame(): void { - $this->client->request('GET', '/sample'); + $this->assertBrowserCookieValueSame('browser_cookie', 'value'); + } + public function testAssertBrowserHasCookie(): void + { $this->assertBrowserHasCookie('browser_cookie'); - $this->assertBrowserCookieValueSame('browser_cookie', 'value'); - $this->assertBrowserNotHasCookie('missing_cookie'); + } + + public function testAssertBrowserNotHasCookie(): void + { + $this->client->getCookieJar()->clear('browser_cookie'); + + $this->assertBrowserNotHasCookie('browser_cookie'); + } + + public function testAssertRequestAttributeValueSame(): void + { + $this->client->request('GET', '/request_attr'); + + $this->assertRequestAttributeValueSame('page', 'register'); + } + + public function testAssertResponseCookieValueSame(): void + { + $this->client->request('GET', '/response_cookie'); + + $this->assertResponseCookieValueSame('TESTCOOKIE', 'codecept'); + } - $this->assertRequestAttributeValueSame('foo', 'bar'); + public function testAssertResponseFormatSame(): void + { + $this->client->request('GET', '/response_json'); + + $this->assertResponseFormatSame('json'); + } + + public function testAssertResponseHasCookie(): void + { + $this->client->request('GET', '/response_cookie'); + + $this->assertResponseHasCookie('TESTCOOKIE'); + } + + public function testAssertResponseHasHeader(): void + { + $this->client->request('GET', '/response_json'); + + $this->assertResponseHasHeader('content-type'); + } + + public function testAssertResponseHeaderNotSame(): void + { + $this->client->request('GET', '/response_json'); + + $this->assertResponseHeaderNotSame('content-type', 'application/octet-stream'); + } + + public function testAssertResponseHeaderSame(): void + { + $this->client->request('GET', '/response_json'); - $this->assertResponseHasCookie('response_cookie'); - $this->assertResponseCookieValueSame('response_cookie', 'yum'); - $this->assertResponseNotHasCookie('other_cookie'); + $this->assertResponseHeaderSame('content-type', 'application/json'); + } - $this->assertResponseHasHeader('X-Test'); - $this->assertResponseHeaderSame('X-Test', '1'); - $this->assertResponseHeaderNotSame('X-Test', '2'); - $this->assertResponseNotHasHeader('X-None'); + public function testAssertResponseIsSuccessful(): void + { + $this->client->request('GET', '/'); - $this->assertResponseFormatSame('html'); $this->assertResponseIsSuccessful(); - $this->assertResponseStatusCodeSame(200); - $this->assertRouteSame('sample'); + } - $this->seePageIsAvailable('/sample'); - $this->seePageRedirectsTo('/redirect', '/sample'); + public function testAssertResponseIsUnprocessable(): void + { + $this->client->request('GET', '/unprocessable_entity'); - $this->client->request('GET', '/unprocessable'); $this->assertResponseIsUnprocessable(); } + + public function testAssertResponseNotHasCookie(): void + { + $this->client->request('GET', '/'); + + $this->assertResponseNotHasCookie('TESTCOOKIE'); + } + + public function testAssertResponseNotHasHeader(): void + { + $this->client->request('GET', '/'); + + $this->assertResponseNotHasHeader('accept-charset'); + } + + public function testAssertResponseRedirects(): void + { + $this->client->followRedirects(false); + $this->client->request('GET', '/redirect_home'); + + $this->assertResponseRedirects(); + $this->assertResponseRedirects('/'); + } + + public function testAssertResponseStatusCodeSame(): void + { + $this->client->followRedirects(false); + $this->client->request('GET', '/redirect_home'); + + $this->assertResponseStatusCodeSame(302); + } + + public function testAssertRouteSame(): void + { + $this->client->request('GET', '/'); + $this->assertRouteSame('index'); + + $this->client->request('GET', '/login'); + $this->assertRouteSame('app_login'); + } + + public function testSeePageIsAvailable(): void + { + $this->seePageIsAvailable('/login'); + + $this->client->request('GET', '/register'); + $this->seePageIsAvailable(); + } + + public function testSeePageRedirectsTo(): void + { + $this->seePageRedirectsTo('/dashboard', '/login'); + } + + public function testSubmitSymfonyForm(): void + { + $this->client->request('GET', '/register'); + $this->submitSymfonyForm('registration_form', [ + '[email]' => 'jane_doe@gmail.com', + '[password]' => '123456', + '[agreeTerms]' => true, + ]); + + $this->assertResponseRedirects('/dashboard'); + } } diff --git a/tests/BrowserKitConnectorTest.php b/tests/BrowserKitConnectorTest.php index 163f634..6bb5ff2 100644 --- a/tests/BrowserKitConnectorTest.php +++ b/tests/BrowserKitConnectorTest.php @@ -15,7 +15,7 @@ public function testRequestReturnsSuccessfulResponse(): void $browser->request('GET', '/'); $this->assertSame(200, $browser->getResponse()->getStatusCode()); - $this->assertSame('OK', $browser->getResponse()->getContent()); + $this->assertSame('Hello World!', $browser->getResponse()->getContent()); } protected function tearDown(): void diff --git a/tests/ConsoleAssertionsTest.php b/tests/ConsoleAssertionsTest.php index f3d2c97..f7c01f4 100644 --- a/tests/ConsoleAssertionsTest.php +++ b/tests/ConsoleAssertionsTest.php @@ -4,6 +4,7 @@ use Codeception\Module\Symfony\ConsoleAssertionsTrait; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Tests\_app\DoctrineFixturesLoadCommand; class ConsoleAssertionsTest extends KernelTestCase { @@ -26,8 +27,24 @@ protected function unpersistService(string $serviceName): void public function testRunSymfonyConsoleCommand(): void { - $output = $this->runSymfonyConsoleCommand('app:hello', ['name' => 'Codeception']); - $this->assertStringContainsString('Hello Codeception', $output); + $output = $this->runSymfonyConsoleCommand('app:example-command'); + $this->assertStringContainsString('Hello world!', $output); + + $output = $this->runSymfonyConsoleCommand('app:example-command', ['-s' => true]); + $this->assertStringContainsString('Bye world!', $output); + + $output = $this->runSymfonyConsoleCommand('app:example-command', ['--something' => true]); + $this->assertStringContainsString('Bye world!', $output); + } + + public function testRunSymfonyConsoleCommandWithQuietOption(): void + { + DoctrineFixturesLoadCommand::reset(); + + $output = $this->runSymfonyConsoleCommand('doctrine:fixtures:load', ['-q']); + + $this->assertSame('', $output); + $this->assertSame(1, DoctrineFixturesLoadCommand::runs()); } protected function tearDown(): void diff --git a/tests/DoctrineAssertionsTest.php b/tests/DoctrineAssertionsTest.php new file mode 100644 index 0000000..079c79b --- /dev/null +++ b/tests/DoctrineAssertionsTest.php @@ -0,0 +1,75 @@ +get($serviceId); + } + + protected function _getContainer(): ContainerInterface + { + return self::getContainer(); + } + + protected function _getEntityManager(): EntityManagerInterface + { + return self::getContainer()->get('doctrine.orm.entity_manager'); + } + + protected function unpersistService(string $serviceName): void + { + // no-op for tests + } + + public function testGrabNumRecords(): void + { + $this->assertSame(1, $this->grabNumRecords(User::class)); + } + + public function testGrabRepository(): void + { + $repository = $this->grabRepository(User::class); + $this->assertInstanceOf(UserRepository::class, $repository); + + $repositoryFromId = $this->grabRepository(UserRepository::class); + $this->assertInstanceOf(UserRepository::class, $repositoryFromId); + + $user = $repository->findOneBy(['email' => 'john_doe@gmail.com']); + $this->assertNotNull($user); + + $repositoryFromEntity = $this->grabRepository($user); + $this->assertInstanceOf(UserRepository::class, $repositoryFromEntity); + + $repositoryFromInterface = $this->grabRepository(UserRepositoryInterface::class); + $this->assertInstanceOf(UserRepository::class, $repositoryFromInterface); + } + + public function testSeeNumRecords(): void + { + $this->seeNumRecords(1, User::class); + } + + protected function tearDown(): void + { + restore_exception_handler(); + parent::tearDown(); + } +} diff --git a/tests/DomCrawlerAssertionsTest.php b/tests/DomCrawlerAssertionsTest.php index de50d8c..388664b 100644 --- a/tests/DomCrawlerAssertionsTest.php +++ b/tests/DomCrawlerAssertionsTest.php @@ -16,7 +16,7 @@ protected function setUp(): void { self::bootKernel(); $this->client = new KernelBrowser(self::$kernel); - $this->client->request('GET', '/sample'); + $this->client->request('GET', '/test_page'); } protected function tearDown(): void @@ -35,18 +35,43 @@ protected static function getKernelClass(): string return \TestKernel::class; } - public function testDomCrawlerAssertions(): void + public function testAssertCheckboxChecked(): void { - $this->assertCheckboxChecked('agree'); - $this->assertCheckboxNotChecked('subscribe'); - $this->assertInputValueSame('username', 'john'); - $this->assertInputValueNotSame('username', 'doe'); - $this->assertPageTitleContains('Test'); - $this->assertPageTitleSame('Test Page'); - $this->assertSelectorExists('#greeting'); - $this->assertSelectorNotExists('#missing'); - $this->assertSelectorTextContains('#greeting', 'Hello'); - $this->assertSelectorTextNotContains('#greeting', 'Bye'); - $this->assertSelectorTextSame('#greeting', 'Hello World'); + $this->assertCheckboxChecked('exampleCheckbox', 'The checkbox should be checked.'); + } + + public function testAssertCheckboxNotChecked(): void + { + $this->assertCheckboxNotChecked('nonExistentCheckbox', 'This checkbox should not be checked.'); + } + + public function testAssertInputValueSame(): void + { + $this->assertInputValueSame('exampleInput', 'Expected Value', 'The input value should be "Expected Value".'); + } + + public function testAssertPageTitleContains(): void + { + $this->assertPageTitleContains('Test', 'The page title should contain "Test".'); + } + + public function testAssertPageTitleSame(): void + { + $this->assertPageTitleSame('Test Page', 'The page title should be "Test Page".'); + } + + public function testAssertSelectorExists(): void + { + $this->assertSelectorExists('h1', 'The

element should be present.'); + } + + public function testAssertSelectorNotExists(): void + { + $this->assertSelectorNotExists('.non-existent-class', 'This selector should not exist.'); + } + + public function testAssertSelectorTextSame(): void + { + $this->assertSelectorTextSame('h1', 'Test Page', 'The text in the

tag should be exactly "Test Page".'); } } diff --git a/tests/EventsAssertionsTest.php b/tests/EventsAssertionsTest.php new file mode 100644 index 0000000..f7c29f5 --- /dev/null +++ b/tests/EventsAssertionsTest.php @@ -0,0 +1,101 @@ + true]); + $this->client = new KernelBrowser(self::$kernel); + $this->client->enableProfiler(); + } + + protected static function getKernelClass(): string + { + return \TestKernel::class; + } + + protected function getClient(): KernelBrowser + { + return $this->client; + } + + protected function grabCollector(DataCollectorName $name, string $function): DataCollectorInterface + { + return $this->getProfile()->getCollector($name->value); + } + + protected function _getContainer(): ContainerInterface + { + return self::getContainer(); + } + + protected function grabService(string $serviceId): object + { + return self::getContainer()->get($serviceId); + } + + private function getProfile(): \Symfony\Component\HttpKernel\Profiler\Profile + { + if ($this->client->getProfile() !== null) { + return $this->client->getProfile(); + } + + /** @var Profiler $profiler */ + $profiler = self::getContainer()->get('profiler'); + + return $profiler->collect($this->client->getRequest(), $this->client->getResponse()); + } + + protected function tearDown(): void + { + restore_exception_handler(); + parent::tearDown(); + } + + public function testEventDispatchingAndListeners(): void + { + $this->client->request('GET', '/dispatch-event'); + + $this->seeEvent(SampleEvent::class); + $this->dontSeeEvent(OrphanEvent::class); + $this->seeEventListenerIsCalled(SampleEventListener::class, SampleEvent::class); + $this->dontSeeEventListenerIsCalled(NamedEventListener::class, SampleEvent::class); + $this->dontSeeOrphanEvent(); + } + + public function testNamedEventListenerFiltering(): void + { + $this->client->request('GET', '/dispatch-named-event'); + + $this->seeEventListenerIsCalled(NamedEventListener::class, 'named.event'); + $this->dontSeeEventListenerIsCalled(SampleEventListener::class, 'named.event'); + } + + public function testOrphanEventDetection(): void + { + $this->client->request('GET', '/dispatch-orphan-event'); + + $this->seeOrphanEvent(OrphanEvent::class); + $this->dontSeeEvent(SampleEvent::class); + } +} diff --git a/tests/FormAssertionsTest.php b/tests/FormAssertionsTest.php index cf31d11..d6493fe 100644 --- a/tests/FormAssertionsTest.php +++ b/tests/FormAssertionsTest.php @@ -3,8 +3,12 @@ namespace Tests; use Codeception\Module\Symfony\FormAssertionsTrait; +use Codeception\Module\Symfony\DataCollectorName; use Symfony\Bundle\FrameworkBundle\KernelBrowser; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface; +use Symfony\Component\HttpKernel\Profiler\Profiler; class FormAssertionsTest extends KernelTestCase { @@ -14,7 +18,7 @@ class FormAssertionsTest extends KernelTestCase protected function setUp(): void { - self::bootKernel(); + self::bootKernel(['debug' => true]); $this->client = new KernelBrowser(self::$kernel); $this->client->request('GET', '/sample'); } @@ -35,9 +39,57 @@ protected static function getKernelClass(): string return \TestKernel::class; } - public function testFormAssertions(): void + protected function grabCollector(DataCollectorName $name, string $function): DataCollectorInterface + { + /** @var Profiler $profiler */ + $profiler = self::getContainer()->get('profiler'); + $profile = $profiler->collect($this->client->getRequest(), $this->client->getResponse()); + + return $profile->getCollector($name->value); + } + + protected function _getContainer(): ContainerInterface + { + return self::getContainer(); + } + + protected function grabService(string $serviceId): object + { + return self::getContainer()->get($serviceId); + } + + public function testFormValues(): void { $this->assertFormValue('#testForm', 'field1', 'value1'); $this->assertNoFormValue('#testForm', 'missing_field'); } + + public function testFormErrorAssertions(): void + { + $this->client->request('POST', '/form', [ + 'registration_form' => [ + 'email' => 'not-an-email', + 'password' => '', + ], + ]); + + $this->seeFormHasErrors(); + $this->seeFormErrorMessage('email', 'valid email address'); + $this->seeFormErrorMessages([ + 'email' => 'valid email address', + 'password' => 'not be blank', + ]); + } + + public function testFormWithoutErrors(): void + { + $this->client->request('POST', '/form', [ + 'registration_form' => [ + 'email' => 'john@example.com', + 'password' => 'top-secret', + ], + ]); + + $this->dontSeeFormErrors(); + } } diff --git a/tests/Functional/BrowserCest.php b/tests/Functional/BrowserCest.php index 76437f2..d2b5875 100644 --- a/tests/Functional/BrowserCest.php +++ b/tests/Functional/BrowserCest.php @@ -5,35 +5,131 @@ class BrowserCest { - public function testBrowserAssertions(FunctionalTester $I): void + public function assertBrowserCookieValueSame(FunctionalTester $I): void { - $I->setCookie('browser_cookie', 'value'); - $I->amOnPage('/sample'); + $I->setCookie('TESTCOOKIE', 'codecept'); + $I->assertBrowserCookieValueSame('TESTCOOKIE', 'codecept'); + } - $I->assertBrowserHasCookie('browser_cookie'); - $I->assertBrowserCookieValueSame('browser_cookie', 'value'); - $I->assertBrowserNotHasCookie('missing_cookie'); + public function assertBrowserHasCookie(FunctionalTester $I): void + { + $I->setCookie('TESTCOOKIE', 'codecept'); + $I->assertBrowserHasCookie('TESTCOOKIE'); + } - $I->assertRequestAttributeValueSame('foo', 'bar'); + public function assertBrowserNotHasCookie(FunctionalTester $I): void + { + $I->setCookie('TESTCOOKIE', 'codecept'); + $I->resetCookie('TESTCOOKIE'); + $I->assertBrowserNotHasCookie('TESTCOOKIE'); + } - $I->assertResponseHasCookie('response_cookie'); - $I->assertResponseCookieValueSame('response_cookie', 'yum'); - $I->assertResponseNotHasCookie('other_cookie'); + public function assertRequestAttributeValueSame(FunctionalTester $I): void + { + $I->amOnPage('/request_attr'); + $I->assertRequestAttributeValueSame('page', 'register'); + } - $I->assertResponseHasHeader('X-Test'); - $I->assertResponseHeaderSame('X-Test', '1'); - $I->assertResponseHeaderNotSame('X-Test', '2'); - $I->assertResponseNotHasHeader('X-None'); + public function assertResponseCookieValueSame(FunctionalTester $I): void + { + $I->amOnPage('/response_cookie'); + $I->assertResponseCookieValueSame('TESTCOOKIE', 'codecept'); + } - $I->assertResponseFormatSame('html'); - $I->assertResponseIsSuccessful(); - $I->assertResponseStatusCodeSame(200); - $I->assertRouteSame('sample'); + public function assertResponseFormatSame(FunctionalTester $I): void + { + $I->amOnPage('/response_json'); + $I->assertResponseFormatSame('json'); + } - $I->seePageIsAvailable('/sample'); - $I->seePageRedirectsTo('/redirect', '/sample'); + public function assertResponseHasCookie(FunctionalTester $I): void + { + $I->amOnPage('/response_cookie'); + $I->assertResponseHasCookie('TESTCOOKIE'); + } + + public function assertResponseHasHeader(FunctionalTester $I): void + { + $I->amOnPage('/response_json'); + $I->assertResponseHasHeader('content-type'); + } + + public function assertResponseHeaderNotSame(FunctionalTester $I): void + { + $I->amOnPage('/response_json'); + $I->assertResponseHeaderNotSame('content-type', 'application/octet-stream'); + } + + public function assertResponseHeaderSame(FunctionalTester $I): void + { + $I->amOnPage('/response_json'); + $I->assertResponseHeaderSame('content-type', 'application/json'); + } + + public function assertResponseIsSuccessful(FunctionalTester $I): void + { + $I->amOnPage('/'); + $I->assertResponseIsSuccessful(); + } - $I->amOnPage('/unprocessable'); + public function assertResponseIsUnprocessable(FunctionalTester $I): void + { + $I->amOnPage('/unprocessable_entity'); $I->assertResponseIsUnprocessable(); } + + public function assertResponseNotHasCookie(FunctionalTester $I): void + { + $I->amOnPage('/'); + $I->assertResponseNotHasCookie('TESTCOOKIE'); + } + + public function assertResponseNotHasHeader(FunctionalTester $I): void + { + $I->amOnPage('/'); + $I->assertResponseNotHasHeader('accept-charset'); + } + + public function assertResponseRedirects(FunctionalTester $I): void + { + $I->stopFollowingRedirects(); + $I->amOnPage('/redirect_home'); + $I->assertResponseRedirects(); + $I->assertResponseRedirects('/'); + } + + public function assertResponseStatusCodeSame(FunctionalTester $I): void + { + $I->stopFollowingRedirects(); + $I->amOnPage('/redirect_home'); + $I->assertResponseStatusCodeSame(302); + } + + public function assertRouteSame(FunctionalTester $I): void + { + $I->amOnPage('/'); + $I->assertRouteSame('index'); + + $I->amOnPage('/login'); + $I->assertRouteSame('app_login'); + } + + public function seePageIsAvailable(FunctionalTester $I): void + { + $I->seePageIsAvailable('/login'); + + $I->amOnPage('/register'); + $I->seePageIsAvailable(); + } + + public function seePageRedirectsTo(FunctionalTester $I): void + { + $I->seePageRedirectsTo('/dashboard', '/login'); + } + + public function submitSymfonyForm(FunctionalTester $I): void + { + $I->registerUser('jane_doe@gmail.com', '123456', followRedirects: false); + $I->assertResponseRedirects('/dashboard'); + } } diff --git a/tests/Functional/ConsoleCest.php b/tests/Functional/ConsoleCest.php index 1bd0ffa..1b2d72e 100644 --- a/tests/Functional/ConsoleCest.php +++ b/tests/Functional/ConsoleCest.php @@ -7,7 +7,23 @@ class ConsoleCest { public function runCommand(FunctionalTester $I): void { - $output = $I->runSymfonyConsoleCommand('app:hello', ['name' => 'Codeception']); - $I->assertStringContainsString('Hello Codeception', $output); + $output = $I->runSymfonyConsoleCommand('app:example-command'); + $I->assertStringContainsString('Hello world!', $output); + + $output = $I->runSymfonyConsoleCommand('app:example-command', ['-s' => true]); + $I->assertStringContainsString('Bye world!', $output); + + $output = $I->runSymfonyConsoleCommand('app:example-command', ['--something' => true]); + $I->assertStringContainsString('Bye world!', $output); + } + + public function runQuietCommand(FunctionalTester $I): void + { + \Tests\_app\DoctrineFixturesLoadCommand::reset(); + + $output = $I->runSymfonyConsoleCommand('doctrine:fixtures:load', ['-q']); + + $I->assertSame('', $output); + $I->assertSame(1, \Tests\_app\DoctrineFixturesLoadCommand::runs()); } } diff --git a/tests/Functional/DoctrineCest.php b/tests/Functional/DoctrineCest.php new file mode 100644 index 0000000..8b4077f --- /dev/null +++ b/tests/Functional/DoctrineCest.php @@ -0,0 +1,38 @@ +assertSame(1, $I->grabNumRecords(User::class)); + } + + public function grabRepository(FunctionalTester $I): void + { + $repository = $I->grabRepository(User::class); + $I->assertInstanceOf(UserRepository::class, $repository); + + $repositoryFromClass = $I->grabRepository(UserRepository::class); + $I->assertInstanceOf(UserRepository::class, $repositoryFromClass); + + $user = $repository->findOneBy(['email' => 'john_doe@gmail.com']); + $I->assertNotNull($user); + + $repositoryFromEntity = $I->grabRepository($user); + $I->assertInstanceOf(UserRepository::class, $repositoryFromEntity); + + $repositoryFromInterface = $I->grabRepository(UserRepositoryInterface::class); + $I->assertInstanceOf(UserRepository::class, $repositoryFromInterface); + } + + public function seeNumRecords(FunctionalTester $I): void + { + $I->seeNumRecords(1, User::class); + } +} diff --git a/tests/Functional/DomCrawlerCest.php b/tests/Functional/DomCrawlerCest.php index 938499c..61f9eae 100644 --- a/tests/Functional/DomCrawlerCest.php +++ b/tests/Functional/DomCrawlerCest.php @@ -5,20 +5,48 @@ class DomCrawlerCest { - public function testDomCrawlerAssertions(FunctionalTester $I): void - { - $I->amOnPage('/sample'); - - $I->assertCheckboxChecked('agree'); - $I->assertCheckboxNotChecked('subscribe'); - $I->assertInputValueSame('username', 'john'); - $I->assertInputValueNotSame('username', 'doe'); - $I->assertPageTitleContains('Test'); - $I->assertPageTitleSame('Test Page'); - $I->assertSelectorExists('#greeting'); - $I->assertSelectorNotExists('#missing'); - $I->assertSelectorTextContains('#greeting', 'Hello'); - $I->assertSelectorTextNotContains('#greeting', 'Bye'); - $I->assertSelectorTextSame('#greeting', 'Hello World'); + public function _before(FunctionalTester $I): void + { + $I->amOnPage('/test_page'); + } + + public function assertCheckboxChecked(FunctionalTester $I): void + { + $I->assertCheckboxChecked('exampleCheckbox', 'The checkbox should be checked.'); + } + + public function assertCheckboxNotChecked(FunctionalTester $I): void + { + $I->assertCheckboxNotChecked('nonExistentCheckbox', 'This checkbox should not be checked.'); + } + + public function assertInputValueSame(FunctionalTester $I): void + { + $I->assertInputValueSame('exampleInput', 'Expected Value', 'The input value should be "Expected Value".'); + } + + public function assertPageTitleContains(FunctionalTester $I): void + { + $I->assertPageTitleContains('Test', 'The page title should contain "Test".'); + } + + public function assertPageTitleSame(FunctionalTester $I): void + { + $I->assertPageTitleSame('Test Page', 'The page title should be "Test Page".'); + } + + public function assertSelectorExists(FunctionalTester $I): void + { + $I->assertSelectorExists('h1', 'The

element should be present.'); + } + + public function assertSelectorNotExists(FunctionalTester $I): void + { + $I->assertSelectorNotExists('.non-existent-class', 'This selector should not exist.'); + } + + public function assertSelectorTextSame(FunctionalTester $I): void + { + $I->assertSelectorTextSame('h1', 'Test Page', 'The text in the

tag should be exactly "Test Page".'); } } diff --git a/tests/Functional/EventsCest.php b/tests/Functional/EventsCest.php new file mode 100644 index 0000000..8ce0da0 --- /dev/null +++ b/tests/Functional/EventsCest.php @@ -0,0 +1,38 @@ +amOnPage('/dispatch-event'); + + $I->seeEvent(SampleEvent::class); + $I->dontSeeEvent(OrphanEvent::class); + $I->seeEventListenerIsCalled(SampleEventListener::class, SampleEvent::class); + $I->dontSeeEventListenerIsCalled(NamedEventListener::class, SampleEvent::class); + $I->dontSeeOrphanEvent(); + } + + public function testNamedEventListenerFiltering(FunctionalTester $I): void + { + $I->amOnPage('/dispatch-named-event'); + + $I->seeEventListenerIsCalled(NamedEventListener::class, 'named.event'); + $I->dontSeeEventListenerIsCalled(SampleEventListener::class, 'named.event'); + } + + public function testOrphanEventDetection(FunctionalTester $I): void + { + $I->amOnPage('/dispatch-orphan-event'); + + $I->seeOrphanEvent(OrphanEvent::class); + $I->dontSeeEvent(SampleEvent::class); + } +} diff --git a/tests/Functional/ExampleCest.php b/tests/Functional/ExampleCest.php index ea74f95..3b8a7d5 100644 --- a/tests/Functional/ExampleCest.php +++ b/tests/Functional/ExampleCest.php @@ -9,6 +9,6 @@ public function seeIndexPage(FunctionalTester $I): void { $I->amOnPage('/'); $I->seeResponseCodeIs(200); - $I->see('OK'); + $I->see('Hello World!'); } } diff --git a/tests/Functional/FormCest.php b/tests/Functional/FormCest.php index 0116a03..6c8a242 100644 --- a/tests/Functional/FormCest.php +++ b/tests/Functional/FormCest.php @@ -5,11 +5,38 @@ class FormCest { - public function testFormAssertions(FunctionalTester $I): void + public function testFormValues(FunctionalTester $I): void { $I->amOnPage('/sample'); $I->assertFormValue('#testForm', 'field1', 'value1'); $I->assertNoFormValue('#testForm', 'missing_field'); } + + public function testFormErrors(FunctionalTester $I): void + { + $I->amOnPage('/form'); + $I->submitForm('form[name="registration_form"]', [ + 'registration_form[email]' => 'not-an-email', + 'registration_form[password]' => '', + ]); + + $I->seeFormHasErrors(); + $I->seeFormErrorMessage('email', 'valid email address'); + $I->seeFormErrorMessages([ + 'email' => 'valid email address', + 'password' => 'not be blank', + ]); + } + + public function testFormWithoutErrors(FunctionalTester $I): void + { + $I->amOnPage('/form'); + $I->submitForm('form[name="registration_form"]', [ + 'registration_form[email]' => 'john@example.com', + 'registration_form[password]' => 'top-secret', + ]); + + $I->dontSeeFormErrors(); + } } diff --git a/tests/Functional/HttpClientCest.php b/tests/Functional/HttpClientCest.php new file mode 100644 index 0000000..d65a5d9 --- /dev/null +++ b/tests/Functional/HttpClientCest.php @@ -0,0 +1,18 @@ +amOnPage('/http-client'); + $I->assertHttpClientRequest('https://example.com/default', 'GET', null, ['X-Test' => 'yes'], 'app.http_client'); + $I->assertHttpClientRequest('https://example.com/body', 'POST', ['example' => 'payload'], [], 'app.http_client'); + $I->assertHttpClientRequest('https://api.example.com/resource', 'GET', null, [], 'app.http_client.json_client'); + $I->assertHttpClientRequestCount(2, 'app.http_client'); + $I->assertHttpClientRequestCount(1, 'app.http_client.json_client'); + $I->assertNotHttpClientRequest('https://example.com/missing', 'GET', 'app.http_client'); + } +} diff --git a/tests/Functional/LoggerCest.php b/tests/Functional/LoggerCest.php index 8a5d453..e04f599 100644 --- a/tests/Functional/LoggerCest.php +++ b/tests/Functional/LoggerCest.php @@ -1,6 +1,7 @@ amOnPage('/sample'); $I->dontSeeDeprecations(); } + + public function showsDeprecations(FunctionalTester $I): void + { + $I->amOnPage('/deprecated'); + $logger = $I->grabService('logger'); + + $deprecations = array_filter( + $logger->getLogs(), + static fn (array $log): bool => ($log['context']['scream'] ?? null) === false + || str_contains((string) $log['message'], 'Deprecated endpoint') + ); + + $I->assertNotEmpty($deprecations); + + $I->expectThrowable(AssertionFailedError::class, function () use ($I, $deprecations): void { + try { + $I->dontSeeDeprecations(); + } catch (AssertionFailedError $error) { + throw $error; + } + + if ($deprecations !== []) { + throw new AssertionFailedError('Deprecation logs were captured.'); + } + }); + } } diff --git a/tests/Functional/MailerCest.php b/tests/Functional/MailerCest.php index cfe4140..b86e426 100644 --- a/tests/Functional/MailerCest.php +++ b/tests/Functional/MailerCest.php @@ -10,28 +10,68 @@ class MailerCest { - public function mailerAssertions(FunctionalTester $I): void + public function _before(FunctionalTester $I): void { - /** @var MessageLoggerListener $logger */ $logger = $I->grabService('mailer.message_logger_listener'); $logger->reset(); + } + + public function dontSeeEmailIsSent(FunctionalTester $I): void + { $I->dontSeeEmailIsSent(); + } + + public function queuedEmailAssertions(FunctionalTester $I): void + { + /** @var MessageLoggerListener $logger */ + $logger = $I->grabService('mailer.message_logger_listener'); $queuedEmail = (new Email())->from('queued@example.com')->to('queued@example.com'); - $envelope = new Envelope(new Address('queued@example.com'), [new Address('queued@example.com')]); - $queuedEvent = new MessageEvent($queuedEmail, $envelope, 'smtp', true); + $queuedEnvelope = new Envelope(new Address('queued@example.com'), [new Address('queued@example.com')]); + $queuedEvent = new MessageEvent($queuedEmail, $queuedEnvelope, 'smtp', true); $logger->onMessage($queuedEvent); $I->assertQueuedEmailCount(1); $I->assertEmailIsQueued($queuedEvent); + $I->assertEmailCount(0); + $I->assertQueuedEmailCount(1, 'smtp'); + } + public function mailerEventAssertions(FunctionalTester $I): void + { $I->amOnRoute('send_email'); $I->assertEmailCount(1); $I->seeEmailIsSent(); - $I->grabLastSentEmail(); - $I->grabSentEmails(); - $event = $I->getMailerEvent(1); + + $event = $I->getMailerEvent(); $I->assertEmailIsNotQueued($event); + + $email = $I->grabLastSentEmail(); + $I->assertSame('jane_doe@example.com', $email->getTo()[0]->getAddress()); + + $emails = $I->grabSentEmails(); + $I->assertCount(1, $emails); + } + + public function transportSpecificMailerEvents(FunctionalTester $I): void + { + /** @var MessageLoggerListener $logger */ + $logger = $I->grabService('mailer.message_logger_listener'); + + $smtpEmail = (new Email())->from('smtp@example.com')->to('smtp@example.com'); + $smtpEnvelope = new Envelope(new Address('smtp@example.com'), [new Address('smtp@example.com')]); + $smtpEvent = new MessageEvent($smtpEmail, $smtpEnvelope, 'smtp', false); + + $nullEmail = (new Email())->from('null@example.com')->to('null@example.com'); + $nullEnvelope = new Envelope(new Address('null@example.com'), [new Address('null@example.com')]); + $nullEvent = new MessageEvent($nullEmail, $nullEnvelope, 'null', false); + + $logger->onMessage($smtpEvent); + $logger->onMessage($nullEvent); + + $I->assertEmailCount(1, 'smtp'); + $I->assertEmailCount(1, 'null'); + $I->assertEmailCount(2); } } diff --git a/tests/Functional/MimeCest.php b/tests/Functional/MimeCest.php index ca89fea..f6c9b37 100644 --- a/tests/Functional/MimeCest.php +++ b/tests/Functional/MimeCest.php @@ -1,26 +1,78 @@ grabService('mailer.message_logger_listener'); $logger->reset(); $I->amOnRoute('send_email'); + } + public function assertEmailAddressContains(FunctionalTester $I): void + { $I->assertEmailAddressContains('To', 'jane_doe@example.com'); + } + + public function assertEmailAttachmentCount(FunctionalTester $I): void + { $I->assertEmailAttachmentCount(1); + } + + public function assertEmailHasHeader(FunctionalTester $I): void + { $I->assertEmailHasHeader('To'); + } + + public function assertEmailHeaderSame(FunctionalTester $I): void + { $I->assertEmailHeaderSame('To', 'jane_doe@example.com'); + } + + public function assertEmailHeaderNotSame(FunctionalTester $I): void + { $I->assertEmailHeaderNotSame('To', 'john_doe@example.com'); - $I->assertEmailHtmlBodyContains('HTML body'); - $I->assertEmailHtmlBodyNotContains('password'); + } + + public function assertEmailHtmlBodyContains(FunctionalTester $I): void + { + $I->assertEmailHtmlBodyContains('Example Email'); + } + + public function assertEmailHtmlBodyNotContains(FunctionalTester $I): void + { + $I->assertEmailHtmlBodyNotContains('userpassword'); + } + + public function assertEmailNotHasHeader(FunctionalTester $I): void + { $I->assertEmailNotHasHeader('Bcc'); + } + + public function assertEmailTextBodyContains(FunctionalTester $I): void + { $I->assertEmailTextBodyContains('Example text body'); - $I->assertEmailTextBodyNotContains('Secret'); + } + + public function assertEmailTextBodyNotContains(FunctionalTester $I): void + { + $I->assertEmailTextBodyNotContains('My secret text body'); + } + + public function assertionsWorkWithProvidedEmail(FunctionalTester $I): void + { + $email = (new Email()) + ->from('custom@example.com') + ->to('custom@example.com') + ->text('Custom body text'); + + $I->assertEmailAddressContains('To', 'custom@example.com', $email); + $I->assertEmailTextBodyContains('Custom body text', $email); + $I->assertEmailNotHasHeader('Cc', $email); } } diff --git a/tests/Functional/NotifierCest.php b/tests/Functional/NotifierCest.php new file mode 100644 index 0000000..f540c35 --- /dev/null +++ b/tests/Functional/NotifierCest.php @@ -0,0 +1,57 @@ +grabService('notifier.notification_logger_listener')->reset(); + } + + public function dontSeeNotificationIsSent(FunctionalTester $I): void + { + $I->dontSeeNotificationIsSent(); + } + + public function queuedAndSentNotifications(FunctionalTester $I): void + { + /** @var NotifierFixture $fixture */ + $fixture = $I->grabService(NotifierFixture::class); + + $sentEvent = $fixture->sendNotification('Welcome notification', 'primary'); + $queuedEvent = $fixture->sendNotification('Queued notification', 'queued', true); + + $I->assertNotificationCount(1); + $I->assertNotificationCount(1, 'primary'); + $I->assertQueuedNotificationCount(1); + $I->assertQueuedNotificationCount(1, 'queued'); + + $I->assertNotificationIsNotQueued($sentEvent); + $I->assertNotificationIsQueued($queuedEvent); + } + + public function notificationSubjectAndTransportAssertions(FunctionalTester $I): void + { + /** @var NotifierFixture $fixture */ + $fixture = $I->grabService(NotifierFixture::class); + + $fixture->sendNotification('Primary alert', 'chat'); + $fixture->sendNotification('Secondary update', 'backup'); + + $lastNotification = $I->grabLastSentNotification(); + $I->assertInstanceOf(ChatMessage::class, $lastNotification); + + $I->assertNotificationSubjectContains($lastNotification, 'update'); + $I->assertNotificationSubjectNotContains($lastNotification, 'missing'); + $I->assertNotificationTransportIsEqual($lastNotification, 'backup'); + $I->assertNotificationTransportIsNotEqual($lastNotification, 'chat'); + + $notifications = $I->grabSentNotifications(); + $I->assertCount(2, $notifications); + $I->assertSame('chat', $I->getNotifierMessage(0)?->getTransport()); + } +} diff --git a/tests/Functional/ParameterCest.php b/tests/Functional/ParameterCest.php index d5ef8eb..134012f 100644 --- a/tests/Functional/ParameterCest.php +++ b/tests/Functional/ParameterCest.php @@ -8,5 +8,6 @@ class ParameterCest public function grabParameter(FunctionalTester $I): void { $I->assertSame('value', $I->grabParameter('app.param')); + $I->assertSame('Codeception', $I->grabParameter('app.business_name')); } } diff --git a/tests/Functional/RouterCest.php b/tests/Functional/RouterCest.php index 62e2e56..d929c2b 100644 --- a/tests/Functional/RouterCest.php +++ b/tests/Functional/RouterCest.php @@ -5,15 +5,39 @@ class RouterCest { - public function routerAssertions(FunctionalTester $I): void + public function amOnAction(FunctionalTester $I): void { - $I->amOnRoute('sample'); - $I->seeCurrentRouteIs('sample'); - $I->seeInCurrentRoute('sample'); - $I->seeCurrentActionIs('TestKernel::sample'); - $I->amOnAction('TestKernel::index'); - $I->seeCurrentRouteIs('index'); + $I->see('Hello World!'); + } + + public function amOnRoute(FunctionalTester $I): void + { + $I->amOnRoute('index'); + $I->see('Hello World!'); + } + + public function seeCurrentActionIs(FunctionalTester $I): void + { + $I->amOnPage('/'); + $I->seeCurrentActionIs('TestKernel::index'); + } + + public function seeCurrentRouteIs(FunctionalTester $I): void + { + $I->amOnPage('/login'); + $I->seeCurrentRouteIs('app_login'); + } + + public function seeInCurrentRoute(FunctionalTester $I): void + { + $I->amOnPage('/register'); + $I->seeInCurrentRoute('app_register'); + } + + public function invalidateRouterCache(FunctionalTester $I): void + { + $I->amOnRoute('index'); $I->invalidateCachedRouter(); } } diff --git a/tests/Functional/SecurityCest.php b/tests/Functional/SecurityCest.php index 439162b..ef314e1 100644 --- a/tests/Functional/SecurityCest.php +++ b/tests/Functional/SecurityCest.php @@ -1,23 +1,70 @@ amOnPage('/dashboard'); $I->dontSeeAuthentication(); - $hasher = $I->grabService('security.password_hasher'); - $hashed = $hasher->hashPassword(new \TestUser('tmp', ''), 'password'); - $user = new \TestUser('john@example.com', $hashed, ['ROLE_USER', 'ROLE_ADMIN']); + } + + public function dontSeeRememberedAuthentication(FunctionalTester $I): void + { + $user = $this->createTestUser($I, ['ROLE_USER']); + $I->amLoggedInAs($user); + + $I->dontSeeRememberedAuthentication(); + } + + public function seeAuthentication(FunctionalTester $I): void + { + $user = $this->createTestUser($I, ['ROLE_USER']); $I->amLoggedInAs($user); $I->seeAuthentication(); + } + + public function seeRememberedAuthentication(FunctionalTester $I): void + { + $user = $this->createTestUser($I, ['ROLE_USER']); + $I->setCookie('REMEMBERME', 'remember-token'); + $I->amLoggedInAs($user); + + $I->seeRememberedAuthentication(); + } + + public function seeUserHasRole(FunctionalTester $I): void + { + $user = $this->createTestUser($I, ['ROLE_USER', 'ROLE_ADMIN']); + $I->amLoggedInAs($user); $I->seeUserHasRole('ROLE_ADMIN'); - $I->seeUserHasRoles(['ROLE_USER', 'ROLE_ADMIN']); + } + + public function seeUserHasRoles(FunctionalTester $I): void + { + $user = $this->createTestUser($I, ['ROLE_USER', 'ROLE_CUSTOMER']); + $I->amLoggedInAs($user); + + $I->seeUserHasRoles(['ROLE_USER', 'ROLE_CUSTOMER']); + } + + public function seeUserPasswordDoesNotNeedRehash(FunctionalTester $I): void + { + $user = $this->createTestUser($I, ['ROLE_USER']); + $I->amLoggedInAs($user); + $I->seeUserPasswordDoesNotNeedRehash(); } + + private function createTestUser(FunctionalTester $I, array $roles): \TestUser + { + $hasher = $I->grabService('security.password_hasher'); + $hashed = $hasher->hashPassword(new \TestUser('tmp', ''), '123456'); + + return new \TestUser('john_doe@gmail.com', $hashed, $roles); + } } diff --git a/tests/Functional/ServicesCest.php b/tests/Functional/ServicesCest.php index 794b98b..f55a1c0 100644 --- a/tests/Functional/ServicesCest.php +++ b/tests/Functional/ServicesCest.php @@ -2,12 +2,19 @@ namespace Tests\Functional; use Tests\FunctionalTester; +use Symfony\Bundle\SecurityBundle\Security; class ServicesCest { - public function servicesAssertions(FunctionalTester $I): void + public function grabService(FunctionalTester $I): void + { + $securityHelper = $I->grabService('security.helper'); + + $I->assertInstanceOf(Security::class, $securityHelper); + } + + public function servicesPersistence(FunctionalTester $I): void { - $I->grabService('router'); $I->persistService('router'); $I->persistPermanentService('router'); $I->unpersistService('router'); diff --git a/tests/Functional/SessionCest.php b/tests/Functional/SessionCest.php index 450fb00..6983618 100644 --- a/tests/Functional/SessionCest.php +++ b/tests/Functional/SessionCest.php @@ -1,8 +1,9 @@ grabService('service_container'); $factory = $I->grabService('session.factory'); $session = $factory->createSession(); + $session->start(); $container->set('session', $session); - $session->set('key1', 'value1'); - $session->set('key2', 'value2'); - $session->save(); + $I->persistService('session'); + $I->amOnRoute('session'); $I->seeInSession('key1'); $I->seeInSession('key1', 'value1'); $I->dontSeeInSession('missing'); @@ -27,21 +28,64 @@ public function sessionAssertions(FunctionalTester $I): void public function loginAndLogoutAssertions(FunctionalTester $I): void { - $user = new InMemoryUser('john@example.com', null, ['ROLE_USER']); + $container = $I->grabService('service_container'); + $factory = $I->grabService('session.factory'); + $session = $factory->createSession(); + $session->start(); + $container->set('session', $session); + + /** @var UserRepository $repository */ + $repository = $I->grabService(UserRepository::class); + $user = $repository->getByEmail('john_doe@gmail.com'); $I->amLoggedInAs($user); - $I->seeInSession('_security_main'); - $I->logout(); - $I->dontSeeInSession('_security_main'); + $I->amOnPage('/dashboard'); + $I->seeAuthentication(); + /** @var TokenStorageInterface $tokenStorage */ + $tokenStorage = $I->grabService('security.token_storage'); + $I->assertNotNull($tokenStorage->getToken()); + $I->see('You are in the Dashboard!'); - $token = new UsernamePasswordToken($user, 'main', ['ROLE_USER']); + $token = new PostAuthenticationToken($user, 'main', $user->getRoles()); $I->amLoggedInWithToken($token); - $I->seeInSession('_security_main'); + $I->amOnPage('/dashboard'); + $I->seeAuthentication(); + $I->see('You are in the Dashboard!'); + + $I->amLoggedInAs($user); + $I->amOnPage('/dashboard'); + $I->see('You are in the Dashboard!'); + $I->goToLogoutPath(); + $I->seeCurrentRouteIs('index'); + $I->dontSeeAuthentication(); $I->amLoggedInAs($user); - $I->seeInSession('_security_main'); + $I->amOnPage('/dashboard'); + $I->see('You are in the Dashboard!'); + $I->logoutProgrammatically(); + $I->amOnPage('/dashboard'); + $I->seeInCurrentUrl('login'); + $I->dontSeeAuthentication(); + + $session = $factory->createSession(); + $session->start(); + $container->set('session', $session); + $I->amLoggedInAs($user); + $I->amOnPage('/'); + $I->seeSessionHasValues(['_security_main', '_security_main']); + $I->unpersistService('session'); + } + + public function dontSeeInSession(FunctionalTester $I): void + { + $factory = $I->grabService('session.factory'); + $session = $factory->createSession(); + $session->start(); + $I->grabService('service_container')->set('session', $session); + + $I->amOnPage('/'); $I->dontSeeInSession('_security_main'); } } diff --git a/tests/Functional/TimeCest.php b/tests/Functional/TimeCest.php index 3b901e5..01ebb1e 100644 --- a/tests/Functional/TimeCest.php +++ b/tests/Functional/TimeCest.php @@ -7,7 +7,8 @@ class TimeCest { public function timeAssertions(FunctionalTester $I): void { - $I->amOnRoute('sample'); - $I->seeRequestTimeIsLessThan(500); + $I->amOnRoute('app_register'); + $I->seeInCurrentUrl('register'); + $I->seeRequestTimeIsLessThan(400); } } diff --git a/tests/Functional/TranslationCest.php b/tests/Functional/TranslationCest.php index 63f9cc5..11670f2 100644 --- a/tests/Functional/TranslationCest.php +++ b/tests/Functional/TranslationCest.php @@ -3,18 +3,53 @@ use Tests\FunctionalTester; -class TranslationCest +final class TranslationCest { - public function translationAssertions(FunctionalTester $I): void + public function dontSeeFallbackTranslations(FunctionalTester $I): void { - $I->amOnRoute('translation'); - $I->dontSeeMissingTranslations(); + $I->amOnPage('/register'); $I->dontSeeFallbackTranslations(); - $I->assertGreaterThanOrEqual(0, $I->grabDefinedTranslationsCount()); + } + + public function dontSeeMissingTranslations(FunctionalTester $I): void + { + $I->amOnPage('/'); + $I->dontSeeMissingTranslations(); + } + + public function grabDefinedTranslationsCount(FunctionalTester $I): void + { + $I->amOnPage('/register'); + $I->assertSame(6, $I->grabDefinedTranslationsCount()); + } + + public function seeAllTranslationsDefined(FunctionalTester $I): void + { + $I->amOnPage('/register'); $I->seeAllTranslationsDefined(); + } + + public function seeDefaultLocaleIs(FunctionalTester $I): void + { + $I->amOnPage('/register'); $I->seeDefaultLocaleIs('en'); + } + + public function seeFallbackLocalesAre(FunctionalTester $I): void + { + $I->amOnPage('/register'); $I->seeFallbackLocalesAre(['es']); + } + + public function seeFallbackTranslationsCountLessThan(FunctionalTester $I): void + { + $I->amOnPage('/register'); $I->seeFallbackTranslationsCountLessThan(1); + } + + public function seeMissingTranslationsCountLessThan(FunctionalTester $I): void + { + $I->amOnPage('/'); $I->seeMissingTranslationsCountLessThan(1); } } diff --git a/tests/Functional/TwigCest.php b/tests/Functional/TwigCest.php index 74515dd..9f3de26 100644 --- a/tests/Functional/TwigCest.php +++ b/tests/Functional/TwigCest.php @@ -7,10 +7,12 @@ class TwigCest { public function twigAssertions(FunctionalTester $I): void { - $I->amOnRoute('twig'); - $I->seeRenderedTemplate('home.html.twig'); + $I->amOnPage('/register'); + $I->dontSeeRenderedTemplate('security/login.html.twig'); + + $I->amOnPage('/login'); $I->seeRenderedTemplate('layout.html.twig'); - $I->dontSeeRenderedTemplate('other.html.twig'); - $I->seeCurrentTemplateIs('home.html.twig'); + $I->seeRenderedTemplate('security/login.html.twig'); + $I->seeCurrentTemplateIs('security/login.html.twig'); } } diff --git a/tests/Functional/ValidatorCest.php b/tests/Functional/ValidatorCest.php index 3da2732..a08f6b5 100644 --- a/tests/Functional/ValidatorCest.php +++ b/tests/Functional/ValidatorCest.php @@ -8,16 +8,28 @@ class ValidatorCest { public function validatorAssertions(FunctionalTester $I): void { - $invalid = new \ValidEntity(); - $valid = new \ValidEntity('John', 'abcd'); + $valid = \ValidEntity::create('test@example.com', 'password123'); - $I->seeViolatedConstraint($invalid); - $I->seeViolatedConstraint($invalid, 'name'); - $I->seeViolatedConstraint($invalid, 'short', Assert\Length::class); - $I->seeViolatedConstraintsCount(2, $invalid); - $I->seeViolatedConstraintsCount(1, $invalid, 'name'); - $I->seeViolatedConstraintMessage('too short', $invalid, 'short'); $I->dontSeeViolatedConstraint($valid); - $I->dontSeeViolatedConstraint($invalid, 'short', Assert\NotBlank::class); + $I->dontSeeViolatedConstraint($valid, 'email'); + $I->dontSeeViolatedConstraint($valid, 'email', Assert\Email::class); + + $invalidEmail = \ValidEntity::create('invalid_email', 'password123'); + $I->seeViolatedConstraint($invalidEmail); + $I->seeViolatedConstraint($invalidEmail, 'email'); + + $weakPassword = \ValidEntity::create('test@example.com', 'weak'); + $I->seeViolatedConstraint($weakPassword); + $I->seeViolatedConstraint($weakPassword, 'password'); + $I->seeViolatedConstraint($weakPassword, 'password', Assert\Length::class); + + $I->seeViolatedConstraintsCount(2, \ValidEntity::create('invalid_email', 'weak')); + $I->seeViolatedConstraintsCount(1, $weakPassword); + $I->seeViolatedConstraintsCount(0, $weakPassword, 'email'); + + $userWithBlankEmail = \ValidEntity::create('', 'weak'); + $I->seeViolatedConstraintMessage('valid email', $invalidEmail, 'email'); + $I->seeViolatedConstraintMessage('should not be blank', $userWithBlankEmail, 'email'); + $I->seeViolatedConstraintMessage('This value is too short', $userWithBlankEmail, 'email'); } } diff --git a/tests/HttpClientAssertionsTest.php b/tests/HttpClientAssertionsTest.php new file mode 100644 index 0000000..e3da3a7 --- /dev/null +++ b/tests/HttpClientAssertionsTest.php @@ -0,0 +1,71 @@ +client = new KernelBrowser(self::$kernel); + $this->client->request('GET', '/http-client'); + } + + protected function tearDown(): void + { + parent::tearDown(); + restore_exception_handler(); + } + + protected static function getKernelClass(): string + { + return \TestKernel::class; + } + + protected function getClient(): KernelBrowser + { + return $this->client; + } + + protected function grabService(string $serviceId): object + { + return self::getContainer()->get($serviceId); + } + + protected function unpersistService(string $serviceName): void + { + // no-op for tests + } + + public function testHttpClientAssertionsAcrossClients(): void + { + $this->assertHttpClientRequest('https://example.com/default', 'GET', null, ['X-Test' => 'yes'], 'app.http_client'); + $this->assertHttpClientRequest('https://example.com/body', 'POST', ['example' => 'payload'], [], 'app.http_client'); + $this->assertHttpClientRequest('https://api.example.com/resource', 'GET', null, [], 'app.http_client.json_client'); + $this->assertHttpClientRequestCount(2, 'app.http_client'); + $this->assertHttpClientRequestCount(1, 'app.http_client.json_client'); + $this->assertNotHttpClientRequest('https://example.com/missing', 'GET', 'app.http_client'); + } + + protected function grabCollector(DataCollectorName $name, string $function): DataCollectorInterface + { + /** @var Profiler $profiler */ + $profiler = self::getContainer()->get('profiler'); + $profile = $profiler->collect($this->client->getRequest(), $this->client->getResponse()); + + return $profile->getCollector($name->value); + } +} diff --git a/tests/LoggerAssertionsTest.php b/tests/LoggerAssertionsTest.php index b9c0c1c..f164d1a 100644 --- a/tests/LoggerAssertionsTest.php +++ b/tests/LoggerAssertionsTest.php @@ -2,11 +2,15 @@ namespace Tests; +require_once __DIR__ . '/_app/TestKernel.php'; + use Codeception\Module\Symfony\LoggerAssertionsTrait; use Codeception\Module\Symfony\DataCollectorName; +use PHPUnit\Framework\AssertionFailedError; use Symfony\Bundle\FrameworkBundle\KernelBrowser; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface; +use Symfony\Component\HttpKernel\DataCollector\LoggerDataCollector; use Symfony\Component\HttpKernel\Profiler\Profiler; class LoggerAssertionsTest extends KernelTestCase @@ -47,6 +51,17 @@ public function testDontSeeDeprecations(): void $this->dontSeeDeprecations(); } + public function testDeprecationsAreReported(): void + { + $this->client->request('GET', '/deprecated'); + try { + $this->dontSeeDeprecations(); + self::fail('Expected deprecations to be reported.'); + } catch (AssertionFailedError $error) { + $this->assertStringContainsString('deprecation', $error->getMessage()); + } + } + protected function tearDown(): void { @@ -56,6 +71,14 @@ protected function tearDown(): void protected function grabCollector(DataCollectorName $name, string $function): DataCollectorInterface { + if ($name === DataCollectorName::LOGGER) { + $collector = new LoggerDataCollector($this->grabService('logger')); + $collector->collect($this->client->getRequest(), $this->client->getResponse()); + $collector->lateCollect(); + + return $collector; + } + /** @var Profiler $profiler */ $profiler = self::getContainer()->get('profiler'); $profile = $profiler->collect($this->client->getRequest(), $this->client->getResponse()); diff --git a/tests/MailerAssertionsTest.php b/tests/MailerAssertionsTest.php index 552413b..ce15c25 100644 --- a/tests/MailerAssertionsTest.php +++ b/tests/MailerAssertionsTest.php @@ -48,37 +48,71 @@ protected function getService(string $serviceId): object return $container->get($serviceId); } - public function testMailerAssertions(): void + public function testDontSeeEmailIsSentWithEmptyLogger(): void { $this->dontSeeEmailIsSent(); + } + public function testQueuedEmailAssertions(): void + { $queuedEmail = (new Email()) ->from('queued@example.com') ->to('queued@example.com'); $envelope = new Envelope(new Address('queued@example.com'), [new Address('queued@example.com')]); $queuedEvent = new MessageEvent($queuedEmail, $envelope, 'smtp', true); + /** @var MessageLoggerListener $logger */ $logger = $this->getService('mailer.message_logger_listener'); $logger->onMessage($queuedEvent); $this->assertQueuedEmailCount(1); $this->assertEmailIsQueued($queuedEvent); + $this->assertEmailCount(0); + $this->assertQueuedEmailCount(1, 'smtp', 'Queued emails can be counted by transport'); + } - $mailer = $this->getService('mailer'); - $mailer->send((new Email()) - ->from('john_doe@example.com') - ->to('jane_doe@example.com') - ->subject('Test') - ->text('Example text body') - ->html('

HTML body

') - ->attach('Attachment content', 'test.txt') - ); + public function testMailerEventAssertionsAgainstSentEmail(): void + { + $this->client->request('GET', '/send-email'); $this->assertEmailCount(1); $this->seeEmailIsSent(); - $this->grabLastSentEmail(); - $this->grabSentEmails(); - $event = $this->getMailerEvent(1); + + $event = $this->getMailerEvent(); + $this->assertInstanceOf(MessageEvent::class, $event); $this->assertEmailIsNotQueued($event); + + $email = $this->grabLastSentEmail(); + $this->assertInstanceOf(Email::class, $email); + $this->assertSame('jane_doe@example.com', $email->getTo()[0]->getAddress()); + $this->assertEmailCount(1, $event?->getTransport()); + + $emails = $this->grabSentEmails(); + $this->assertCount(1, $emails); + } + + public function testTransportSpecificMailerEvents(): void + { + /** @var MessageLoggerListener $logger */ + $logger = $this->getService('mailer.message_logger_listener'); + + $smtpEmail = (new Email()) + ->from('smtp@example.com') + ->to('smtp@example.com'); + $smtpEnvelope = new Envelope(new Address('smtp@example.com'), [new Address('smtp@example.com')]); + $smtpEvent = new MessageEvent($smtpEmail, $smtpEnvelope, 'smtp', false); + + $nullEmail = (new Email()) + ->from('null@example.com') + ->to('null@example.com'); + $nullEnvelope = new Envelope(new Address('null@example.com'), [new Address('null@example.com')]); + $nullEvent = new MessageEvent($nullEmail, $nullEnvelope, 'null', false); + + $logger->onMessage($smtpEvent); + $logger->onMessage($nullEvent); + + $this->assertEmailCount(1, 'smtp'); + $this->assertEmailCount(1, 'null'); + $this->assertEmailCount(2); } } diff --git a/tests/MimeAssertionsTest.php b/tests/MimeAssertionsTest.php index 8c55086..294c9df 100644 --- a/tests/MimeAssertionsTest.php +++ b/tests/MimeAssertionsTest.php @@ -23,15 +23,7 @@ protected function setUp(): void $this->client = new KernelBrowser($this->kernel); $this->getService('mailer.message_logger_listener')->reset(); - $mailer = $this->getService('mailer'); - $mailer->send((new Email()) - ->from('john_doe@example.com') - ->to('jane_doe@example.com') - ->subject('Test') - ->text('Example text body') - ->html('

HTML body

') - ->attach('Attachment content', 'test.txt') - ); + $this->client->request('GET', '/send-email'); } protected function tearDown(): void @@ -55,17 +47,65 @@ protected function getService(string $serviceId): object return $container->get($serviceId); } - public function testMimeAssertions(): void + public function testAssertEmailAddressContains(): void { $this->assertEmailAddressContains('To', 'jane_doe@example.com'); + } + + public function testAssertEmailAttachmentCount(): void + { $this->assertEmailAttachmentCount(1); + } + + public function testAssertEmailHasHeader(): void + { $this->assertEmailHasHeader('To'); + } + + public function testAssertEmailHeaderSame(): void + { $this->assertEmailHeaderSame('To', 'jane_doe@example.com'); + } + + public function testAssertEmailHeaderNotSame(): void + { $this->assertEmailHeaderNotSame('To', 'john_doe@example.com'); - $this->assertEmailHtmlBodyContains('HTML body'); - $this->assertEmailHtmlBodyNotContains('password'); + } + + public function testAssertEmailHtmlBodyContains(): void + { + $this->assertEmailHtmlBodyContains('Example Email'); + } + + public function testAssertEmailHtmlBodyNotContains(): void + { + $this->assertEmailHtmlBodyNotContains('userpassword'); + } + + public function testAssertEmailNotHasHeader(): void + { $this->assertEmailNotHasHeader('Bcc'); + } + + public function testAssertEmailTextBodyContains(): void + { $this->assertEmailTextBodyContains('Example text body'); - $this->assertEmailTextBodyNotContains('Secret'); + } + + public function testAssertEmailTextBodyNotContains(): void + { + $this->assertEmailTextBodyNotContains('My secret text body'); + } + + public function testAssertionsWorkWithProvidedEmail(): void + { + $email = (new Email()) + ->from('custom@example.com') + ->to('custom@example.com') + ->text('Custom body text'); + + $this->assertEmailAddressContains('To', 'custom@example.com', $email); + $this->assertEmailTextBodyContains('Custom body text', $email); + $this->assertEmailNotHasHeader('Cc', $email); } } diff --git a/tests/NotifierAssertionsTest.php b/tests/NotifierAssertionsTest.php new file mode 100644 index 0000000..97dbb24 --- /dev/null +++ b/tests/NotifierAssertionsTest.php @@ -0,0 +1,96 @@ +kernel = new \TestKernel('test', true); + $this->kernel->boot(); + $this->client = new KernelBrowser($this->kernel); + $this->getService('notifier.notification_logger_listener')->reset(); + } + + protected function tearDown(): void + { + $this->kernel->shutdown(); + restore_exception_handler(); + parent::tearDown(); + } + + protected function getClient(): KernelBrowser + { + return $this->client; + } + + protected function getService(string $serviceId): object + { + $container = $this->kernel->getContainer(); + if ($container->has('test.service_container')) { + $container = $container->get('test.service_container'); + } + + return $container->get($serviceId); + } + + public function testNoNotificationsSent(): void + { + $this->dontSeeNotificationIsSent(); + } + + public function testQueuedAndSentNotifications(): void + { + /** @var NotifierFixture $fixture */ + $fixture = $this->getService(NotifierFixture::class); + + $sentEvent = $fixture->sendNotification('Welcome notification', 'primary'); + $queuedEvent = $fixture->sendNotification('Queued notification', 'queued', true); + + $this->assertNotificationCount(1); + $this->assertNotificationCount(1, 'primary'); + $this->assertQueuedNotificationCount(1); + $this->assertQueuedNotificationCount(1, 'queued'); + + $this->assertNotificationIsNotQueued($sentEvent); + $this->assertNotificationIsQueued($queuedEvent); + + $firstEvent = $this->getNotifierEvent(); + $this->assertInstanceOf(MessageEvent::class, $firstEvent); + $this->assertNotificationIsNotQueued($firstEvent); + } + + public function testNotificationSubjectAndTransportAssertions(): void + { + /** @var NotifierFixture $fixture */ + $fixture = $this->getService(NotifierFixture::class); + + $fixture->sendNotification('Primary alert', 'chat'); + $fixture->sendNotification('Secondary update', 'backup'); + + $lastNotification = $this->grabLastSentNotification(); + $this->assertInstanceOf(ChatMessage::class, $lastNotification); + + $this->assertNotificationSubjectContains($lastNotification, 'update'); + $this->assertNotificationSubjectNotContains($lastNotification, 'missing'); + $this->assertNotificationTransportIsEqual($lastNotification, 'backup'); + $this->assertNotificationTransportIsNotEqual($lastNotification, 'chat'); + + $notifications = $this->grabSentNotifications(); + $this->assertCount(2, $notifications); + $this->assertSame('chat', $this->getNotifierMessage(0)?->getTransport()); + } +} diff --git a/tests/ParameterAssertionsTest.php b/tests/ParameterAssertionsTest.php index 398005b..d945bc3 100644 --- a/tests/ParameterAssertionsTest.php +++ b/tests/ParameterAssertionsTest.php @@ -38,6 +38,11 @@ public function testGrabParameter(): void $this->assertSame('value', $this->grabParameter('app.param')); } + public function testGrabBusinessNameParameter(): void + { + $this->assertSame('Codeception', $this->grabParameter('app.business_name')); + } + protected function tearDown(): void { restore_exception_handler(); diff --git a/tests/RouterAssertionsTest.php b/tests/RouterAssertionsTest.php index 5d68527..e52a864 100644 --- a/tests/RouterAssertionsTest.php +++ b/tests/RouterAssertionsTest.php @@ -38,17 +38,40 @@ protected function unpersistService(string $serviceName): void // no-op for tests } - public function testRouterAssertions(): void + public function testAmOnAction(): void { - $this->amOnRoute('sample'); - $this->seeCurrentRouteIs('sample'); - $this->seeInCurrentRoute('sample'); - $this->seeCurrentActionIs('TestKernel::sample'); - $this->amOnAction('TestKernel::index'); - $this->seeCurrentRouteIs('index'); - $this->invalidateCachedRouter(); + $this->assertSame(200, $this->client->getResponse()->getStatusCode()); + $this->assertStringContainsString('Hello World!', $this->client->getResponse()->getContent()); + } + + public function testAmOnRoute(): void + { + $this->amOnRoute('index'); + + $this->assertStringContainsString('Hello World!', $this->client->getResponse()->getContent()); + } + + public function testSeeCurrentActionIs(): void + { + $this->client->request('GET', '/'); + + $this->seeCurrentActionIs('TestKernel::index'); + } + + public function testSeeCurrentRouteIs(): void + { + $this->client->request('GET', '/login'); + + $this->seeCurrentRouteIs('app_login'); + } + + public function testSeeInCurrentRoute(): void + { + $this->client->request('GET', '/register'); + + $this->seeInCurrentRoute('app_register'); } protected function tearDown(): void diff --git a/tests/SecurityAssertionsTest.php b/tests/SecurityAssertionsTest.php index 072049b..8ef2412 100644 --- a/tests/SecurityAssertionsTest.php +++ b/tests/SecurityAssertionsTest.php @@ -45,26 +45,70 @@ protected function grabSecurityService() return new Security(self::getContainer()); } - public function testSecurityAssertions(): void + public function testDontSeeAuthentication(): void { + $this->client->request('GET', '/dashboard'); + $this->dontSeeAuthentication(); + } + + public function testDontSeeRememberedAuthentication(): void + { + $user = $this->createTestUser(['ROLE_USER']); + $this->client->loginUser($user); + $this->dontSeeRememberedAuthentication(); + } - $hasher = $this->grabService('security.password_hasher'); - $hashed = $hasher->hashPassword(new \TestUser('tmp', ''), 'password'); - $user = new \TestUser('john@example.com', $hashed, ['ROLE_USER', 'ROLE_ADMIN']); - $this->getClient()->loginUser($user); + public function testSeeAuthentication(): void + { + $user = $this->createTestUser(['ROLE_USER']); + $this->client->loginUser($user); $this->seeAuthentication(); + } + + public function testSeeRememberedAuthentication(): void + { + $user = $this->createTestUser(['ROLE_USER']); + $this->client->loginUser($user); + $this->client->getCookieJar()->set(new Cookie('REMEMBERME', 'test-remember')); - $this->getClient()->getCookieJar()->set(new Cookie('REMEMBERME', 'test')); $this->seeRememberedAuthentication(); + } + + public function testSeeUserHasRole(): void + { + $user = $this->createTestUser(['ROLE_USER', 'ROLE_ADMIN']); + $this->client->loginUser($user); $this->seeUserHasRole('ROLE_ADMIN'); - $this->seeUserHasRoles(['ROLE_USER', 'ROLE_ADMIN']); + } + + public function testSeeUserHasRoles(): void + { + $user = $this->createTestUser(['ROLE_USER', 'ROLE_CUSTOMER']); + $this->client->loginUser($user); + + $this->seeUserHasRoles(['ROLE_USER', 'ROLE_CUSTOMER']); + } + + public function testSeeUserPasswordDoesNotNeedRehash(): void + { + $user = $this->createTestUser(['ROLE_USER']); + $this->client->loginUser($user); + $this->seeUserPasswordDoesNotNeedRehash(); } + private function createTestUser(array $roles): \TestUser + { + $hasher = $this->grabService('security.password_hasher'); + $hashed = $hasher->hashPassword(new \TestUser('tmp', ''), '123456'); + + return new \TestUser('john_doe@gmail.com', $hashed, $roles); + } + protected function tearDown(): void { restore_exception_handler(); diff --git a/tests/ServicesAssertionsTest.php b/tests/ServicesAssertionsTest.php index 3972cfe..5278907 100644 --- a/tests/ServicesAssertionsTest.php +++ b/tests/ServicesAssertionsTest.php @@ -6,6 +6,7 @@ use Symfony\Bundle\FrameworkBundle\KernelBrowser; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Bundle\SecurityBundle\Security; class ServicesAssertionsTest extends KernelTestCase { @@ -37,9 +38,15 @@ protected function getClient(): KernelBrowser return $this->client; } - public function testServicesAssertions(): void + public function testGrabServiceReturnsSecurityHelper(): void + { + $securityHelper = $this->grabService('security.helper'); + + $this->assertInstanceOf(Security::class, $securityHelper); + } + + public function testPersistAndUnpersistService(): void { - $this->grabService('router'); $this->persistService('router'); $this->assertArrayHasKey('router', $this->persistentServices); diff --git a/tests/SessionAssertionsTest.php b/tests/SessionAssertionsTest.php index b277762..45b321f 100644 --- a/tests/SessionAssertionsTest.php +++ b/tests/SessionAssertionsTest.php @@ -3,14 +3,17 @@ namespace Tests; use Codeception\Module\Symfony\SessionAssertionsTrait; +use Codeception\Module\Symfony\SecurityAssertionsTrait; use Symfony\Bundle\FrameworkBundle\KernelBrowser; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; -use Symfony\Component\Security\Core\User\InMemoryUser; +use Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken; +use Tests\_app\Entity\User; +use Tests\_app\Repository\UserRepository; class SessionAssertionsTest extends KernelTestCase { + use SecurityAssertionsTrait; use SessionAssertionsTrait; private KernelBrowser $client; @@ -41,12 +44,35 @@ protected function _getContainer(): ContainerInterface return self::getContainer(); } + public function testAmLoggedInAsShowsDashboard(): void + { + $user = $this->getTestUser(); + + $this->amLoggedInAs($user); + $this->client->request('GET', '/dashboard'); + + $this->seeAuthentication(); + $this->assertSame(200, $this->client->getResponse()->getStatusCode()); + $this->assertStringContainsString('You are in the Dashboard!', $this->client->getResponse()->getContent()); + } + + public function testAmLoggedInWithTokenShowsDashboard(): void + { + $user = $this->getTestUser(); + $token = new PostAuthenticationToken($user, 'main', $user->getRoles()); + + $this->amLoggedInWithToken($token); + $this->client->request('GET', '/dashboard'); + + $this->seeAuthentication(); + $this->assertStringContainsString('You are in the Dashboard!', $this->client->getResponse()->getContent()); + } + public function testSessionAssertions(): void { - $container = self::getContainer(); - $factory = $container->get('session.factory'); + $factory = self::getContainer()->get('session.factory'); $session = $factory->createSession(); - $container->set('session', $session); + self::getContainer()->set('session', $session); $session->set('key1', 'value1'); $session->set('key2', 'value2'); $session->save(); @@ -59,24 +85,50 @@ public function testSessionAssertions(): void $this->seeSessionHasValues(['key1' => 'value1', 'key2' => 'value2']); } - public function testLoginAndLogoutAssertions(): void + public function testDontSeeInSessionWhenAnonymous(): void { - $user = new InMemoryUser('john@example.com', null, ['ROLE_USER']); + $this->client->request('GET', '/'); - $this->amLoggedInAs($user); - $this->seeInSession('_security_main'); - $this->logout(); $this->dontSeeInSession('_security_main'); + } + + public function testGoToLogoutPath(): void + { + $user = $this->getTestUser(); + $this->amLoggedInAs($user); + $this->client->request('GET', '/dashboard'); + $this->assertStringContainsString('You are in the Dashboard!', $this->client->getResponse()->getContent()); - $token = new UsernamePasswordToken($user, 'main', ['ROLE_USER']); - $this->amLoggedInWithToken($token); - $this->seeInSession('_security_main'); $this->goToLogoutPath(); + $this->assertSame('/logout', $this->client->getRequest()->getPathInfo()); + $this->assertSame(302, $this->client->getResponse()->getStatusCode()); + $this->client->followRedirect(); + $this->dontSeeAuthentication(); + $this->assertSame('/', $this->client->getRequest()->getPathInfo()); + } + + public function testLogoutProgrammatically(): void + { + $user = $this->getTestUser(); $this->amLoggedInAs($user); - $this->seeInSession('_security_main'); + $this->logoutProgrammatically(); - $this->dontSeeInSession('_security_main'); + $this->client->request('GET', '/dashboard'); + + $this->dontSeeAuthentication(); + $this->assertSame(302, $this->client->getResponse()->getStatusCode()); + $this->assertSame('/login', $this->client->getResponse()->headers->get('Location')); + } + + private function getTestUser(): User + { + /** @var UserRepository $repository */ + $repository = self::getContainer()->get(UserRepository::class); + $user = $repository->getByEmail('john_doe@gmail.com'); + $this->assertNotNull($user); + + return $user; } protected function tearDown(): void diff --git a/tests/TimeAssertionsTest.php b/tests/TimeAssertionsTest.php index 12a7c32..a13739c 100644 --- a/tests/TimeAssertionsTest.php +++ b/tests/TimeAssertionsTest.php @@ -20,7 +20,7 @@ protected function setUp(): void { self::bootKernel(); $this->client = new KernelBrowser(self::$kernel); - $this->client->request('GET', '/sample'); + $this->client->request('GET', '/register'); } protected static function getKernelClass(): string @@ -45,7 +45,8 @@ protected function _getContainer(): ContainerInterface public function testRequestTime(): void { - $this->seeRequestTimeIsLessThan(500); + $this->assertStringContainsString('register', $this->client->getRequest()->getPathInfo()); + $this->seeRequestTimeIsLessThan(400); } protected function grabCollector(DataCollectorName $name, string $function): DataCollectorInterface diff --git a/tests/TranslationAssertionsTest.php b/tests/TranslationAssertionsTest.php index 6a91c28..d9b8321 100644 --- a/tests/TranslationAssertionsTest.php +++ b/tests/TranslationAssertionsTest.php @@ -18,9 +18,9 @@ class TranslationAssertionsTest extends KernelTestCase protected function setUp(): void { - self::bootKernel(); + self::bootKernel(['debug' => true]); $this->client = new KernelBrowser(self::$kernel); - $this->client->request('GET', '/translation'); + $this->client->enableProfiler(); } protected static function getKernelClass(): string @@ -43,15 +43,51 @@ protected function _getContainer(): ContainerInterface return self::getContainer(); } - public function testTranslationAssertions(): void + public function testDontSeeFallbackTranslations(): void { - $this->dontSeeMissingTranslations(); + $this->client->request('GET', '/register'); $this->dontSeeFallbackTranslations(); - $this->assertGreaterThanOrEqual(0, $this->grabDefinedTranslationsCount()); + } + + public function testDontSeeMissingTranslations(): void + { + $this->client->request('GET', '/'); + $this->dontSeeMissingTranslations(); + } + + public function testGrabDefinedTranslationsCount(): void + { + $this->client->request('GET', '/register'); + $this->assertSame(6, $this->grabDefinedTranslationsCount()); + } + + public function testSeeAllTranslationsDefined(): void + { + $this->client->request('GET', '/register'); $this->seeAllTranslationsDefined(); + } + + public function testSeeDefaultLocaleIs(): void + { + $this->client->request('GET', '/register'); $this->seeDefaultLocaleIs('en'); + } + + public function testSeeFallbackLocalesAre(): void + { + $this->client->request('GET', '/register'); $this->seeFallbackLocalesAre(['es']); + } + + public function testSeeFallbackTranslationsCountLessThan(): void + { + $this->client->request('GET', '/register'); $this->seeFallbackTranslationsCountLessThan(1); + } + + public function testSeeMissingTranslationsCountLessThan(): void + { + $this->client->request('GET', '/'); $this->seeMissingTranslationsCountLessThan(1); } @@ -59,7 +95,8 @@ protected function grabCollector(DataCollectorName $name, string $function): Dat { /** @var Profiler $profiler */ $profiler = self::getContainer()->get('profiler'); - $profile = $profiler->collect($this->client->getRequest(), $this->client->getResponse()); + $profile = $this->client->getProfile() ?? $profiler->collect($this->client->getRequest(), $this->client->getResponse()); + return $profile->getCollector($name->value); } diff --git a/tests/TwigAssertionsTest.php b/tests/TwigAssertionsTest.php index 0bdaccd..48a93ba 100644 --- a/tests/TwigAssertionsTest.php +++ b/tests/TwigAssertionsTest.php @@ -18,9 +18,9 @@ class TwigAssertionsTest extends KernelTestCase protected function setUp(): void { - self::bootKernel(); + self::bootKernel(['debug' => true]); $this->client = new KernelBrowser(self::$kernel); - $this->client->request('GET', '/twig'); + $this->client->enableProfiler(); } protected static function getKernelClass(): string @@ -43,19 +43,34 @@ protected function _getContainer(): ContainerInterface return self::getContainer(); } - public function testTwigAssertions(): void + public function testDontSeeRenderedTemplate(): void { - $this->seeRenderedTemplate('home.html.twig'); + $this->client->request('GET', '/register'); + + $this->dontSeeRenderedTemplate('security/login.html.twig'); + } + + public function testSeeCurrentTemplateIs(): void + { + $this->client->request('GET', '/login'); + + $this->seeCurrentTemplateIs('security/login.html.twig'); + } + + public function testSeeRenderedTemplate(): void + { + $this->client->request('GET', '/login'); + $this->seeRenderedTemplate('layout.html.twig'); - $this->dontSeeRenderedTemplate('other.html.twig'); - $this->seeCurrentTemplateIs('home.html.twig'); + $this->seeRenderedTemplate('security/login.html.twig'); } protected function grabCollector(DataCollectorName $name, string $function): DataCollectorInterface { /** @var Profiler $profiler */ $profiler = self::getContainer()->get('profiler'); - $profile = $profiler->collect($this->client->getRequest(), $this->client->getResponse()); + $profile = $this->client->getProfile() ?? $profiler->collect($this->client->getRequest(), $this->client->getResponse()); + return $profile->getCollector($name->value); } diff --git a/tests/ValidatorAssertionsTest.php b/tests/ValidatorAssertionsTest.php index 5aa2bcc..0d6c316 100644 --- a/tests/ValidatorAssertionsTest.php +++ b/tests/ValidatorAssertionsTest.php @@ -40,19 +40,59 @@ protected function _getContainer(): ContainerInterface return self::getContainer(); } - public function testValidatorAssertions(): void - { - $invalid = new \ValidEntity(); - $valid = new \ValidEntity('John', 'abcd'); - - $this->seeViolatedConstraint($invalid); - $this->seeViolatedConstraint($invalid, 'name'); - $this->seeViolatedConstraint($invalid, 'short', Assert\Length::class); - $this->seeViolatedConstraintsCount(2, $invalid); - $this->seeViolatedConstraintsCount(1, $invalid, 'name'); - $this->seeViolatedConstraintMessage('too short', $invalid, 'short'); - $this->dontSeeViolatedConstraint($valid); - $this->dontSeeViolatedConstraint($invalid, 'short', Assert\NotBlank::class); + public function testDontSeeViolatedConstraint(): void + { + $user = \ValidEntity::create('test@example.com', 'password123'); + + $this->dontSeeViolatedConstraint($user); + $this->dontSeeViolatedConstraint($user, 'email'); + $this->dontSeeViolatedConstraint($user, 'email', Assert\Email::class); + + $user->setEmail('invalid_email'); + $this->dontSeeViolatedConstraint($user, 'password'); + + $user->setEmail('test@example.com'); + $user->setPassword('weak'); + $this->dontSeeViolatedConstraint($user, 'email'); + $this->dontSeeViolatedConstraint($user, 'password', Assert\NotBlank::class); + } + + public function testSeeViolatedConstraint(): void + { + $user = \ValidEntity::create('invalid_email', 'password123'); + + $this->seeViolatedConstraint($user); + $this->seeViolatedConstraint($user, 'email'); + + $user->setEmail('test@example.com'); + $user->setPassword('weak'); + $this->seeViolatedConstraint($user); + $this->seeViolatedConstraint($user, 'password'); + $this->seeViolatedConstraint($user, 'password', Assert\Length::class); + } + + public function testSeeViolatedConstraintCount(): void + { + $user = \ValidEntity::create('invalid_email', 'weak'); + + $this->seeViolatedConstraintsCount(2, $user); + $this->seeViolatedConstraintsCount(1, $user, 'email'); + + $user->setEmail('test@example.com'); + + $this->seeViolatedConstraintsCount(1, $user); + $this->seeViolatedConstraintsCount(0, $user, 'email'); + } + + public function testSeeViolatedConstraintMessageContains(): void + { + $user = \ValidEntity::create('invalid_email', 'weak'); + + $this->seeViolatedConstraintMessage('valid email', $user, 'email'); + + $user->setEmail(''); + $this->seeViolatedConstraintMessage('should not be blank', $user, 'email'); + $this->seeViolatedConstraintMessage('This value is too short', $user, 'email'); } protected function tearDown(): void diff --git a/tests/_app/DoctrineFixturesLoadCommand.php b/tests/_app/DoctrineFixturesLoadCommand.php new file mode 100644 index 0000000..3fd489c --- /dev/null +++ b/tests/_app/DoctrineFixturesLoadCommand.php @@ -0,0 +1,32 @@ +writeln('Fixtures loaded'); + + return Command::SUCCESS; + } +} diff --git a/tests/_app/Entity/User.php b/tests/_app/Entity/User.php new file mode 100644 index 0000000..46ee3b5 --- /dev/null +++ b/tests/_app/Entity/User.php @@ -0,0 +1,86 @@ +email = $email; + $user->password = $password; + $user->roles = $roles; + + return $user; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getEmail(): string + { + return $this->email; + } + + public function setEmail(string $email): self + { + $this->email = $email; + + return $this; + } + + public function getRoles(): array + { + return $this->roles; + } + + public function setRoles(array $roles): self + { + $this->roles = $roles; + + return $this; + } + + public function getPassword(): string + { + return $this->password; + } + + public function setPassword(string $password): self + { + $this->password = $password; + + return $this; + } + + public function getUserIdentifier(): string + { + return $this->email; + } + + public function eraseCredentials(): void + { + } +} diff --git a/tests/_app/Event/NamedEvent.php b/tests/_app/Event/NamedEvent.php new file mode 100644 index 0000000..bfd0109 --- /dev/null +++ b/tests/_app/Event/NamedEvent.php @@ -0,0 +1,9 @@ +addOption( + self::OPTION_SOMETHING, + self::OPTION_SHORT_SOMETHING, + InputOption::VALUE_NONE, + 'Give some output' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + if ($input->getOption(self::OPTION_SOMETHING)) { + $io->text('Bye world!'); + } else { + $io->text('Hello world!'); + } + + return Command::SUCCESS; + } +} diff --git a/tests/_app/HelloCommand.php b/tests/_app/HelloCommand.php index f583d40..26fe091 100644 --- a/tests/_app/HelloCommand.php +++ b/tests/_app/HelloCommand.php @@ -1,5 +1,7 @@ $options + */ + public function __invoke(string $method, string $url, array $options = []): ResponseInterface + { + $statusCode = match ($url) { + 'https://example.com/body' => 201, + 'https://api.example.com/resource' => 202, + default => 200, + }; + + return new MockResponse( + json_encode([ + 'method' => $method, + 'url' => $url, + 'options' => $options, + ], JSON_THROW_ON_ERROR), + [ + 'http_code' => $statusCode, + 'response_headers' => ['Content-Type' => 'application/json'], + ] + ); + } +} diff --git a/tests/_app/Listener/NamedEventListener.php b/tests/_app/Listener/NamedEventListener.php new file mode 100644 index 0000000..bfcf893 --- /dev/null +++ b/tests/_app/Listener/NamedEventListener.php @@ -0,0 +1,12 @@ +> + */ + private array $logs = []; + + public function log($level, $message, array $context = []): void + { + $priorityMap = [ + 'DEBUG' => 100, + 'INFO' => 200, + 'NOTICE' => 250, + 'WARNING' => 300, + 'ERROR' => 400, + 'CRITICAL' => 500, + 'ALERT' => 550, + 'EMERGENCY' => 600, + ]; + + $priorityName = strtoupper((string) $level); + $priority = $priorityMap[$priorityName] ?? 200; + $timestamp = microtime(true); + + $this->logs[] = [ + 'message' => (string) $message, + 'context' => $context, + 'priority' => $priority, + 'priorityName' => $priorityName, + 'channel' => 'app', + 'timestamp' => $timestamp, + 'timestamp_rfc3339' => date(DATE_RFC3339, (int) $timestamp), + 'errorCount' => 1, + ]; + } + + public function getLogs(?Request $request = null): array + { + return $this->logs; + } + + public function countErrors(?Request $request = null): int + { + return count(array_filter( + $this->logs, + static fn (array $log): bool => $log['priority'] >= 400, + )); + } + + public function clear(): void + { + $this->logs = []; + } +} diff --git a/tests/_app/Mailer/RegistrationMailer.php b/tests/_app/Mailer/RegistrationMailer.php new file mode 100644 index 0000000..541575f --- /dev/null +++ b/tests/_app/Mailer/RegistrationMailer.php @@ -0,0 +1,27 @@ +from(new Address('jeison_doe@gmail.com', 'No Reply')) + ->to(new Address($recipient)) + ->subject('Account created successfully') + ->attach('Example attachment') + ->text('Example text body') + ->htmlTemplate('emails/registration.html.twig'); + + $this->mailer->send($email); + } +} diff --git a/tests/_app/Notifier/NotifierFixture.php b/tests/_app/Notifier/NotifierFixture.php new file mode 100644 index 0000000..de1a4ee --- /dev/null +++ b/tests/_app/Notifier/NotifierFixture.php @@ -0,0 +1,23 @@ +transport($transport); + $event = new MessageEvent($message, $queued); + $this->dispatcher->dispatch($event); + + return $event; + } +} diff --git a/tests/_app/Repository/Model/UserRepositoryInterface.php b/tests/_app/Repository/Model/UserRepositoryInterface.php new file mode 100644 index 0000000..6cd17c7 --- /dev/null +++ b/tests/_app/Repository/Model/UserRepositoryInterface.php @@ -0,0 +1,12 @@ +_em->persist($user); + $this->_em->flush(); + $this->_em->clear(); + } + + public function getByEmail(string $email): ?User + { + /** @var User|null $user */ + $user = $this->findOneBy(['email' => $email]); + + return $user; + } +} diff --git a/tests/_app/Security/TestUserProvider.php b/tests/_app/Security/TestUserProvider.php new file mode 100644 index 0000000..bc9fcc2 --- /dev/null +++ b/tests/_app/Security/TestUserProvider.php @@ -0,0 +1,44 @@ +repository->getByEmail($identifier); + + if ($user === null) { + $exception = new UserNotFoundException(); + $exception->setUserIdentifier($identifier); + throw $exception; + } + + return $user; + } + + public function refreshUser(UserInterface $user): UserInterface + { + if (!$this->supportsClass($user::class)) { + throw new UnsupportedUserException(); + } + + return $this->loadUserByIdentifier($user->getUserIdentifier()); + } + + public function supportsClass(string $class): bool + { + return $class === User::class || is_subclass_of($class, User::class); + } +} diff --git a/tests/_app/TestKernel.php b/tests/_app/TestKernel.php index d8dc71e..a10ad02 100644 --- a/tests/_app/TestKernel.php +++ b/tests/_app/TestKernel.php @@ -4,34 +4,75 @@ use Symfony\Component\HttpKernel\Kernel as BaseKernel; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Cookie; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Psr\Log\LoggerInterface; +use Doctrine\DBAL\Connection; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\ORMSetup; +use Doctrine\ORM\Tools\SchemaTool; +use Symfony\Component\HttpClient\DataCollector\HttpClientDataCollector; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\TraceableHttpClient; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Mailer\MailerInterface; -use Symfony\Component\Mime\Email; +use Symfony\Component\HttpKernel\DataCollector\LoggerDataCollector; +use Symfony\Component\Form\Extension\Core\Type\EmailType; +use Symfony\Component\Form\Extension\Core\Type\PasswordType; +use Symfony\Component\Form\FormFactoryInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Bundle\SecurityBundle\Security; use Symfony\Bundle\TwigBundle\TwigBundle; use Symfony\Component\Mailer\EventListener\MessageLoggerListener; +use Symfony\Component\Notifier\EventListener\NotificationLoggerListener; use Symfony\Contracts\Translation\TranslatorInterface; +use Symfony\Component\Validator\Constraints\Email as EmailConstraint; +use Symfony\Component\Validator\Constraints\NotBlank; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken; +use Tests\_app\Security\TestUserProvider; use Twig\Environment; +use Twig\Extension\ProfilerExtension; +use Twig\Profiler\Profile; use function Symfony\Component\DependencyInjection\Loader\Configurator\service; +use Tests\_app\DoctrineFixturesLoadCommand; +use Tests\_app\Entity\User; +use Tests\_app\Event\NamedEvent; +use Tests\_app\Event\OrphanEvent; +use Tests\_app\Event\SampleEvent; +use Tests\_app\ExampleCommand; +use Tests\_app\HelloCommand; +use Tests\_app\HttpClient\MockResponseFactory; +use Tests\_app\Logger\ArrayLogger; +use Tests\_app\Listener\NamedEventListener; +use Tests\_app\Listener\SampleEventListener; +use Tests\_app\Mailer\RegistrationMailer; +use Tests\_app\Notifier\NotifierFixture; +use Tests\_app\Repository\Model\UserRepositoryInterface; +use Tests\_app\Repository\UserRepository; +use Doctrine\DBAL\DriverManager; class TestKernel extends BaseKernel { use MicroKernelTrait; + private static ?EntityManagerInterface $entityManager = null; + protected function configureContainer(ContainerConfigurator $container): void { $container->extension('framework', [ 'secret' => 'test', 'test' => true, 'profiler' => ['enabled' => true, 'collect' => true, 'collect_serializer_data' => true], - 'property_info' => ['with_constructor_extractor' => false], + 'property_info' => ['enabled' => true], 'session' => [ 'handler_id' => null, 'storage_factory_id' => 'session.storage.factory.mock_file', @@ -41,12 +82,19 @@ protected function configureContainer(ContainerConfigurator $container): void 'translator' => [ 'default_path' => __DIR__ . '/translations', 'fallbacks' => ['es'], + 'logging' => true, ], 'validation' => ['enabled' => true], + 'form' => ['enabled' => true], + 'notifier' => [ + 'chatter_transports' => ['async' => 'null://null'], + 'texter_transports' => ['sms' => 'null://null'], + ], ]); $container->extension('twig', [ 'default_path' => __DIR__ . '/templates', + 'debug' => true, ]); $container->extension('security', [ @@ -54,16 +102,14 @@ protected function configureContainer(ContainerConfigurator $container): void PasswordAuthenticatedUserInterface::class => 'auto', ], 'providers' => [ - 'users_in_memory' => [ - 'memory' => [ - 'users' => [], - ], + 'doctrine_users' => [ + 'id' => 'security.user.provider.test', ], ], 'firewalls' => [ 'main' => [ 'lazy' => true, - 'provider' => 'users_in_memory', + 'provider' => 'doctrine_users', 'remember_me' => ['secret' => 'test'], 'logout' => ['path' => 'logout'], ], @@ -71,11 +117,35 @@ protected function configureContainer(ContainerConfigurator $container): void ]); $container->parameters()->set('app.param', 'value'); + $container->parameters()->set('app.business_name', 'Codeception'); $services = $container->services(); $services->set(HelloCommand::class, HelloCommand::class) ->tag('console.command', ['command' => 'app:hello']) ->public(); + $services->set(ExampleCommand::class, ExampleCommand::class) + ->tag('console.command', ['command' => 'app:example-command']) + ->public(); + $services->set(DoctrineFixturesLoadCommand::class, DoctrineFixturesLoadCommand::class) + ->tag('console.command', ['command' => 'doctrine:fixtures:load']) + ->public(); + $services->set('doctrine.orm.entity_manager', EntityManagerInterface::class) + ->factory([self::class, 'createEntityManager']) + ->public() + ->share(true); + $services->alias('doctrine.orm.default_entity_manager', 'doctrine.orm.entity_manager')->public(); + $services->set('doctrine.dbal.default_connection', Connection::class) + ->factory([self::class, 'createConnection']) + ->public() + ->share(true); + $services->set('security.user.provider.test', TestUserProvider::class) + ->arg('$repository', service(UserRepository::class)) + ->tag('security.user_provider') + ->public(); + $services->set(UserRepository::class) + ->factory([self::class, 'createUserRepository']) + ->public(); + $services->alias(UserRepositoryInterface::class, UserRepository::class)->public(); $services->set(Security::class) ->public() ->arg('$container', service('test.service_container')); @@ -83,6 +153,64 @@ protected function configureContainer(ContainerConfigurator $container): void $services->set('mailer.message_logger_listener', MessageLoggerListener::class) ->tag('kernel.event_subscriber') ->public(); + $services->set('notifier.notification_logger_listener', NotificationLoggerListener::class) + ->tag('kernel.event_subscriber') + ->public(); + $services->alias('notifier.logger_notification_listener', 'notifier.notification_logger_listener')->public(); + $services->set(RegistrationMailer::class) + ->arg('$mailer', service('mailer')) + ->public(); + $services->set(NotifierFixture::class) + ->arg('$dispatcher', service('event_dispatcher')) + ->public(); + $services->set(SampleEventListener::class) + ->tag('kernel.event_listener', ['event' => SampleEvent::class]) + ->public(); + $services->set(NamedEventListener::class) + ->tag('kernel.event_listener', ['event' => 'named.event', 'method' => 'onNamedEvent']) + ->public(); + $services->set(MockResponseFactory::class) + ->public(); + $services->set('logger', ArrayLogger::class) + ->public(); + $services->alias(LoggerInterface::class, 'logger')->public(); + $services->set('app.http_client.inner', MockHttpClient::class) + ->arg('$responseFactory', service(MockResponseFactory::class)) + ->public(); + $services->set('app.http_client', TraceableHttpClient::class) + ->args([service('app.http_client.inner'), service('debug.stopwatch')->nullOnInvalid()]) + ->public(); + $services->set('app.http_client.json_client.inner', MockHttpClient::class) + ->args([service(MockResponseFactory::class), 'https://api.example.com/']) + ->public(); + $services->set('app.http_client.json_client', TraceableHttpClient::class) + ->args([service('app.http_client.json_client.inner'), service('debug.stopwatch')->nullOnInvalid()]) + ->public(); + $services->set(HttpClientDataCollector::class) + ->public() + ->call('registerClient', ['app.http_client', service('app.http_client')]) + ->call('registerClient', ['app.http_client.json_client', service('app.http_client.json_client')]) + ->tag('data_collector', [ + 'id' => 'http_client', + 'template' => '@WebProfiler/Collector/http_client.html.twig', + 'priority' => 100, + ]); + $services->alias('data_collector.http_client', HttpClientDataCollector::class)->public(); + $services->set(LoggerDataCollector::class) + ->public() + ->arg('$logger', service('logger')) + ->tag('data_collector', [ + 'id' => 'logger', + 'template' => '@WebProfiler/Collector/logger.html.twig', + 'priority' => 300, + ]); + $services->alias('data_collector.logger', LoggerDataCollector::class)->public(); + $services->set(Profile::class) + ->public(); + $services->set(ProfilerExtension::class) + ->arg('$profile', service(Profile::class)) + ->tag('twig.extension') + ->public(); } public function registerBundles(): iterable @@ -98,10 +226,28 @@ protected function configureRoutes(RoutingConfigurator $routes): void { $routes->add('index', '/') ->controller(self::class . '::index'); + $routes->add('app_login', '/login') + ->controller(self::class . '::login'); + $routes->add('app_register', '/register') + ->controller(self::class . '::register'); + $routes->add('dashboard', '/dashboard') + ->controller(self::class . '::dashboard'); $routes->add('sample', '/sample') ->controller(self::class . '::sample'); + $routes->add('request_attr', '/request_attr') + ->controller(self::class . '::requestWithAttribute'); + $routes->add('response_cookie', '/response_cookie') + ->controller(self::class . '::responseWithCookie'); + $routes->add('response_json', '/response_json') + ->controller(self::class . '::responseJsonFormat'); + $routes->add('test_page', '/test_page') + ->controller(self::class . '::testPage'); + $routes->add('unprocessable_entity', '/unprocessable_entity') + ->controller(self::class . '::unprocessableEntity'); $routes->add('redirect', '/redirect') ->controller(self::class . '::redirect'); + $routes->add('redirect_home', '/redirect_home') + ->controller(self::class . '::redirectToHome'); $routes->add('unprocessable', '/unprocessable') ->controller(self::class . '::unprocessable'); $routes->add('session', '/session') @@ -115,12 +261,71 @@ protected function configureRoutes(RoutingConfigurator $routes): void $routes->add('twig', '/twig') ->controller(self::class . '::twig'); $routes->add('logout', '/logout') - ->controller(self::class . '::index'); + ->controller(self::class . '::logout'); + $routes->add('dispatch_event', '/dispatch-event') + ->controller(self::class . '::dispatchEvent'); + $routes->add('dispatch_named_event', '/dispatch-named-event') + ->controller(self::class . '::dispatchNamedEvent'); + $routes->add('dispatch_orphan_event', '/dispatch-orphan-event') + ->controller(self::class . '::dispatchOrphanEvent'); + $routes->add('form_handler', '/form') + ->controller(self::class . '::form'); + $routes->add('http_client', '/http-client') + ->controller(self::class . '::httpClientRequests'); } public function index(): Response { - return new Response('OK'); + return new Response('Hello World!'); + } + + public function login(Environment $twig): Response + { + return new Response($twig->render('security/login.html.twig')); + } + + public function register(Request $request, Environment $twig): Response + { + if ($request->isMethod('POST')) { + return new RedirectResponse('/dashboard'); + } + + return new Response($twig->render('security/register.html.twig')); + } + + public function logout(Request $request): RedirectResponse + { + /** @var TokenStorageInterface $tokenStorage */ + $tokenStorage = $this->getContainer()->get('test.service_container')->get('security.token_storage'); + $tokenStorage->setToken(null); + + $sessionName = null; + if ($request->hasSession()) { + $session = $request->getSession(); + $sessionName = $session->getName(); + $session->invalidate(); + } + + $response = new RedirectResponse('/'); + if ($sessionName !== null) { + $response->headers->clearCookie($sessionName); + } + $response->headers->clearCookie('MOCKSESSID'); + $response->headers->clearCookie('REMEMBERME'); + + return $response; + } + + public function dashboard(): Response + { + /** @var TokenStorageInterface $tokenStorage */ + $tokenStorage = $this->getContainer()->get('test.service_container')->get('security.token_storage'); + $token = $tokenStorage->getToken(); + if ($token === null || !is_object($token->getUser())) { + return new RedirectResponse('/login'); + } + + return new Response('You are in the Dashboard!'); } public function sample(Request $request): Response @@ -146,11 +351,65 @@ public function sample(Request $request): Response return $response; } + public function testPage(): Response + { + $html = << + Test Page + +

Test Page

+ + + + +HTML; + + return new Response($html); + } + public function redirect(): RedirectResponse { return new RedirectResponse('/sample'); } + public function requestWithAttribute(Request $request): Response + { + $request->attributes->set('page', 'register'); + + return new Response('Request attribute set'); + } + + public function responseWithCookie(): Response + { + $response = new Response('TESTCOOKIE has been set.'); + $response->headers->setCookie(new Cookie('TESTCOOKIE', 'codecept')); + + return $response; + } + + public function responseJsonFormat(Request $request): JsonResponse + { + $request->setRequestFormat('json'); + + return new JsonResponse([ + 'status' => 'success', + 'message' => "Expected format: 'json'.", + ]); + } + + public function unprocessableEntity(): JsonResponse + { + return new JsonResponse([ + 'status' => 'error', + 'message' => 'The request was well-formed but could not be processed.', + ], Response::HTTP_UNPROCESSABLE_ENTITY); + } + + public function redirectToHome(): RedirectResponse + { + return new RedirectResponse('/'); + } + public function unprocessable(): Response { return new Response('Unprocessable', 422); @@ -159,28 +418,26 @@ public function unprocessable(): Response public function session(Request $request): Response { $session = $request->getSession(); - $session->set('key1', 'value1'); - $session->set('key2', 'value2'); - return new Response('Session'); + $session->set('key1', 'value1'); + $session->set('key2', 'value2'); + $session->save(); + + $this->getContainer()->set('session', $session); + + return new Response('Session'); } - public function deprecated(): Response + public function deprecated(LoggerInterface $logger): Response { trigger_error('Deprecated endpoint', E_USER_DEPRECATED); + $logger->info('Deprecated endpoint', ['scream' => false]); + return new Response('Deprecated'); } - public function sendEmail(MailerInterface $mailer): Response + public function sendEmail(RegistrationMailer $mailer): Response { - $email = (new Email()) - ->from('john_doe@example.com') - ->to('jane_doe@example.com') - ->subject('Test') - ->text('Example text body') - ->html('

HTML body

') - ->attach('Attachment content', 'test.txt'); - - $mailer->send($email); + $mailer->sendConfirmationEmail('jane_doe@example.com'); return new Response('Email sent'); } @@ -195,4 +452,122 @@ public function twig(Environment $twig): Response { return new Response($twig->render('home.html.twig')); } + + public function dispatchEvent(EventDispatcherInterface $dispatcher): Response + { + $dispatcher->dispatch(new SampleEvent()); + + return new Response('Event dispatched'); + } + + public function dispatchNamedEvent(EventDispatcherInterface $dispatcher): Response + { + $dispatcher->dispatch(new NamedEvent(), 'named.event'); + + return new Response('Named event dispatched'); + } + + public function dispatchOrphanEvent(EventDispatcherInterface $dispatcher): Response + { + $dispatcher->dispatch(new OrphanEvent()); + + return new Response('Orphan event dispatched'); + } + + public function httpClientRequests( + #[Autowire(service: 'app.http_client')] HttpClientInterface $httpClient, + #[Autowire(service: 'app.http_client.json_client')] HttpClientInterface $jsonClient, + ): Response { + $httpClient->request('GET', 'https://example.com/default', [ + 'headers' => ['X-Test' => 'yes'], + ]); + $httpClient->request('POST', 'https://example.com/body', [ + 'json' => ['example' => 'payload'], + ]); + $jsonClient->request('GET', 'https://api.example.com/resource', [ + 'headers' => ['Accept' => 'application/json'], + ]); + + return new Response('HTTP client calls executed'); + } + + public function form(Request $request, FormFactoryInterface $formFactory): Response + { + $builder = $formFactory->createNamedBuilder('registration_form', options: ['csrf_protection' => false]); + $builder->add('email', EmailType::class, [ + 'constraints' => [new NotBlank(), new EmailConstraint()], + ]); + $builder->add('password', PasswordType::class, [ + 'constraints' => [new NotBlank()], + ]); + $form = $builder->getForm(); + + $form->handleRequest($request); + + $content = << + +
+ + + +
+ + +HTML; + + $status = $form->isSubmitted() && !$form->isValid() ? 422 : 200; + + return new Response($content, $status); + } + + public static function createEntityManager(): EntityManagerInterface + { + if (self::$entityManager !== null && self::$entityManager->isOpen()) { + return self::$entityManager; + } + + $config = ORMSetup::createAttributeMetadataConfig([__DIR__ . '/Entity'], true); + $proxyDir = sys_get_temp_dir() . '/doctrine-proxies'; + if (!is_dir($proxyDir)) { + mkdir($proxyDir, 0777, true); + } + $config->setProxyDir($proxyDir); + $config->setProxyNamespace('TestsProxies'); + $config->setAutoGenerateProxyClasses(true); + + $connection = DriverManager::getConnection([ + 'driver' => 'pdo_sqlite', + 'memory' => true, + ]); + + $entityManager = new EntityManager($connection, $config); + + $schemaTool = new SchemaTool($entityManager); + $metadata = [$entityManager->getClassMetadata(User::class)]; + $schemaTool->dropSchema($metadata); + $schemaTool->createSchema($metadata); + + $user = User::create('john_doe@gmail.com', 'secret', ['ROLE_TEST']); + $entityManager->persist($user); + $entityManager->flush(); + $entityManager->clear(); + + self::$entityManager = $entityManager; + + return $entityManager; + } + + public static function createConnection(): Connection + { + return self::createEntityManager()->getConnection(); + } + + public static function createUserRepository(): UserRepository + { + /** @var UserRepository $repository */ + $repository = self::createEntityManager()->getRepository(User::class); + + return $repository; + } } diff --git a/tests/_app/ValidEntity.php b/tests/_app/ValidEntity.php index 2729a7d..2066821 100644 --- a/tests/_app/ValidEntity.php +++ b/tests/_app/ValidEntity.php @@ -5,14 +5,46 @@ class ValidEntity { #[Assert\NotBlank] - public string $name; + #[Assert\Email] + #[Assert\Length(min: 6)] + private string $email; - #[Assert\Length(min: 3)] - public string $short; + #[Assert\NotBlank] + #[Assert\Length(min: 8)] + private string $password; + + public function __construct(string $email = 'test@example.com', string $password = 'password123') + { + $this->email = $email; + $this->password = $password; + } + + public static function create(string $email, string $password): self + { + return new self($email, $password); + } + + public function getEmail(): string + { + return $this->email; + } + + public function setEmail(string $email): self + { + $this->email = $email; + + return $this; + } - public function __construct(string $name = '', string $short = 'ab') + public function getPassword(): string { - $this->name = $name; - $this->short = $short; + return $this->password; + } + + public function setPassword(string $password): self + { + $this->password = $password; + + return $this; } } diff --git a/tests/_app/templates/emails/registration.html.twig b/tests/_app/templates/emails/registration.html.twig new file mode 100644 index 0000000..69744e0 --- /dev/null +++ b/tests/_app/templates/emails/registration.html.twig @@ -0,0 +1,5 @@ +{% extends 'layout.html.twig' %} + +{% block content %} +

Example Email.

+{% endblock %} diff --git a/tests/_app/templates/layout.html.twig b/tests/_app/templates/layout.html.twig index 82f27ae..0131177 100644 --- a/tests/_app/templates/layout.html.twig +++ b/tests/_app/templates/layout.html.twig @@ -1,6 +1,8 @@ -{% block content %}{% endblock %} +{% block content %} + {% block body %}{% endblock %} +{% endblock %} diff --git a/tests/_app/templates/security/login.html.twig b/tests/_app/templates/security/login.html.twig new file mode 100644 index 0000000..db8c465 --- /dev/null +++ b/tests/_app/templates/security/login.html.twig @@ -0,0 +1,19 @@ +{% extends 'layout.html.twig' %} + +{% block body %} +
+

Please sign in

+ + + + + +
+ +
+ + +
+{% endblock %} diff --git a/tests/_app/templates/security/register.html.twig b/tests/_app/templates/security/register.html.twig new file mode 100644 index 0000000..0855e3e --- /dev/null +++ b/tests/_app/templates/security/register.html.twig @@ -0,0 +1,20 @@ +{% extends 'layout.html.twig' %} + +{% block body %} +

{{ 'register.title'|trans }}

+ +

{{ 'register.heading'|trans }}

+ +
+ + + + + + + + + + +
+{% endblock %} diff --git a/tests/_app/translations/messages.en.yaml b/tests/_app/translations/messages.en.yaml index e209d18..9159a7e 100644 --- a/tests/_app/translations/messages.en.yaml +++ b/tests/_app/translations/messages.en.yaml @@ -1 +1,8 @@ defined_message: "Hello" +register: + title: "Register" + heading: "Sign Up" + email_label: "Email Address" + password_label: "Password" + agree_terms_label: "I agree to the terms and conditions" + submit_button: "Sign Up" diff --git a/tests/_app/translations/messages.es.yaml b/tests/_app/translations/messages.es.yaml new file mode 100644 index 0000000..9e0585c --- /dev/null +++ b/tests/_app/translations/messages.es.yaml @@ -0,0 +1,7 @@ +register: + title: "Registro" + heading: "Registrarse" + email_label: "Correo Electrónico" + password_label: "Contraseña" + agree_terms_label: "Acepto los términos y condiciones" + submit_button: "Registrarse" diff --git a/tests/_support/FunctionalTester.php b/tests/_support/FunctionalTester.php new file mode 100644 index 0000000..2b29f9b --- /dev/null +++ b/tests/_support/FunctionalTester.php @@ -0,0 +1,43 @@ +amOnPage('/register'); + + if (!$followRedirects) { + $this->stopFollowingRedirects(); + } + + $this->submitSymfonyForm('registration_form', [ + '[email]' => $email, + '[password]' => $password, + '[agreeTerms]' => true, + ]); + } +}