Skip to content

Commit 03592b9

Browse files
authored
feature #1515 [make:security:form-login] add ability to generate tests
1 parent 48846de commit 03592b9

File tree

4 files changed

+180
-2
lines changed

4 files changed

+180
-2
lines changed

src/Maker/Security/MakeFormLogin.php

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,18 @@
1212
namespace Symfony\Bundle\MakerBundle\Maker\Security;
1313

1414
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
15+
use Doctrine\ORM\EntityManager;
1516
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
17+
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
18+
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
1619
use Symfony\Bundle\MakerBundle\ConsoleStyle;
1720
use Symfony\Bundle\MakerBundle\DependencyBuilder;
1821
use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
1922
use Symfony\Bundle\MakerBundle\FileManager;
2023
use Symfony\Bundle\MakerBundle\Generator;
2124
use Symfony\Bundle\MakerBundle\InputConfiguration;
2225
use Symfony\Bundle\MakerBundle\Maker\AbstractMaker;
26+
use Symfony\Bundle\MakerBundle\Maker\Common\CanGenerateTestsTrait;
2327
use Symfony\Bundle\MakerBundle\Security\InteractiveSecurityHelper;
2428
use Symfony\Bundle\MakerBundle\Security\SecurityConfigUpdater;
2529
use Symfony\Bundle\MakerBundle\Security\SecurityControllerBuilder;
@@ -33,6 +37,7 @@
3337
use Symfony\Component\Console\Command\Command;
3438
use Symfony\Component\Console\Input\InputInterface;
3539
use Symfony\Component\HttpFoundation\Response;
40+
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
3641
use Symfony\Component\Routing\Attribute\Route;
3742
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
3843
use Symfony\Component\Yaml\Yaml;
@@ -48,10 +53,13 @@
4853
*/
4954
final class MakeFormLogin extends AbstractMaker
5055
{
56+
use CanGenerateTestsTrait;
57+
5158
private const SECURITY_CONFIG_PATH = 'config/packages/security.yaml';
5259
private YamlSourceManipulator $ysm;
5360
private string $controllerName;
5461
private string $firewallToUpdate;
62+
private string $userClass;
5563
private string $userNameField;
5664
private bool $willLogout;
5765

@@ -70,6 +78,8 @@ public static function getCommandName(): string
7078
public function configureCommand(Command $command, InputConfiguration $inputConfig): void
7179
{
7280
$command->setHelp(file_get_contents(\dirname(__DIR__, 2).'/Resources/help/security/MakeFormLogin.txt'));
81+
82+
$this->configureCommandWithTestsOption($command);
7383
}
7484

7585
public static function getCommandDescription(): string
@@ -116,9 +126,11 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma
116126

117127
$securityHelper = new InteractiveSecurityHelper();
118128
$this->firewallToUpdate = $securityHelper->guessFirewallName($io, $securityData);
119-
$userClass = $securityHelper->guessUserClass($io, $securityData['security']['providers']);
120-
$this->userNameField = $securityHelper->guessUserNameField($io, $userClass, $securityData['security']['providers']);
129+
$this->userClass = $securityHelper->guessUserClass($io, $securityData['security']['providers']);
130+
$this->userNameField = $securityHelper->guessUserNameField($io, $this->userClass, $securityData['security']['providers']);
121131
$this->willLogout = $io->confirm('Do you want to generate a \'/logout\' URL?');
132+
133+
$this->interactSetGenerateTests($input, $io);
122134
}
123135

124136
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void
@@ -167,6 +179,40 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
167179
$securityData = $this->securityConfigUpdater->updateForLogout($securityData, $this->firewallToUpdate);
168180
}
169181

182+
if ($this->shouldGenerateTests()) {
183+
$userClassNameDetails = $generator->createClassNameDetails(
184+
'\\'.$this->userClass,
185+
'Entity\\'
186+
);
187+
188+
$testClassDetails = $generator->createClassNameDetails(
189+
'LoginControllerTest',
190+
'Test\\',
191+
);
192+
193+
$useStatements = new UseStatementGenerator([
194+
$userClassNameDetails->getFullName(),
195+
KernelBrowser::class,
196+
EntityManager::class,
197+
WebTestCase::class,
198+
UserPasswordHasherInterface::class,
199+
]);
200+
201+
$generator->generateFile(
202+
targetPath: sprintf('tests/%s.php', $testClassDetails->getShortName()),
203+
templateName: 'security/formLogin/Test.LoginController.tpl.php',
204+
variables: [
205+
'use_statements' => $useStatements,
206+
'user_class' => $this->userClass,
207+
'user_short_name' => $userClassNameDetails->getShortName(),
208+
],
209+
);
210+
211+
if (!class_exists(WebTestCase::class)) {
212+
$io->caution('You\'ll need to install the `symfony/test-pack` to execute the tests for your new controller.');
213+
}
214+
}
215+
170216
$generator->dumpFile(self::SECURITY_CONFIG_PATH, $securityData);
171217

172218
$generator->writeChanges();
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?= "<?php\n" ?>
2+
namespace App\Tests;
3+
4+
<?= $use_statements ?>
5+
6+
class LoginControllerTest extends WebTestCase
7+
{
8+
private KernelBrowser $client;
9+
10+
protected function setUp(): void
11+
{
12+
$this->client = static::createClient();
13+
$container = static::getContainer();
14+
$em = $container->get('doctrine.orm.entity_manager');
15+
$userRepository = $em->getRepository(<?= $user_short_name ?>::class);
16+
17+
// Remove any existing users from the test database
18+
foreach ($userRepository->findAll() as $user) {
19+
$em->remove($user);
20+
}
21+
22+
$em->flush();
23+
24+
// Create a <?= $user_short_name ?> fixture
25+
/** @var UserPasswordHasherInterface $passwordHasher */
26+
$passwordHasher = $container->get('security.user_password_hasher');
27+
28+
$user = (new <?= $user_short_name ?>())->setEmail('[email protected]');
29+
$user->setPassword($passwordHasher->hashPassword($user, 'password'));
30+
31+
$em->persist($user);
32+
$em->flush();
33+
}
34+
35+
public function testLogin(): void
36+
{
37+
// Denied - Can't login with invalid email address.
38+
$this->client->request('GET', '/login');
39+
self::assertResponseIsSuccessful();
40+
41+
$this->client->submitForm('Sign in', [
42+
'_username' => '[email protected]',
43+
'_password' => 'password',
44+
]);
45+
46+
self::assertResponseRedirects('/login');
47+
$this->client->followRedirect();
48+
49+
// Ensure we do not reveal if the user exists or not.
50+
self::assertSelectorTextContains('.alert-danger', 'Invalid credentials.');
51+
52+
// Denied - Can't login with invalid password.
53+
$this->client->request('GET', '/login');
54+
self::assertResponseIsSuccessful();
55+
56+
$this->client->submitForm('Sign in', [
57+
'_username' => '[email protected]',
58+
'_password' => 'bad-password',
59+
]);
60+
61+
self::assertResponseRedirects('/login');
62+
$this->client->followRedirect();
63+
64+
// Ensure we do not reveal the user exists but the password is wrong.
65+
self::assertSelectorTextContains('.alert-danger', 'Invalid credentials.');
66+
67+
// Success - Login with valid credentials is allowed.
68+
$this->client->submitForm('Sign in', [
69+
'_username' => '[email protected]',
70+
'_password' => 'password',
71+
]);
72+
73+
self::assertResponseRedirects('/');
74+
$this->client->followRedirect();
75+
76+
self::assertSelectorNotExists('.alert-danger');
77+
self::assertResponseIsSuccessful();
78+
}
79+
}

tests/Maker/Security/MakeFormLoginTest.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,37 @@ public function getTestDetails(): \Generator
9999
$this->assertSame('app_logout', $securityConfig['security']['firewalls']['main']['logout']['path']);
100100
}),
101101
];
102+
103+
yield 'generates_form_login_using_defaults_with_test' => [$this->createMakerTest()
104+
->run(function (MakerTestRunner $runner) {
105+
// Make the UserPasswordHasherInterface available in the test
106+
$runner->renderTemplateFile('security/make-form-login/FixtureController.php', 'src/Controller/FixtureController.php', []);
107+
108+
$this->makeUser($runner);
109+
110+
$output = $runner->runMaker([
111+
'SecurityController', // Controller Name
112+
'y', // Generate Logout,
113+
'y', // Generate tests
114+
]);
115+
116+
$this->assertStringContainsString('Success', $output);
117+
$fixturePath = \dirname(__DIR__, 2).'/fixtures/security/make-form-login/expected';
118+
119+
$this->assertFileEquals($fixturePath.'/SecurityController.php', $runner->getPath('src/Controller/SecurityController.php'));
120+
$this->assertFileEquals($fixturePath.'/login.html.twig', $runner->getPath('templates/security/login.html.twig'));
121+
122+
$securityConfig = $runner->readYaml('config/packages/security.yaml');
123+
124+
$this->assertSame('app_login', $securityConfig['security']['firewalls']['main']['form_login']['login_path']);
125+
$this->assertSame('app_login', $securityConfig['security']['firewalls']['main']['form_login']['check_path']);
126+
$this->assertTrue($securityConfig['security']['firewalls']['main']['form_login']['enable_csrf']);
127+
$this->assertSame('app_logout', $securityConfig['security']['firewalls']['main']['logout']['path']);
128+
129+
$runner->configureDatabase();
130+
$runner->runTests();
131+
}),
132+
];
102133
}
103134

104135
private function runLoginTest(MakerTestRunner $runner): void
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace App\Controller;
4+
5+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
6+
use Symfony\Component\HttpFoundation\Response;
7+
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
8+
use Symfony\Component\Routing\Attribute\Route;
9+
10+
class FixtureController extends AbstractController
11+
{
12+
public function __construct(
13+
public UserPasswordHasherInterface $passwordHasher
14+
) {
15+
}
16+
17+
#[Route(name: 'app_index')]
18+
public function index(): Response
19+
{
20+
return $this->render('base.html.twig');
21+
}
22+
}

0 commit comments

Comments
 (0)