diff --git a/src/Maker/Security/MakeFormLogin.php b/src/Maker/Security/MakeFormLogin.php index 34d5bd12fe..9ef63c138f 100644 --- a/src/Maker/Security/MakeFormLogin.php +++ b/src/Maker/Security/MakeFormLogin.php @@ -12,7 +12,9 @@ namespace Symfony\Bundle\MakerBundle\Maker\Security; use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; +use Doctrine\ORM\EntityManager; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Bundle\MakerBundle\ConsoleStyle; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException; @@ -20,6 +22,7 @@ use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Bundle\MakerBundle\Maker\AbstractMaker; +use Symfony\Bundle\MakerBundle\Maker\Common\CanGenerateTestsTrait; use Symfony\Bundle\MakerBundle\Security\InteractiveSecurityHelper; use Symfony\Bundle\MakerBundle\Security\SecurityConfigUpdater; use Symfony\Bundle\MakerBundle\Security\SecurityControllerBuilder; @@ -33,6 +36,7 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; use Symfony\Component\Yaml\Yaml; @@ -48,10 +52,13 @@ */ final class MakeFormLogin extends AbstractMaker { + use CanGenerateTestsTrait; + private const SECURITY_CONFIG_PATH = 'config/packages/security.yaml'; private YamlSourceManipulator $ysm; private string $controllerName; private string $firewallToUpdate; + private string $userClass; private string $userNameField; private bool $willLogout; @@ -70,6 +77,8 @@ public static function getCommandName(): string public function configureCommand(Command $command, InputConfiguration $inputConfig): void { $command->setHelp(file_get_contents(\dirname(__DIR__, 2).'/Resources/help/security/MakeFormLogin.txt')); + + $this->configureCommandWithTestsOption($command); } public static function getCommandDescription(): string @@ -116,9 +125,11 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma $securityHelper = new InteractiveSecurityHelper(); $this->firewallToUpdate = $securityHelper->guessFirewallName($io, $securityData); - $userClass = $securityHelper->guessUserClass($io, $securityData['security']['providers']); - $this->userNameField = $securityHelper->guessUserNameField($io, $userClass, $securityData['security']['providers']); + $this->userClass = $securityHelper->guessUserClass($io, $securityData['security']['providers']); + $this->userNameField = $securityHelper->guessUserNameField($io, $this->userClass, $securityData['security']['providers']); $this->willLogout = $io->confirm('Do you want to generate a \'/logout\' URL?'); + + $this->interactSetGenerateTests($input, $io); } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void @@ -167,6 +178,39 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen $securityData = $this->securityConfigUpdater->updateForLogout($securityData, $this->firewallToUpdate); } + if ($this->shouldGenerateTests()) { + $testClassDetails = $generator->createClassNameDetails( + 'LoginControllerTest', + 'Test\\', + ); + + $useStatements = new UseStatementGenerator([ + sprintf('\%s', $this->userClass), + // $userClassNameDetails->getFullName(), + // $userRepositoryDetails->getFullName(), + EntityManager::class, + WebTestCase::class, + UserPasswordHasherInterface::class, + ]); + + $generator->generateFile( + targetPath: sprintf('tests/%s.php', $testClassDetails->getShortName()), + templateName: 'security/formLogin/Test.LoginController.tpl.php', + variables: [ + 'use_statements' => $useStatements, + 'user_class' => $this->userClass, + // 'user_short_name' => $userClassNameDetails->getShortName(), + // 'user_repo_short_name' => $userRepositoryDetails->getShortName(), + // 'success_route_path' => null !== $this->controllerResetSuccessRoute ? $this->controllerResetSuccessRoute->getPath() : '/', + // 'from_email' => $this->fromEmailAddress, + ], + ); + + if (!class_exists(WebTestCase::class)) { + $io->caution('You\'ll need to install the `symfony/test-pack` to execute the tests for your new controller.'); + } + } + $generator->dumpFile(self::SECURITY_CONFIG_PATH, $securityData); $generator->writeChanges(); diff --git a/src/Resources/skeleton/security/formLogin/Test.LoginController.tpl.php b/src/Resources/skeleton/security/formLogin/Test.LoginController.tpl.php new file mode 100644 index 0000000000..cbec8c1c7d --- /dev/null +++ b/src/Resources/skeleton/security/formLogin/Test.LoginController.tpl.php @@ -0,0 +1,59 @@ + +namespace App\Tests; + + + +class LoginControllerTest extends WebTestCase +{ + public function testRegister(): void + { + $client = static::createClient(); + $client->disableReboot(); + $container = static::getContainer(); + $em = $container->get('doctrine.orm.entity_manager'); + $userRepository = $em->getRepository(\::class); + + foreach ($userRepository->findAll() as $user) { + $em->remove($user); + } + + $em->flush(); + + self::assertCount(0, $userRepository->findAll()); + + // Ensure login with invalid credentials shows error message + $client->request('GET', '/login'); + self::assertResponseIsSuccessful(); + + $client->submitForm('Sign in', [ + '_username' => 'email@example.com', + '_password' => 'password', + ]); + + self::assertResponseRedirects('/login'); + + $client->followRedirect(); + + self::assertSelectorTextContains('.alert-danger', 'Invalid credentials.'); + + // Ensure login with valid credentials + /** @var UserPasswordHasherInterface $passwordHasher */ + $passwordHasher = (static::getContainer())->get('security.user_password_hasher'); + + $user = (new User())->setEmail('email@example.com'); + $user->setPassword($passwordHasher->hashPassword($user, 'password')); + + $em->persist($user); + $em->flush(); + + $client->submitForm('Sign in', [ + '_username' => 'email@example.com', + '_password' => 'password', + ]); + + self::assertResponseRedirects('/'); + $client->followRedirect(); + self::assertSelectorNotExists('.alert-danger'); + self::assertResponseIsSuccessful(); + } +} diff --git a/tests/Maker/Security/MakeFormLoginTest.php b/tests/Maker/Security/MakeFormLoginTest.php index 4e1c90c214..90e9bd1c5b 100644 --- a/tests/Maker/Security/MakeFormLoginTest.php +++ b/tests/Maker/Security/MakeFormLoginTest.php @@ -99,6 +99,34 @@ public function getTestDetails(): \Generator $this->assertSame('app_logout', $securityConfig['security']['firewalls']['main']['logout']['path']); }), ]; + + yield 'generates_form_login_using_defaults_with_test' => [$this->createMakerTest() + ->run(function (MakerTestRunner $runner) { + $this->makeUser($runner); + + $output = $runner->runMaker([ + 'SecurityController', // Controller Name + 'y', // Generate Logout, + 'y', // Generate tests + ]); + + $this->assertStringContainsString('Success', $output); + $fixturePath = \dirname(__DIR__, 2).'/fixtures/security/make-form-login/expected'; + + $this->assertFileEquals($fixturePath.'/SecurityController.php', $runner->getPath('src/Controller/SecurityController.php')); + $this->assertFileEquals($fixturePath.'/login.html.twig', $runner->getPath('templates/security/login.html.twig')); + + $securityConfig = $runner->readYaml('config/packages/security.yaml'); + + $this->assertSame('app_login', $securityConfig['security']['firewalls']['main']['form_login']['login_path']); + $this->assertSame('app_login', $securityConfig['security']['firewalls']['main']['form_login']['check_path']); + $this->assertTrue($securityConfig['security']['firewalls']['main']['form_login']['enable_csrf']); + $this->assertSame('app_logout', $securityConfig['security']['firewalls']['main']['logout']['path']); + + $runner->configureDatabase(); + $runner->runTests(); + }), + ]; } private function runLoginTest(MakerTestRunner $runner): void