Skip to content

Commit

Permalink
feature #1515 [make:security:form-login] add ability to generate tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jrushlow committed Apr 27, 2024
1 parent 48846de commit 03592b9
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 2 deletions.
50 changes: 48 additions & 2 deletions src/Maker/Security/MakeFormLogin.php
Expand Up @@ -12,14 +12,18 @@
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\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Bundle\MakerBundle\ConsoleStyle;
use Symfony\Bundle\MakerBundle\DependencyBuilder;
use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
use Symfony\Bundle\MakerBundle\FileManager;
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;
Expand All @@ -33,6 +37,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;
Expand All @@ -48,10 +53,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;

Expand All @@ -70,6 +78,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
Expand Down Expand Up @@ -116,9 +126,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
Expand Down Expand Up @@ -167,6 +179,40 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
$securityData = $this->securityConfigUpdater->updateForLogout($securityData, $this->firewallToUpdate);
}

if ($this->shouldGenerateTests()) {
$userClassNameDetails = $generator->createClassNameDetails(
'\\'.$this->userClass,
'Entity\\'
);

$testClassDetails = $generator->createClassNameDetails(
'LoginControllerTest',
'Test\\',
);

$useStatements = new UseStatementGenerator([
$userClassNameDetails->getFullName(),
KernelBrowser::class,
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(),
],
);

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();
Expand Down
@@ -0,0 +1,79 @@
<?= "<?php\n" ?>
namespace App\Tests;

<?= $use_statements ?>

class LoginControllerTest extends WebTestCase
{
private KernelBrowser $client;

protected function setUp(): void
{
$this->client = static::createClient();
$container = static::getContainer();
$em = $container->get('doctrine.orm.entity_manager');
$userRepository = $em->getRepository(<?= $user_short_name ?>::class);

// Remove any existing users from the test database
foreach ($userRepository->findAll() as $user) {
$em->remove($user);
}

$em->flush();

// Create a <?= $user_short_name ?> fixture
/** @var UserPasswordHasherInterface $passwordHasher */
$passwordHasher = $container->get('security.user_password_hasher');

$user = (new <?= $user_short_name ?>())->setEmail('email@example.com');
$user->setPassword($passwordHasher->hashPassword($user, 'password'));

$em->persist($user);
$em->flush();
}

public function testLogin(): void
{
// Denied - Can't login with invalid email address.
$this->client->request('GET', '/login');
self::assertResponseIsSuccessful();

$this->client->submitForm('Sign in', [
'_username' => 'doesNotExist@example.com',
'_password' => 'password',
]);

self::assertResponseRedirects('/login');
$this->client->followRedirect();

// Ensure we do not reveal if the user exists or not.
self::assertSelectorTextContains('.alert-danger', 'Invalid credentials.');

// Denied - Can't login with invalid password.
$this->client->request('GET', '/login');
self::assertResponseIsSuccessful();

$this->client->submitForm('Sign in', [
'_username' => 'email@example.com',
'_password' => 'bad-password',
]);

self::assertResponseRedirects('/login');
$this->client->followRedirect();

// Ensure we do not reveal the user exists but the password is wrong.
self::assertSelectorTextContains('.alert-danger', 'Invalid credentials.');

// Success - Login with valid credentials is allowed.
$this->client->submitForm('Sign in', [
'_username' => 'email@example.com',
'_password' => 'password',
]);

self::assertResponseRedirects('/');
$this->client->followRedirect();

self::assertSelectorNotExists('.alert-danger');
self::assertResponseIsSuccessful();
}
}
31 changes: 31 additions & 0 deletions tests/Maker/Security/MakeFormLoginTest.php
Expand Up @@ -99,6 +99,37 @@ 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) {
// Make the UserPasswordHasherInterface available in the test
$runner->renderTemplateFile('security/make-form-login/FixtureController.php', 'src/Controller/FixtureController.php', []);

$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
Expand Down
22 changes: 22 additions & 0 deletions tests/fixtures/security/make-form-login/FixtureController.php
@@ -0,0 +1,22 @@
<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;

class FixtureController extends AbstractController
{
public function __construct(
public UserPasswordHasherInterface $passwordHasher
) {
}

#[Route(name: 'app_index')]
public function index(): Response
{
return $this->render('base.html.twig');
}
}

0 comments on commit 03592b9

Please sign in to comment.