From 3093a44cc7ef6931804d49897ee977d1d7e075a6 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Mon, 28 Mar 2022 15:11:14 -0400 Subject: [PATCH 01/21] wip add scaffolding system --- src/Maker/MakeScaffold.php | 183 +++++++++ src/Resources/config/makers.xml | 5 + src/Resources/scaffolds/6.0/auth.json | 10 + .../6.0/auth/config/packages/security.yaml | 58 +++ .../scaffolds/6.0/auth/config/services.yaml | 28 ++ .../src/Controller/SecurityController.php | 35 ++ .../6.0/auth/src/Security/LoginUser.php | 22 + .../6.0/auth/templates/login.html.twig | 31 ++ .../6.0/auth/tests/Browser/Authentication.php | 61 +++ .../tests/Functional/AuthenticationTest.php | 260 ++++++++++++ .../scaffolds/6.0/change-password.json | 10 + .../User/ChangePasswordController.php | 55 +++ .../src/Form/User/ChangePasswordFormType.php | 57 +++ .../templates/user/change_password.html.twig | 15 + .../Functional/User/ChangePasswordTest.php | 178 ++++++++ src/Resources/scaffolds/6.0/homepage.json | 8 + .../src/Controller/HomepageController.php | 16 + .../6.0/homepage/templates/index.html.twig | 15 + .../tests/Functional/HomepageTest.php | 22 + src/Resources/scaffolds/6.0/profile.json | 10 + .../src/Controller/User/ProfileController.php | 41 ++ .../profile/src/Form/User/ProfileFormType.php | 30 ++ .../profile/templates/user/profile.html.twig | 13 + .../tests/Functional/User/ProfileTest.php | 73 ++++ src/Resources/scaffolds/6.0/register.json | 10 + .../Controller/User/RegisterController.php | 52 +++ .../6.0/register/src/Entity/User.php | 113 +++++ .../src/Form/User/RegistrationFormType.php | 56 +++ .../templates/user/register.html.twig | 17 + .../tests/Functional/User/RegisterTest.php | 159 +++++++ .../scaffolds/6.0/reset-password.json | 13 + .../config/packages/reset_password.yaml | 4 + .../Controller/ResetPasswordController.php | 188 +++++++++ .../src/Entity/ResetPasswordRequest.php | 39 ++ .../Factory/ResetPasswordRequestFactory.php | 40 ++ .../ResetPassword/RequestResetFormType.php | 31 ++ .../ResetPassword/ResetPasswordFormType.php | 51 +++ .../ResetPasswordRequestRepository.php | 31 ++ .../reset_password/check_email.html.twig | 11 + .../templates/reset_password/email.html.twig | 9 + .../reset_password/request.html.twig | 22 + .../templates/reset_password/reset.html.twig | 12 + .../tests/Functional/ResetPasswordTest.php | 387 ++++++++++++++++++ src/Resources/scaffolds/6.0/user.json | 9 + .../scaffolds/6.0/user/src/Entity/User.php | 111 +++++ .../6.0/user/src/Factory/UserFactory.php | 61 +++ .../user/src/Repository/UserRepository.php | 43 ++ .../6.0/user/tests/Unit/Entity/UserTest.php | 22 + src/Test/MakerTestEnvironment.php | 2 +- tests/Maker/MakeScaffoldTest.php | 65 +++ 50 files changed, 2793 insertions(+), 1 deletion(-) create mode 100644 src/Maker/MakeScaffold.php create mode 100644 src/Resources/scaffolds/6.0/auth.json create mode 100644 src/Resources/scaffolds/6.0/auth/config/packages/security.yaml create mode 100644 src/Resources/scaffolds/6.0/auth/config/services.yaml create mode 100644 src/Resources/scaffolds/6.0/auth/src/Controller/SecurityController.php create mode 100644 src/Resources/scaffolds/6.0/auth/src/Security/LoginUser.php create mode 100644 src/Resources/scaffolds/6.0/auth/templates/login.html.twig create mode 100644 src/Resources/scaffolds/6.0/auth/tests/Browser/Authentication.php create mode 100644 src/Resources/scaffolds/6.0/auth/tests/Functional/AuthenticationTest.php create mode 100644 src/Resources/scaffolds/6.0/change-password.json create mode 100644 src/Resources/scaffolds/6.0/change-password/src/Controller/User/ChangePasswordController.php create mode 100644 src/Resources/scaffolds/6.0/change-password/src/Form/User/ChangePasswordFormType.php create mode 100644 src/Resources/scaffolds/6.0/change-password/templates/user/change_password.html.twig create mode 100644 src/Resources/scaffolds/6.0/change-password/tests/Functional/User/ChangePasswordTest.php create mode 100644 src/Resources/scaffolds/6.0/homepage.json create mode 100644 src/Resources/scaffolds/6.0/homepage/src/Controller/HomepageController.php create mode 100644 src/Resources/scaffolds/6.0/homepage/templates/index.html.twig create mode 100644 src/Resources/scaffolds/6.0/homepage/tests/Functional/HomepageTest.php create mode 100644 src/Resources/scaffolds/6.0/profile.json create mode 100644 src/Resources/scaffolds/6.0/profile/src/Controller/User/ProfileController.php create mode 100644 src/Resources/scaffolds/6.0/profile/src/Form/User/ProfileFormType.php create mode 100644 src/Resources/scaffolds/6.0/profile/templates/user/profile.html.twig create mode 100644 src/Resources/scaffolds/6.0/profile/tests/Functional/User/ProfileTest.php create mode 100644 src/Resources/scaffolds/6.0/register.json create mode 100644 src/Resources/scaffolds/6.0/register/src/Controller/User/RegisterController.php create mode 100644 src/Resources/scaffolds/6.0/register/src/Entity/User.php create mode 100644 src/Resources/scaffolds/6.0/register/src/Form/User/RegistrationFormType.php create mode 100644 src/Resources/scaffolds/6.0/register/templates/user/register.html.twig create mode 100644 src/Resources/scaffolds/6.0/register/tests/Functional/User/RegisterTest.php create mode 100644 src/Resources/scaffolds/6.0/reset-password.json create mode 100644 src/Resources/scaffolds/6.0/reset-password/config/packages/reset_password.yaml create mode 100644 src/Resources/scaffolds/6.0/reset-password/src/Controller/ResetPasswordController.php create mode 100644 src/Resources/scaffolds/6.0/reset-password/src/Entity/ResetPasswordRequest.php create mode 100644 src/Resources/scaffolds/6.0/reset-password/src/Factory/ResetPasswordRequestFactory.php create mode 100644 src/Resources/scaffolds/6.0/reset-password/src/Form/ResetPassword/RequestResetFormType.php create mode 100644 src/Resources/scaffolds/6.0/reset-password/src/Form/ResetPassword/ResetPasswordFormType.php create mode 100644 src/Resources/scaffolds/6.0/reset-password/src/Repository/ResetPasswordRequestRepository.php create mode 100644 src/Resources/scaffolds/6.0/reset-password/templates/reset_password/check_email.html.twig create mode 100644 src/Resources/scaffolds/6.0/reset-password/templates/reset_password/email.html.twig create mode 100644 src/Resources/scaffolds/6.0/reset-password/templates/reset_password/request.html.twig create mode 100644 src/Resources/scaffolds/6.0/reset-password/templates/reset_password/reset.html.twig create mode 100644 src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php create mode 100644 src/Resources/scaffolds/6.0/user.json create mode 100644 src/Resources/scaffolds/6.0/user/src/Entity/User.php create mode 100644 src/Resources/scaffolds/6.0/user/src/Factory/UserFactory.php create mode 100644 src/Resources/scaffolds/6.0/user/src/Repository/UserRepository.php create mode 100644 src/Resources/scaffolds/6.0/user/tests/Unit/Entity/UserTest.php create mode 100644 tests/Maker/MakeScaffoldTest.php diff --git a/src/Maker/MakeScaffold.php b/src/Maker/MakeScaffold.php new file mode 100644 index 000000000..6c9c31dfe --- /dev/null +++ b/src/Maker/MakeScaffold.php @@ -0,0 +1,183 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MakerBundle\Maker; + +use Composer\InstalledVersions; +use Symfony\Bundle\MakerBundle\ConsoleStyle; +use Symfony\Bundle\MakerBundle\DependencyBuilder; +use Symfony\Bundle\MakerBundle\Generator; +use Symfony\Bundle\MakerBundle\InputConfiguration; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Finder\Finder; +use Symfony\Component\HttpKernel\Kernel; +use Symfony\Component\Process\Process; + +/** + * @author Kevin Bond + */ +final class MakeScaffold extends AbstractMaker +{ + private $projectDir; + private $availableScaffolds; + private $installedScaffolds = []; + private $installedPackages = []; + + public function __construct($projectDir) + { + $this->projectDir = $projectDir; + } + + public static function getCommandName(): string + { + return 'make:scaffold'; + } + + public static function getCommandDescription(): string + { + return 'Create scaffold'; // todo + } + + public function configureCommand(Command $command, InputConfiguration $inputConfig): void + { + $command + ->addArgument('name', InputArgument::OPTIONAL|InputArgument::IS_ARRAY, 'Scaffold name(s) to create') + ; + + $inputConfig->setArgumentAsNonInteractive('name'); + } + + public function configureDependencies(DependencyBuilder $dependencies): void + { + $dependencies->addClassDependency(Process::class, 'process'); + } + + public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void + { + $names = $input->getArgument('name'); + + if (!$names) { + throw new \InvalidArgumentException('You must select at least one scaffold.'); + } + + foreach ($names as $name) { + $this->generateScaffold($name, $io); + } + } + + public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void + { + if ($input->getArgument('name')) { + return; + } + + $availableScaffolds = array_combine( + array_keys($this->availableScaffolds()), + array_map(fn(array $scaffold) => $scaffold['description'], $this->availableScaffolds()) + ); + + $input->setArgument('name', [$io->choice('Available scaffolds', $availableScaffolds)]); + } + + private function generateScaffold(string $name, ConsoleStyle $io): void + { + if ($this->isScaffoldInstalled($name)) { + return; + } + + if (!isset($this->availableScaffolds()[$name])) { + throw new \InvalidArgumentException("Scaffold \"{$name}\" does not exist for your version of Symfony."); + } + + $scaffold = $this->availableScaffolds()[$name]; + + // install dependent scaffolds + foreach ($scaffold['dependents'] ?? [] as $dependent) { + $this->generateScaffold($dependent, $io); + } + + $io->text("Generating {$name} Scaffold..."); + + // install required packages + foreach ($scaffold['packages'] ?? [] as $package => $env) { + if (!$this->isPackageInstalled($package)) { + $io->text("Installing {$package}..."); + + // todo composer bin detection + $command = ['composer', 'require', '--no-scripts', 'dev' === $env ? '--dev' : null, $package]; + $process = new Process(array_filter($command), $this->projectDir); + + $process->run(); + + if (!$process->isSuccessful()) { + throw new \RuntimeException("Error installing \"{$package}\"."); + } + + $this->installedPackages[] = $package; + } + } + + $io->text('Copying scaffold files...'); + + (new Filesystem())->mirror($scaffold['dir'], $this->projectDir, null, ['override' => true]); + + $io->text("Successfully installed scaffold {$name}."); + $io->newLine(); + + $this->installedScaffolds[] = $name; + } + + private function availableScaffolds(): array + { + if (is_array($this->availableScaffolds)) { + return $this->availableScaffolds; + } + + $this->availableScaffolds = []; + $finder = Finder::create() + // todo, improve versioning system + ->in(\sprintf('%s/../Resources/scaffolds/%s.0', __DIR__, Kernel::MAJOR_VERSION)) + ->name('*.json') + ; + + foreach ($finder as $file) { + $name = $file->getFilenameWithoutExtension(); + + $this->availableScaffolds[$name] = array_merge( + json_decode(file_get_contents($file), true), + ['dir' => dirname($file->getRealPath()).'/'.$name] + ); + } + + return $this->availableScaffolds; + } + + /** + * Detect if package already installed or installed in this process + * (when installing multiple scaffolds at once). + */ + private function isPackageInstalled(string $package): bool + { + return InstalledVersions::isInstalled($package) || \in_array($package, $this->installedPackages, true); + } + + /** + * Detect if package is installed in the same process (when installing + * multiple scaffolds at once). + */ + private function isScaffoldInstalled(string $name): bool + { + return \in_array($name, $this->installedScaffolds, true); + } +} diff --git a/src/Resources/config/makers.xml b/src/Resources/config/makers.xml index 624a230e3..80b9f092a 100644 --- a/src/Resources/config/makers.xml +++ b/src/Resources/config/makers.xml @@ -25,6 +25,11 @@ + + %kernel.project_dir% + + + diff --git a/src/Resources/scaffolds/6.0/auth.json b/src/Resources/scaffolds/6.0/auth.json new file mode 100644 index 000000000..810935c8c --- /dev/null +++ b/src/Resources/scaffolds/6.0/auth.json @@ -0,0 +1,10 @@ +{ + "description": "Create login form and tests.", + "dependents": [ + "homepage", + "user" + ], + "packages": { + "profiler": "dev" + } +} diff --git a/src/Resources/scaffolds/6.0/auth/config/packages/security.yaml b/src/Resources/scaffolds/6.0/auth/config/packages/security.yaml new file mode 100644 index 000000000..f2a480054 --- /dev/null +++ b/src/Resources/scaffolds/6.0/auth/config/packages/security.yaml @@ -0,0 +1,58 @@ +security: + enable_authenticator_manager: true + # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords + password_hashers: + Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' + # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider + providers: + # used to reload user from session & other features (e.g. switch_user) + app_user_provider: + entity: + class: App\Entity\User + property: email + firewalls: + dev: + pattern: ^/(_(profiler|wdt)|css|images|js)/ + security: false + main: + lazy: true + provider: app_user_provider + form_login: + login_path: login + check_path: login + username_parameter: email + password_parameter: password + enable_csrf: true + logout: + path: logout + # where to redirect after logout + target: homepage + remember_me: + secret: '%kernel.secret%' + secure: auto + samesite: lax + + # activate different ways to authenticate + # https://symfony.com/doc/current/security.html#the-firewall + + # https://symfony.com/doc/current/security/impersonating_user.html + # switch_user: true + + # Easy way to control access for large sections of your site + # Note: Only the *first* access control that matches will be used + access_control: + # - { path: ^/admin, roles: ROLE_ADMIN } + # - { path: ^/profile, roles: ROLE_USER } + +when@test: + security: + password_hashers: + # By default, password hashers are resource intensive and take time. This is + # important to generate secure password hashes. In tests however, secure hashes + # are not important, waste resources and increase test times. The following + # reduces the work factor to the lowest possible values. + Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: + algorithm: auto + cost: 4 # Lowest possible value for bcrypt + time_cost: 3 # Lowest possible value for argon + memory_cost: 10 # Lowest possible value for argon diff --git a/src/Resources/scaffolds/6.0/auth/config/services.yaml b/src/Resources/scaffolds/6.0/auth/config/services.yaml new file mode 100644 index 000000000..198b98572 --- /dev/null +++ b/src/Resources/scaffolds/6.0/auth/config/services.yaml @@ -0,0 +1,28 @@ +# This file is the entry point to configure your own services. +# Files in the packages/ subdirectory configure your dependencies. + +# Put parameters here that don't need to change on each machine where the app is deployed +# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration +parameters: + +services: + # default configuration for services in *this* file + _defaults: + autowire: true # Automatically injects dependencies in your services. + autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. + + # makes classes in src/ available to be used as services + # this creates a service per class whose id is the fully-qualified class name + App\: + resource: '../src/' + exclude: + - '../src/DependencyInjection/' + - '../src/Entity/' + - '../src/Kernel.php' + + # add more service definitions when explicit configuration is needed + # please note that last definitions always *replace* previous ones + + # programmatic user login service + App\Security\LoginUser: + $authenticator: '@security.authenticator.form_login.main' diff --git a/src/Resources/scaffolds/6.0/auth/src/Controller/SecurityController.php b/src/Resources/scaffolds/6.0/auth/src/Controller/SecurityController.php new file mode 100644 index 000000000..2e0b2e385 --- /dev/null +++ b/src/Resources/scaffolds/6.0/auth/src/Controller/SecurityController.php @@ -0,0 +1,35 @@ +isGranted('IS_AUTHENTICATED_FULLY')) { + return new RedirectResponse($request->query->get('target', $this->generateUrl('homepage'))); + } + + // get the login error if there is one + $error = $authenticationUtils->getLastAuthenticationError(); + + // current user's username (if already logged in) or last username entered by the user + $lastUsername = $this->getUser()?->getUserIdentifier() ?? $authenticationUtils->getLastUsername(); + + return $this->render('login.html.twig', ['last_username' => $lastUsername, 'error' => $error]); + } + + #[Route(path: '/logout', name: 'logout')] + public function logout(): void + { + throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.'); + } +} diff --git a/src/Resources/scaffolds/6.0/auth/src/Security/LoginUser.php b/src/Resources/scaffolds/6.0/auth/src/Security/LoginUser.php new file mode 100644 index 000000000..7eca75d72 --- /dev/null +++ b/src/Resources/scaffolds/6.0/auth/src/Security/LoginUser.php @@ -0,0 +1,22 @@ +userAuthenticator->authenticateUser($user, $this->authenticator, $request); + } +} diff --git a/src/Resources/scaffolds/6.0/auth/templates/login.html.twig b/src/Resources/scaffolds/6.0/auth/templates/login.html.twig new file mode 100644 index 000000000..c0cb0c3b6 --- /dev/null +++ b/src/Resources/scaffolds/6.0/auth/templates/login.html.twig @@ -0,0 +1,31 @@ +{% extends 'base.html.twig' %} + +{% block title %}Log in!{% endblock %} + +{% block body %} +
+ {% if error %} +
{{ error.messageKey|trans(error.messageData, 'security') }}
+ {% endif %} + +

Please sign in

+ + + + + +
+ +
+ + {% if app.request.query.has('target') %} + + {% endif %} + + +
+{% endblock %} diff --git a/src/Resources/scaffolds/6.0/auth/tests/Browser/Authentication.php b/src/Resources/scaffolds/6.0/auth/tests/Browser/Authentication.php new file mode 100644 index 000000000..576c31b81 --- /dev/null +++ b/src/Resources/scaffolds/6.0/auth/tests/Browser/Authentication.php @@ -0,0 +1,61 @@ +collector()->isAuthenticated()); + }; + } + + public static function assertAuthenticatedAs(string $email): \Closure + { + return static function(self $auth) use ($email) { + $collector = $auth->collector(); + + Assert::assertTrue($collector->isAuthenticated()); + Assert::assertSame($email, $collector->getUser()); + }; + } + + public static function assertNotAuthenticated(): \Closure + { + return static function(self $auth) { + Assert::assertFalse($auth->collector()->isAuthenticated()); + }; + } + + public static function expireSession(): \Closure + { + return static function(CookieJar $cookies) { + $cookies->expire('MOCKSESSID'); + }; + } + + private function collector(): SecurityDataCollector + { + $browser = $this->browser(); + + assert($browser instanceof KernelBrowser); + + $collector = $browser + ->withProfiling() + ->visit('/') + ->profile() + ->getCollector('security') + ; + + assert($collector instanceof SecurityDataCollector); + + return $collector; + } +} diff --git a/src/Resources/scaffolds/6.0/auth/tests/Functional/AuthenticationTest.php b/src/Resources/scaffolds/6.0/auth/tests/Functional/AuthenticationTest.php new file mode 100644 index 000000000..2cd26ad0f --- /dev/null +++ b/src/Resources/scaffolds/6.0/auth/tests/Functional/AuthenticationTest.php @@ -0,0 +1,260 @@ + 'mary@example.com', 'password' => '1234']); + + $this->browser() + ->use(Authentication::assertNotAuthenticated()) + ->visit('/login') + ->fillField('Email', 'mary@example.com') + ->fillField('Password', '1234') + ->click('Sign in') + ->assertOn('/') + ->assertSuccessful() + ->use(Authentication::assertAuthenticatedAs('mary@example.com')) + ->visit('/logout') + ->assertOn('/') + ->use(Authentication::assertNotAuthenticated()) + ; + } + + /** + * @test + */ + public function login_with_target(): void + { + UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); + + $this->browser() + ->use(Authentication::assertNotAuthenticated()) + ->visit('/login?target=/some/page') + ->fillField('Email', 'mary@example.com') + ->fillField('Password', '1234') + ->click('Sign in') + ->assertOn('/some/page') + ->use(Authentication::assertAuthenticatedAs('mary@example.com')) + ; + } + + /** + * @test + */ + public function login_with_invalid_password(): void + { + UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); + + $this->browser() + ->visit('/login') + ->fillField('Email', 'mary@example.com') + ->fillField('Password', 'invalid') + ->click('Sign in') + ->assertOn('/login') + ->assertSuccessful() + ->assertFieldEquals('Email', 'mary@example.com') + ->assertSee('Invalid credentials.') + ->use(Authentication::assertNotAuthenticated()) + ; + } + + /** + * @test + */ + public function login_with_invalid_email(): void + { + $this->browser() + ->visit('/login') + ->fillField('Email', 'invalid@example.com') + ->fillField('Password', '1234') + ->click('Sign in') + ->assertOn('/login') + ->assertSuccessful() + ->assertFieldEquals('Email', 'invalid@example.com') + ->assertSee('Invalid credentials.') + ->use(Authentication::assertNotAuthenticated()) + ; + } + + /** + * @test + */ + public function login_with_invalid_csrf(): void + { + UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); + + $this->browser() + ->use(Authentication::assertNotAuthenticated()) + ->post('/login', ['body' => ['email' => 'mary@example.com', 'password' => '1234']]) + ->assertOn('/login') + ->assertSuccessful() + ->assertSee('Invalid CSRF token.') + ->use(Authentication::assertNotAuthenticated()) + ; + } + + /** + * @test + */ + public function remember_me_enabled_by_default(): void + { + UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); + + $this->browser() + ->visit('/login') + ->fillField('Email', 'mary@example.com') + ->fillField('Password', '1234') + ->click('Sign in') + ->assertOn('/') + ->assertSuccessful() + ->use(Authentication::assertAuthenticatedAs('mary@example.com')) + ->use(Authentication::expireSession()) + ->use(Authentication::assertAuthenticatedAs('mary@example.com')) + ; + } + + /** + * @test + */ + public function can_disable_remember_me(): void + { + UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); + + $this->browser() + ->visit('/login') + ->fillField('Email', 'mary@example.com') + ->fillField('Password', '1234') + ->uncheckField('Remember me') + ->click('Sign in') + ->assertOn('/') + ->assertSuccessful() + ->use(Authentication::assertAuthenticatedAs('mary@example.com')) + ->use(Authentication::expireSession()) + ->use(Authentication::assertNotAuthenticated()) + ; + } + + /** + * @test + */ + public function fully_authenticated_login_redirect(): void + { + UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); + + $this->browser() + ->visit('/login') + ->fillField('Email', 'mary@example.com') + ->fillField('Password', '1234') + ->click('Sign in') + ->assertOn('/') + ->use(Authentication::assertAuthenticated()) + ->visit('/login') + ->assertOn('/') + ->use(Authentication::assertAuthenticated()) + ; + } + + /** + * @test + */ + public function fully_authenticated_login_target(): void + { + UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); + + $this->browser() + ->visit('/login') + ->fillField('Email', 'mary@example.com') + ->fillField('Password', '1234') + ->click('Sign in') + ->assertOn('/') + ->use(Authentication::assertAuthenticated()) + ->visit('/login?target=/some/page') + ->assertOn('/some/page') + ->use(Authentication::assertAuthenticated()) + ; + } + + /** + * @test + */ + public function can_fully_authenticate_if_only_remembered(): void + { + UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); + + $this->browser() + ->visit('/login') + ->fillField('Email', 'mary@example.com') + ->fillField('Password', '1234') + ->click('Sign in') + ->assertOn('/') + ->use(Authentication::assertAuthenticatedAs('mary@example.com')) + ->use(Authentication::expireSession()) + ->visit('/login') + ->assertOn('/login') + ->fillField('Password', '1234') + ->click('Sign in') + ->assertOn('/') + ->use(Authentication::assertAuthenticatedAs('mary@example.com')) + ; + } + + /** + * @test + */ + public function legacy_password_hash_is_automatically_migrated_on_login(): void + { + $user = UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); + + // set the password to a legacy hash (argon2id, 1234) + $user->setPassword('$argon2id$v=19$m=10,t=3,p=1$K9AFR15goJiUD6AdpK0a6Q$RsP6y+FRnYUBovBmhVZO7wN6Caj2eI8dMTnm3+5aTxk'); + $user->save(); + + $this->assertSame(\PASSWORD_ARGON2ID, \password_get_info($user->getPassword())['algo']); + + $this->browser() + ->use(Authentication::assertNotAuthenticated()) + ->visit('/login') + ->fillField('Email', 'mary@example.com') + ->fillField('Password', '1234') + ->click('Sign in') + ->assertOn('/') + ->assertSuccessful() + ->use(Authentication::assertAuthenticatedAs('mary@example.com')) + ; + + $this->assertSame(\PASSWORD_DEFAULT, \password_get_info($user->getPassword())['algo']); + } + + /** + * @test + */ + public function auto_redirected_to_authenticated_resource_after_login(): void + { + // complete this test when you have a page that requires authentication + $this->markTestIncomplete(); + } + + /** + * @test + */ + public function auto_redirected_to_fully_authenticated_resource_after_fully_authenticated(): void + { + // complete this test when/if you have a page that requires the user be "fully authenticated" + $this->markTestIncomplete(); + } +} diff --git a/src/Resources/scaffolds/6.0/change-password.json b/src/Resources/scaffolds/6.0/change-password.json new file mode 100644 index 000000000..89c310704 --- /dev/null +++ b/src/Resources/scaffolds/6.0/change-password.json @@ -0,0 +1,10 @@ +{ + "description": "Create change password form and tests.", + "dependents": [ + "auth" + ], + "packages": { + "form": "all", + "validator": "all" + } +} diff --git a/src/Resources/scaffolds/6.0/change-password/src/Controller/User/ChangePasswordController.php b/src/Resources/scaffolds/6.0/change-password/src/Controller/User/ChangePasswordController.php new file mode 100644 index 000000000..2d8084427 --- /dev/null +++ b/src/Resources/scaffolds/6.0/change-password/src/Controller/User/ChangePasswordController.php @@ -0,0 +1,55 @@ +createAccessDeniedException(); + } + + if (!$user instanceof User) { + throw new \LogicException('Invalid user type.'); + } + + $form = $this->createForm(ChangePasswordFormType::class, $user); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + // encode the plain password + $user->setPassword( + $userPasswordHasher->hashPassword( + $user, + $form->get('plainPassword')->getData() + ) + ); + + $userRepository->save($user); + $this->addFlash('success', 'You\'ve successfully changed your password.'); + + return $this->redirectToRoute('homepage'); + } + + return $this->render('user/change_password.html.twig', [ + 'changePasswordForm' => $form->createView() + ]); + } +} diff --git a/src/Resources/scaffolds/6.0/change-password/src/Form/User/ChangePasswordFormType.php b/src/Resources/scaffolds/6.0/change-password/src/Form/User/ChangePasswordFormType.php new file mode 100644 index 000000000..fa227d2ca --- /dev/null +++ b/src/Resources/scaffolds/6.0/change-password/src/Form/User/ChangePasswordFormType.php @@ -0,0 +1,57 @@ +add('currentPassword', PasswordType::class, [ + 'constraints' => [ + new UserPassword(['message' => 'This is not your current password.']), + ], + 'mapped' => false, + ]) + ->add('plainPassword', RepeatedType::class, [ + 'type' => PasswordType::class, + 'first_options' => [ + 'attr' => ['autocomplete' => 'new-password'], + 'constraints' => [ + new NotBlank([ + 'message' => 'Please enter a password.', + ]), + new Length([ + 'min' => 6, + 'minMessage' => 'Your password should be at least {{ limit }} characters', + // max length allowed by Symfony for security reasons + 'max' => 4096, + ]), + ], + 'label' => 'New password', + ], + 'second_options' => [ + 'attr' => ['autocomplete' => 'new-password'], + 'label' => 'Repeat Password', + ], + 'invalid_message' => 'The password fields must match.', + // Instead of being set onto the object directly, + // this is read and encoded in the controller + 'mapped' => false, + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + } +} diff --git a/src/Resources/scaffolds/6.0/change-password/templates/user/change_password.html.twig b/src/Resources/scaffolds/6.0/change-password/templates/user/change_password.html.twig new file mode 100644 index 000000000..99da0c874 --- /dev/null +++ b/src/Resources/scaffolds/6.0/change-password/templates/user/change_password.html.twig @@ -0,0 +1,15 @@ +{% extends 'base.html.twig' %} + +{% block title %}Change Password{% endblock %} + +{% block body %} +

Change Password

+ + {{ form_start(changePasswordForm) }} + {{ form_row(changePasswordForm.currentPassword, { label: 'Current Password' }) }} + {{ form_row(changePasswordForm.plainPassword.first, { label: 'New Password' }) }} + {{ form_row(changePasswordForm.plainPassword.second, { label: 'Repeat New Password' }) }} + + + {{ form_end(changePasswordForm) }} +{% endblock %} diff --git a/src/Resources/scaffolds/6.0/change-password/tests/Functional/User/ChangePasswordTest.php b/src/Resources/scaffolds/6.0/change-password/tests/Functional/User/ChangePasswordTest.php new file mode 100644 index 000000000..a96ced7f1 --- /dev/null +++ b/src/Resources/scaffolds/6.0/change-password/tests/Functional/User/ChangePasswordTest.php @@ -0,0 +1,178 @@ + + */ +final class ChangePasswordTest extends KernelTestCase +{ + use HasBrowser, Factories, ResetDatabase; + + /** + * @test + */ + public function can_change_password(): void + { + $user = UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); + $currentPassword = $user->getPassword(); + + $this->browser() + ->actingAs($user->object()) + ->visit('/user/change-password') + ->fillField('Current Password', '1234') + ->fillField('New Password', 'new-password') + ->fillField('Repeat New Password', 'new-password') + ->click('Change Password') + ->assertSuccessful() + ->assertOn('/') + ->assertSeeIn('.flash', 'You\'ve successfully changed your password.') + ->use(Authentication::assertAuthenticatedAs('mary@example.com')) + ->visit('/logout') + ->visit('/login') + ->fillField('Email', 'mary@example.com') + ->fillField('Password', 'new-password') + ->click('Sign in') + ->assertOn('/') + ->assertSuccessful() + ->use(Authentication::assertAuthenticatedAs('mary@example.com')) + ; + + $this->assertNotSame($currentPassword, $user->getPassword()); + } + + /** + * @test + */ + public function current_password_must_be_correct(): void + { + $user = UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); + $currentPassword = $user->getPassword(); + + $this->browser() + ->actingAs($user->object()) + ->visit('/user/change-password') + ->fillField('Current Password', 'invalid') + ->fillField('New Password', 'new-password') + ->fillField('Repeat New Password', 'new-password') + ->click('Change Password') + ->assertSuccessful() + ->assertOn('/user/change-password') + ->assertSee('This is not your current password.') + ->use(Authentication::assertAuthenticatedAs('mary@example.com')) + ; + + $this->assertSame($currentPassword, $user->getPassword()); + } + + /** + * @test + */ + public function current_password_is_required(): void + { + $user = UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); + $currentPassword = $user->getPassword(); + + $this->browser() + ->actingAs($user->object()) + ->visit('/user/change-password') + ->fillField('New Password', 'new-password') + ->fillField('Repeat New Password', 'new-password') + ->click('Change Password') + ->assertSuccessful() + ->assertOn('/user/change-password') + ->assertSee('This is not your current password.') + ->use(Authentication::assertAuthenticatedAs('mary@example.com')) + ; + + $this->assertSame($currentPassword, $user->getPassword()); + } + + /** + * @test + */ + public function new_password_is_required(): void + { + $user = UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); + $currentPassword = $user->getPassword(); + + $this->browser() + ->actingAs($user->object()) + ->visit('/user/change-password') + ->fillField('Current Password', '1234') + ->click('Change Password') + ->assertSuccessful() + ->assertOn('/user/change-password') + ->assertSee('Please enter a password.') + ->use(Authentication::assertAuthenticatedAs('mary@example.com')) + ; + + $this->assertSame($currentPassword, $user->getPassword()); + } + + /** + * @test + */ + public function new_passwords_must_match(): void + { + $user = UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); + $currentPassword = $user->getPassword(); + + $this->browser() + ->actingAs($user->object()) + ->visit('/user/change-password') + ->fillField('Current Password', '1234') + ->fillField('New Password', 'new-password') + ->fillField('Repeat New Password', 'different-new-password') + ->click('Change Password') + ->assertSuccessful() + ->assertOn('/user/change-password') + ->assertSee('The password fields must match.') + ->use(Authentication::assertAuthenticatedAs('mary@example.com')) + ; + + $this->assertSame($currentPassword, $user->getPassword()); + } + + /** + * @test + */ + public function new_password_must_be_min_length(): void + { + $user = UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); + $currentPassword = $user->getPassword(); + + $this->browser() + ->actingAs($user->object()) + ->visit('/user/change-password') + ->fillField('Current Password', '1234') + ->fillField('New Password', '4321') + ->fillField('Repeat New Password', '4321') + ->click('Change Password') + ->assertSuccessful() + ->assertOn('/user/change-password') + ->assertSee('Your password should be at least 6 characters') + ->use(Authentication::assertAuthenticatedAs('mary@example.com')) + ; + + $this->assertSame($currentPassword, $user->getPassword()); + } + + /** + * @test + */ + public function cannot_access_change_password_page_if_not_logged_in(): void + { + $this->browser() + ->visit('/user/change-password') + ->assertOn('/login') + ; + } +} diff --git a/src/Resources/scaffolds/6.0/homepage.json b/src/Resources/scaffolds/6.0/homepage.json new file mode 100644 index 000000000..8eb83473f --- /dev/null +++ b/src/Resources/scaffolds/6.0/homepage.json @@ -0,0 +1,8 @@ +{ + "description": "Create a basic homepage controller/template/test.", + "packages": { + "twig": "all", + "phpunit": "dev", + "zenstruck/browser": "dev" + } +} diff --git a/src/Resources/scaffolds/6.0/homepage/src/Controller/HomepageController.php b/src/Resources/scaffolds/6.0/homepage/src/Controller/HomepageController.php new file mode 100644 index 000000000..fd9ace081 --- /dev/null +++ b/src/Resources/scaffolds/6.0/homepage/src/Controller/HomepageController.php @@ -0,0 +1,16 @@ +render('index.html.twig'); + } +} diff --git a/src/Resources/scaffolds/6.0/homepage/templates/index.html.twig b/src/Resources/scaffolds/6.0/homepage/templates/index.html.twig new file mode 100644 index 000000000..c11b35c7b --- /dev/null +++ b/src/Resources/scaffolds/6.0/homepage/templates/index.html.twig @@ -0,0 +1,15 @@ +{% extends 'base.html.twig' %} + +{% block title %}Home{% endblock %} + +{% block body %} +

Home

+ + {% for type, messages in app.flashes %} + {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endfor %} +{% endblock %} diff --git a/src/Resources/scaffolds/6.0/homepage/tests/Functional/HomepageTest.php b/src/Resources/scaffolds/6.0/homepage/tests/Functional/HomepageTest.php new file mode 100644 index 000000000..8faac80e3 --- /dev/null +++ b/src/Resources/scaffolds/6.0/homepage/tests/Functional/HomepageTest.php @@ -0,0 +1,22 @@ +browser() + ->visit('/') + ->assertSuccessful() + ; + } +} diff --git a/src/Resources/scaffolds/6.0/profile.json b/src/Resources/scaffolds/6.0/profile.json new file mode 100644 index 000000000..7246dedc7 --- /dev/null +++ b/src/Resources/scaffolds/6.0/profile.json @@ -0,0 +1,10 @@ +{ + "description": "Create user profile form and tests.", + "dependents": [ + "auth" + ], + "packages": { + "form": "all", + "validator": "all" + } +} diff --git a/src/Resources/scaffolds/6.0/profile/src/Controller/User/ProfileController.php b/src/Resources/scaffolds/6.0/profile/src/Controller/User/ProfileController.php new file mode 100644 index 000000000..1e943bed8 --- /dev/null +++ b/src/Resources/scaffolds/6.0/profile/src/Controller/User/ProfileController.php @@ -0,0 +1,41 @@ +createAccessDeniedException(); + } + + if (!$user instanceof User) { + throw new \LogicException('Invalid user type.'); + } + + $form = $this->createForm(ProfileFormType::class, $user); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $userRepository->save($user); + $this->addFlash('success', 'You\'ve successfully updated your profile.'); + + return $this->redirectToRoute('homepage'); + } + + return $this->render('user/profile.html.twig', [ + 'profileForm' => $form->createView(), + ]); + } +} diff --git a/src/Resources/scaffolds/6.0/profile/src/Form/User/ProfileFormType.php b/src/Resources/scaffolds/6.0/profile/src/Form/User/ProfileFormType.php new file mode 100644 index 000000000..2a88fe4f6 --- /dev/null +++ b/src/Resources/scaffolds/6.0/profile/src/Form/User/ProfileFormType.php @@ -0,0 +1,30 @@ +add('name', null, [ + 'constraints' => [ + new NotBlank(['message' => 'Name is required']), + ], + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => User::class, + ]); + } +} diff --git a/src/Resources/scaffolds/6.0/profile/templates/user/profile.html.twig b/src/Resources/scaffolds/6.0/profile/templates/user/profile.html.twig new file mode 100644 index 000000000..a2bae421f --- /dev/null +++ b/src/Resources/scaffolds/6.0/profile/templates/user/profile.html.twig @@ -0,0 +1,13 @@ +{% extends 'base.html.twig' %} + +{% block title %}Manage Profile{% endblock %} + +{% block body %} +

Manage Profile

+ + {{ form_start(profileForm) }} + {{ form_row(profileForm.name) }} + + + {{ form_end(profileForm) }} +{% endblock %} diff --git a/src/Resources/scaffolds/6.0/profile/tests/Functional/User/ProfileTest.php b/src/Resources/scaffolds/6.0/profile/tests/Functional/User/ProfileTest.php new file mode 100644 index 000000000..26afef8c3 --- /dev/null +++ b/src/Resources/scaffolds/6.0/profile/tests/Functional/User/ProfileTest.php @@ -0,0 +1,73 @@ + + */ +final class ProfileTest extends KernelTestCase +{ + use HasBrowser, Factories, ResetDatabase; + + /** + * @test + */ + public function can_update_profile(): void + { + $user = UserFactory::createOne(['name' => 'Mary Edwards']); + + $this->assertSame('Mary Edwards', $user->getName()); + + $this->browser() + ->actingAs($user->object()) + ->visit('/user') + ->assertFieldEquals('Name', 'Mary Edwards') + ->fillField('Name', 'John Smith') + ->click('Save') + ->assertOn('/') + ->assertSuccessful() + ->assertSeeIn('.flash', 'You\'ve successfully updated your profile.') + ; + + $this->assertSame('John Smith', $user->getName()); + } + + /** + * @test + */ + public function name_is_required(): void + { + $user = UserFactory::createOne(['name' => 'Mary Edwards']); + + UserFactory::assert()->exists(['name' => 'Mary Edwards']); + + $this->browser() + ->actingAs($user->object()) + ->visit('/user') + ->fillField('Name', '') + ->click('Save') + ->assertOn('/user') + ->assertSuccessful() + ->assertSee('Name is required') + ; + + UserFactory::assert()->exists(['name' => 'Mary Edwards']); + } + + /** + * @test + */ + public function cannot_access_profile_page_if_not_logged_in(): void + { + $this->browser() + ->visit('/user') + ->assertOn('/login') + ; + } +} diff --git a/src/Resources/scaffolds/6.0/register.json b/src/Resources/scaffolds/6.0/register.json new file mode 100644 index 000000000..015a2f497 --- /dev/null +++ b/src/Resources/scaffolds/6.0/register.json @@ -0,0 +1,10 @@ +{ + "description": "Create registration form and tests.", + "dependents": [ + "auth" + ], + "packages": { + "form": "all", + "validator": "all" + } +} diff --git a/src/Resources/scaffolds/6.0/register/src/Controller/User/RegisterController.php b/src/Resources/scaffolds/6.0/register/src/Controller/User/RegisterController.php new file mode 100644 index 000000000..b1b317011 --- /dev/null +++ b/src/Resources/scaffolds/6.0/register/src/Controller/User/RegisterController.php @@ -0,0 +1,52 @@ +createForm(RegistrationFormType::class, $user); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + // encode the plain password + $user->setPassword( + $userPasswordHasher->hashPassword( + $user, + $form->get('plainPassword')->getData() + ) + ); + + $userRepository->save($user); + // do anything else you need here, like send an email + + // authenticate the user + $login($user, $request); + $this->addFlash('success', 'You\'ve successfully registered and are now logged in.'); + + return $this->redirectToRoute('homepage'); + } + + return $this->render('user/register.html.twig', [ + 'registrationForm' => $form->createView(), + ]); + } +} diff --git a/src/Resources/scaffolds/6.0/register/src/Entity/User.php b/src/Resources/scaffolds/6.0/register/src/Entity/User.php new file mode 100644 index 000000000..5579a6115 --- /dev/null +++ b/src/Resources/scaffolds/6.0/register/src/Entity/User.php @@ -0,0 +1,113 @@ +id; + } + + public function getEmail(): ?string + { + return $this->email; + } + + public function setEmail(string $email): self + { + $this->email = $email; + + return $this; + } + + /** + * A visual identifier that represents this user. + * + * @see UserInterface + */ + public function getUserIdentifier(): string + { + return (string) $this->email; + } + + /** + * @see UserInterface + */ + public function getRoles(): array + { + $roles = $this->roles; + // guarantee every user at least has ROLE_USER + $roles[] = 'ROLE_USER'; + + return array_unique($roles); + } + + public function setRoles(array $roles): self + { + $this->roles = $roles; + + return $this; + } + + /** + * @see PasswordAuthenticatedUserInterface + */ + public function getPassword(): string + { + return $this->password; + } + + public function setPassword(string $password): self + { + $this->password = $password; + + return $this; + } + + /** + * @see UserInterface + */ + public function eraseCredentials() + { + // If you store any temporary, sensitive data on the user, clear it here + // $this->plainPassword = null; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(?string $name): self + { + $this->name = $name; + + return $this; + } +} diff --git a/src/Resources/scaffolds/6.0/register/src/Form/User/RegistrationFormType.php b/src/Resources/scaffolds/6.0/register/src/Form/User/RegistrationFormType.php new file mode 100644 index 000000000..a3a6f7e14 --- /dev/null +++ b/src/Resources/scaffolds/6.0/register/src/Form/User/RegistrationFormType.php @@ -0,0 +1,56 @@ +add('name', null, [ + 'constraints' => [ + new NotBlank(['message' => 'Name is required']), + ], + ]) + ->add('email', null, [ + 'constraints' => [ + new NotBlank(['message' => 'Email is required']), + new Email(['message' => 'This is not a valid email address']), + ], + ]) + ->add('plainPassword', PasswordType::class, [ + // instead of being set onto the object directly, + // this is read and encoded in the controller + 'mapped' => false, + 'attr' => ['autocomplete' => 'new-password'], + 'constraints' => [ + new NotBlank([ + 'message' => 'Please enter a password', + ]), + new Length([ + 'min' => 6, + 'minMessage' => 'Your password should be at least {{ limit }} characters', + // max length allowed by Symfony for security reasons + 'max' => 4096, + ]), + ], + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => User::class, + ]); + } +} diff --git a/src/Resources/scaffolds/6.0/register/templates/user/register.html.twig b/src/Resources/scaffolds/6.0/register/templates/user/register.html.twig new file mode 100644 index 000000000..35b032945 --- /dev/null +++ b/src/Resources/scaffolds/6.0/register/templates/user/register.html.twig @@ -0,0 +1,17 @@ +{% extends 'base.html.twig' %} + +{% block title %}Register{% endblock %} + +{% block body %} +

Register

+ + {{ form_start(registrationForm) }} + {{ form_row(registrationForm.name) }} + {{ form_row(registrationForm.email) }} + {{ form_row(registrationForm.plainPassword, { + label: 'Password' + }) }} + + + {{ form_end(registrationForm) }} +{% endblock %} diff --git a/src/Resources/scaffolds/6.0/register/tests/Functional/User/RegisterTest.php b/src/Resources/scaffolds/6.0/register/tests/Functional/User/RegisterTest.php new file mode 100644 index 000000000..c213f1fa0 --- /dev/null +++ b/src/Resources/scaffolds/6.0/register/tests/Functional/User/RegisterTest.php @@ -0,0 +1,159 @@ +empty(); + + $this->browser() + ->visit('/register') + ->assertSuccessful() + ->fillField('Name', 'Madison') + ->fillField('Email', 'madison@example.com') + ->fillField('Password', 'password') + ->click('Register') + ->assertOn('/') + ->assertSeeIn('.flash', 'You\'ve successfully registered and are now logged in.') + ->use(Authentication::assertAuthenticatedAs('madison@example.com')) + ->visit('/logout') + ->use(Authentication::assertNotAuthenticated()) + ->visit('/login') + ->fillField('Email', 'madison@example.com') + ->fillField('Password', 'password') + ->click('Sign in') + ->assertOn('/') + ->use(Authentication::assertAuthenticatedAs('madison@example.com')) + ; + + UserFactory::assert()->count(1); + UserFactory::assert()->exists(['name' => 'Madison', 'email' => 'madison@example.com']); + } + + /** + * @test + */ + public function name_is_required(): void + { + $this->browser() + ->throwExceptions() + ->visit('/register') + ->assertSuccessful() + ->fillField('Email', 'madison@example.com') + ->fillField('Password', 'password') + ->click('Register') + ->assertOn('/register') + ->assertSee('Name is required') + ->use(Authentication::assertNotAuthenticated()) + ; + } + + /** + * @test + */ + public function email_is_required(): void + { + $this->browser() + ->throwExceptions() + ->visit('/register') + ->assertSuccessful() + ->fillField('Name', 'Madison') + ->fillField('Password', 'password') + ->click('Register') + ->assertOn('/register') + ->assertSee('Email is required') + ->use(Authentication::assertNotAuthenticated()) + ; + } + + /** + * @test + */ + public function email_must_be_email_address(): void + { + $this->browser() + ->throwExceptions() + ->visit('/register') + ->assertSuccessful() + ->fillField('Name', 'Madison') + ->fillField('Email', 'invalid-email') + ->fillField('Password', 'password') + ->click('Register') + ->assertOn('/register') + ->assertSee('This is not a valid email address') + ->use(Authentication::assertNotAuthenticated()) + ; + } + + /** + * @test + */ + public function email_must_be_unique(): void + { + UserFactory::createOne(['email' => 'madison@example.com']); + + $this->browser() + ->throwExceptions() + ->visit('/register') + ->assertSuccessful() + ->fillField('Name', 'Madison') + ->fillField('Email', 'madison@example.com') + ->fillField('Password', 'password') + ->click('Register') + ->assertOn('/register') + ->assertSee('There is already an account with this email') + ->use(Authentication::assertNotAuthenticated()) + ; + } + + /** + * @test + */ + public function password_is_required(): void + { + $this->browser() + ->throwExceptions() + ->visit('/register') + ->assertSuccessful() + ->fillField('Name', 'Madison') + ->fillField('Email', 'madison@example.com') + ->click('Register') + ->assertOn('/register') + ->assertSee('Please enter a password') + ->use(Authentication::assertNotAuthenticated()) + ; + } + + /** + * @test + */ + public function password_must_be_min_length(): void + { + $this->browser() + ->throwExceptions() + ->visit('/register') + ->assertSuccessful() + ->fillField('Name', 'Madison') + ->fillField('Email', 'madison@example.com') + ->fillField('Password', '1234') + ->click('Register') + ->assertOn('/register') + ->assertSee('Your password should be at least 6 characters') + ->use(Authentication::assertNotAuthenticated()) + ; + } +} diff --git a/src/Resources/scaffolds/6.0/reset-password.json b/src/Resources/scaffolds/6.0/reset-password.json new file mode 100644 index 000000000..0626bd699 --- /dev/null +++ b/src/Resources/scaffolds/6.0/reset-password.json @@ -0,0 +1,13 @@ +{ + "description": "Reset password system using symfonycasts/reset-password-bundle with tests.", + "dependents": [ + "auth" + ], + "packages": { + "form": "all", + "validator": "all", + "mailer": "all", + "symfonycasts/reset-password-bundle": "all", + "zenstruck/mailer-test": "dev" + } +} diff --git a/src/Resources/scaffolds/6.0/reset-password/config/packages/reset_password.yaml b/src/Resources/scaffolds/6.0/reset-password/config/packages/reset_password.yaml new file mode 100644 index 000000000..fdc6a4bc4 --- /dev/null +++ b/src/Resources/scaffolds/6.0/reset-password/config/packages/reset_password.yaml @@ -0,0 +1,4 @@ +symfonycasts_reset_password: + request_password_repository: App\Repository\ResetPasswordRequestRepository + throttle_limit: 900 # 15 minutes + lifetime: 3600 # 1 hour diff --git a/src/Resources/scaffolds/6.0/reset-password/src/Controller/ResetPasswordController.php b/src/Resources/scaffolds/6.0/reset-password/src/Controller/ResetPasswordController.php new file mode 100644 index 000000000..834c8c2f6 --- /dev/null +++ b/src/Resources/scaffolds/6.0/reset-password/src/Controller/ResetPasswordController.php @@ -0,0 +1,188 @@ +createForm(RequestResetFormType::class); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + return $this->processSendingPasswordResetEmail( + $form->get('email')->getData(), + $mailer + ); + } + + return $this->render('reset_password/request.html.twig', [ + 'requestForm' => $form->createView(), + ]); + } + + /** + * Confirmation page after a user has requested a password reset. + */ + #[Route('/check-email', name: 'reset_password_check_email')] + public function checkEmail(): Response + { + // Generate a fake token if the user does not exist or someone hit this page directly. + // This prevents exposing whether or not a user was found with the given email address or not + if (null === ($resetToken = $this->getTokenObjectFromSession())) { + $resetToken = $this->resetPasswordHelper->generateFakeResetToken(); + } + + return $this->render('reset_password/check_email.html.twig', [ + 'resetToken' => $resetToken, + ]); + } + + /** + * Validates and process the reset URL that the user clicked in their email. + */ + #[Route('/reset/{token}', name: 'reset_password')] + public function reset(Request $request, LoginUser $login, UserPasswordHasherInterface $userPasswordHasher, string $token = null): Response + { + if ($token) { + // We store the token in session and remove it from the URL, to avoid the URL being + // loaded in a browser and potentially leaking the token to 3rd party JavaScript. + $this->storeTokenInSession($token); + + return $this->redirectToRoute('reset_password'); + } + + if (null === $token = $this->getTokenFromSession()) { + throw $this->createNotFoundException('No reset password token found in the URL or in the session.'); + } + + try { + /** @var User $user */ + $user = $this->resetPasswordHelper->validateTokenAndFetchUser($token); + } catch (ResetPasswordExceptionInterface $e) { + $this->addFlash('error', $e->getReason()); + + return $this->redirectToRoute('homepage'); + } + + // The token is valid; allow the user to change their password. + $form = $this->createForm(ResetPasswordFormType::class); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + // A password reset token should be used only once, remove it. + $this->resetPasswordHelper->removeResetRequest($token); + + // Encode(hash) the plain password, and set it. + $user->setPassword($userPasswordHasher->hashPassword($user, $form->get('plainPassword')->getData())); + + $this->userRepository->save($user); + + // The session is cleaned up after the password has been changed. + $this->cleanSessionAfterReset(); + + // programmatic user login + $login($user, $request); + + $this->addFlash('success', 'Your password was successfully reset, you are now logged in.'); + + return $this->redirectToRoute('homepage'); + } + + return $this->render('reset_password/reset.html.twig', [ + 'resetForm' => $form->createView(), + ]); + } + + private function processSendingPasswordResetEmail(string $emailFormData, MailerInterface $mailer): RedirectResponse + { + $user = $this->userRepository->findOneBy([ + 'email' => $emailFormData, + ]); + + // Do not reveal whether a user account was found or not. + if (!$user) { + return $this->redirectToRoute('reset_password_check_email'); + } + + try { + $resetToken = $this->resetPasswordHelper->generateResetToken($user); + } catch (TooManyPasswordRequestsException $e) { + $this->addFlash('error', $e->getReason()); + + return $this->redirectToRoute('homepage'); + } catch (ResetPasswordExceptionInterface $e) { + // If you want to tell the user why a reset email was not sent, uncomment + // the lines below and change the redirect to 'reset_password_request'. + // Caution: This may reveal if a user is registered or not. + // + // $this->addFlash('reset_password_error', sprintf( + // '%s - %s', + // ResetPasswordExceptionInterface::MESSAGE_PROBLEM_HANDLE, + // $e->getReason() + // )); + + return $this->redirectToRoute('reset_password_check_email'); + } + + $email = (new TemplatedEmail()) + ->from(new Address('webmaster@example.com', 'Webmaster')) // todo change email/name + ->to(new Address($user->getEmail(), $user->getName())) + ->subject('Your password reset request') + ->htmlTemplate('reset_password/email.html.twig') + ->context([ + 'resetToken' => $resetToken, + ]) + ; + + $email->getHeaders() + ->add(new TagHeader('reset-password')) // https://symfony.com/doc/current/mailer.html#adding-tags-and-metadata-to-emails + ->addTextHeader('X-CTA', $this->generateUrl( // helps with testing + 'reset_password', + ['token' => $resetToken->getToken()], + UrlGeneratorInterface::ABSOLUTE_URL + )) + ; + + $mailer->send($email); + + // Store the token object in session for retrieval in check-email route. + $this->setTokenObjectInSession($resetToken); + + return $this->redirectToRoute('reset_password_check_email'); + } +} diff --git a/src/Resources/scaffolds/6.0/reset-password/src/Entity/ResetPasswordRequest.php b/src/Resources/scaffolds/6.0/reset-password/src/Entity/ResetPasswordRequest.php new file mode 100644 index 000000000..2676ef571 --- /dev/null +++ b/src/Resources/scaffolds/6.0/reset-password/src/Entity/ResetPasswordRequest.php @@ -0,0 +1,39 @@ +user = $user; + $this->initialize($expiresAt, $selector, $hashedToken); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getUser(): object + { + return $this->user; + } +} diff --git a/src/Resources/scaffolds/6.0/reset-password/src/Factory/ResetPasswordRequestFactory.php b/src/Resources/scaffolds/6.0/reset-password/src/Factory/ResetPasswordRequestFactory.php new file mode 100644 index 000000000..5696a7499 --- /dev/null +++ b/src/Resources/scaffolds/6.0/reset-password/src/Factory/ResetPasswordRequestFactory.php @@ -0,0 +1,40 @@ + + * + * @method static ResetPasswordRequest|Proxy createOne(array $attributes = []) + * @method static ResetPasswordRequest[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static ResetPasswordRequest|Proxy find(object|array|mixed $criteria) + * @method static ResetPasswordRequest|Proxy findOrCreate(array $attributes) + * @method static ResetPasswordRequest|Proxy first(string $sortedField = 'id') + * @method static ResetPasswordRequest|Proxy last(string $sortedField = 'id') + * @method static ResetPasswordRequest|Proxy random(array $attributes = []) + * @method static ResetPasswordRequest|Proxy randomOrCreate(array $attributes = []) + * @method static ResetPasswordRequest[]|Proxy[] all() + * @method static ResetPasswordRequest[]|Proxy[] findBy(array $attributes) + * @method static ResetPasswordRequest[]|Proxy[] randomSet(int $number, array $attributes = []) + * @method static ResetPasswordRequest[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @method static ResetPasswordRequestRepository|RepositoryProxy repository() + * @method ResetPasswordRequest|Proxy create(array|callable $attributes = []) + */ +final class ResetPasswordRequestFactory extends ModelFactory +{ + protected function getDefaults(): array + { + return []; + } + + protected static function getClass(): string + { + return ResetPasswordRequest::class; + } +} diff --git a/src/Resources/scaffolds/6.0/reset-password/src/Form/ResetPassword/RequestResetFormType.php b/src/Resources/scaffolds/6.0/reset-password/src/Form/ResetPassword/RequestResetFormType.php new file mode 100644 index 000000000..2a3c838e9 --- /dev/null +++ b/src/Resources/scaffolds/6.0/reset-password/src/Form/ResetPassword/RequestResetFormType.php @@ -0,0 +1,31 @@ +add('email', EmailType::class, [ + 'attr' => ['autocomplete' => 'email'], + 'constraints' => [ + new NotBlank(['message' => 'Please enter your email']), + new Email(['message' => 'This is not a valid email address']), + ], + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([]); + } +} diff --git a/src/Resources/scaffolds/6.0/reset-password/src/Form/ResetPassword/ResetPasswordFormType.php b/src/Resources/scaffolds/6.0/reset-password/src/Form/ResetPassword/ResetPasswordFormType.php new file mode 100644 index 000000000..0a794e90e --- /dev/null +++ b/src/Resources/scaffolds/6.0/reset-password/src/Form/ResetPassword/ResetPasswordFormType.php @@ -0,0 +1,51 @@ +add('plainPassword', RepeatedType::class, [ + 'type' => PasswordType::class, + 'first_options' => [ + 'attr' => ['autocomplete' => 'new-password'], + 'constraints' => [ + new NotBlank([ + 'message' => 'Please enter a password', + ]), + new Length([ + 'min' => 6, + 'minMessage' => 'Your password should be at least {{ limit }} characters', + // max length allowed by Symfony for security reasons + 'max' => 4096, + ]), + ], + 'label' => 'New password', + ], + 'second_options' => [ + 'attr' => ['autocomplete' => 'new-password'], + 'label' => 'Repeat Password', + ], + 'invalid_message' => 'The password fields must match.', + // Instead of being set onto the object directly, + // this is read and encoded in the controller + 'mapped' => false, + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([]); + } +} diff --git a/src/Resources/scaffolds/6.0/reset-password/src/Repository/ResetPasswordRequestRepository.php b/src/Resources/scaffolds/6.0/reset-password/src/Repository/ResetPasswordRequestRepository.php new file mode 100644 index 000000000..5a428b78d --- /dev/null +++ b/src/Resources/scaffolds/6.0/reset-password/src/Repository/ResetPasswordRequestRepository.php @@ -0,0 +1,31 @@ + + If an account matching your email exists, then an email was just sent that contains a link that you can use to reset your password. + This link will expire in {{ resetToken.expirationMessageKey|trans(resetToken.expirationMessageData, 'ResetPasswordBundle') }}. +

+

If you don't receive an email please check your spam folder or try again.

+{% endblock %} diff --git a/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/email.html.twig b/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/email.html.twig new file mode 100644 index 000000000..7acbbdca6 --- /dev/null +++ b/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/email.html.twig @@ -0,0 +1,9 @@ +

Hi!

+ +

To reset your password, please visit the following link

+ +{{ url('reset_password', {token: resetToken.token}) }} + +

This link will expire in {{ resetToken.expirationMessageKey|trans(resetToken.expirationMessageData, 'ResetPasswordBundle') }}.

+ +

Cheers!

diff --git a/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/request.html.twig b/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/request.html.twig new file mode 100644 index 000000000..095903052 --- /dev/null +++ b/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/request.html.twig @@ -0,0 +1,22 @@ +{% extends 'base.html.twig' %} + +{% block title %}Reset your password{% endblock %} + +{% block body %} + {% for flash_error in app.flashes('reset_password_error') %} + + {% endfor %} +

Reset your password

+ + {{ form_start(requestForm) }} + {{ form_row(requestForm.email) }} +
+ + Enter your email address and we will send you a + link to reset your password. + +
+ + + {{ form_end(requestForm) }} +{% endblock %} diff --git a/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/reset.html.twig b/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/reset.html.twig new file mode 100644 index 000000000..799aa10f5 --- /dev/null +++ b/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/reset.html.twig @@ -0,0 +1,12 @@ +{% extends 'base.html.twig' %} + +{% block title %}Reset your password{% endblock %} + +{% block body %} +

Reset your password

+ + {{ form_start(resetForm) }} + {{ form_row(resetForm.plainPassword) }} + + {{ form_end(resetForm) }} +{% endblock %} diff --git a/src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php b/src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php new file mode 100644 index 000000000..dc5b09d12 --- /dev/null +++ b/src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php @@ -0,0 +1,387 @@ + + */ +final class ResetPasswordTest extends KernelTestCase +{ + use HasBrowser, Factories, ResetDatabase, InteractsWithMailer; + + /** + * @test + */ + public function can_reset_password(): void + { + UserFactory::createOne(['email' => 'john@example.com', 'name' => 'John', 'password' => '1234']); + + $this->browser() + ->visit('/reset-password') + ->assertSuccessful() + ->fillField('Email', 'john@example.com') + ->click('Send password reset email') + ->assertOn('/reset-password/check-email') + ->assertSuccessful() + ->assertSee('Password Reset Email Sent') + ; + + $email = $this->mailer() + ->sentEmails() + ->assertCount(1) + ->first() + ; + $resetUrl = $email->getHeaders()->get('X-CTA')?->getBody(); + + $this->assertNotNull($resetUrl, 'The reset url header was not set.'); + + $email + ->assertTo('john@example.com', 'John') + ->assertContains('To reset your password, please visit the following link') + ->assertContains($resetUrl) + ->assertHasTag('reset-password') + ; + + $this->browser() + ->visit($resetUrl) + ->fillField('New password', 'new-password') + ->fillField('Repeat Password', 'new-password') + ->click('Reset password') + ->assertOn('/') + ->assertSeeIn('.flash', 'Your password was successfully reset, you are now logged in.') + ->use(Authentication::assertAuthenticatedAs('john@example.com')) + ->visit('/logout') + ->visit('/login') + ->fillField('Email', 'john@example.com') + ->fillField('Password', 'new-password') + ->click('Sign in') + ->assertOn('/') + ->use(Authentication::assertAuthenticatedAs('john@example.com')) + ; + } + + /** + * @test + */ + public function request_email_is_required(): void + { + $this->browser() + ->visit('/reset-password') + ->click('Send password reset email') + ->assertOn('/reset-password') + ->assertSee('Please enter your email') + ; + + $this->mailer()->assertNoEmailSent(); + } + + /** + * @test + */ + public function request_email_must_be_an_email(): void + { + $this->browser() + ->visit('/reset-password') + ->fillField('Email', 'invalid') + ->click('Send password reset email') + ->assertOn('/reset-password') + ->assertSee('This is not a valid email address') + ; + + $this->mailer()->assertNoEmailSent(); + } + + /** + * @test + */ + public function requests_are_throttled(): void + { + UserFactory::createOne(['email' => 'john@example.com']); + + $this->browser() + ->visit('/reset-password') + ->fillField('Email', 'john@example.com') + ->click('Send password reset email') + ->assertOn('/reset-password/check-email') + ->assertSuccessful() + ->assertSee('Password Reset Email Sent') + ->visit('/reset-password') + ->fillField('Email', 'john@example.com') + ->click('Send password reset email') + ->assertOn('/') + ->assertSeeIn('.flash', 'You have already requested a reset password email. Please check your email or try again soon.') + ; + + $this->mailer()->assertSentEmailCount(1); + + ResetPasswordRequestFactory::assert()->count(1); + } + + /** + * @test + */ + public function can_request_again_after_throttle_expires(): void + { + UserFactory::createOne(['email' => 'john@example.com']); + + $this->browser() + ->visit('/reset-password') + ->fillField('Email', 'john@example.com') + ->click('Send password reset email') + ->assertOn('/reset-password/check-email') + ->assertSuccessful() + ->assertSee('Password Reset Email Sent') + ->use(function() { + ResetPasswordRequestFactory::first() + ->forceSet('requestedAt', new \DateTimeImmutable('-16 minutes')) + ->save() + ; + }) + ->visit('/reset-password') + ->fillField('Email', 'john@example.com') + ->click('Send password reset email') + ->assertOn('/reset-password/check-email') + ->assertSuccessful() + ->assertSee('Password Reset Email Sent') + ; + + $this->mailer()->assertSentEmailCount(2); + + ResetPasswordRequestFactory::assert()->count(2); + } + + /** + * @test + */ + public function request_does_not_expose_if_user_was_not_found(): void + { + $this->browser() + ->visit('/reset-password') + ->fillField('Email', 'john@example.com') + ->click('Send password reset email') + ->assertOn('/reset-password/check-email') + ->assertSuccessful() + ->assertSee('Password Reset Email Sent') + ; + + $this->mailer()->assertNoEmailSent(); + } + + /** + * @test + */ + public function reset_password_is_required(): void + { + $user = UserFactory::createOne(['email' => 'john@example.com']); + $currentPassword = $user->getPassword(); + + $this->browser() + ->visit('/reset-password') + ->assertSuccessful() + ->fillField('Email', 'john@example.com') + ->click('Send password reset email') + ->visit($this->mailer()->sentEmails()->first()->getHeaders()->get('X-CTA')->getBody()) + ->click('Reset password') + ->assertOn('/reset-password/reset') + ->assertSee('Please enter a password') + ; + + $this->assertSame($currentPassword, UserFactory::find(['email' => 'john@example.com'])->getPassword()); + } + + /** + * @test + */ + public function reset_passwords_must_match(): void + { + $user = UserFactory::createOne(['email' => 'john@example.com']); + $currentPassword = $user->getPassword(); + + $this->browser() + ->visit('/reset-password') + ->assertSuccessful() + ->fillField('Email', 'john@example.com') + ->click('Send password reset email') + ->visit($this->mailer()->sentEmails()->first()->getHeaders()->get('X-CTA')->getBody()) + ->fillField('New password', 'new-password') + ->fillField('Repeat Password', 'mismatch-password') + ->click('Reset password') + ->assertOn('/reset-password/reset') + ->assertSee('The password fields must match.') + ; + + $this->assertSame($currentPassword, UserFactory::find(['email' => 'john@example.com'])->getPassword()); + } + + /** + * @test + */ + public function reset_password_must_be_min_length(): void + { + $user = UserFactory::createOne(['email' => 'john@example.com']); + $currentPassword = $user->getPassword(); + + $this->browser() + ->visit('/reset-password') + ->assertSuccessful() + ->fillField('Email', 'john@example.com') + ->click('Send password reset email') + ->visit($this->mailer()->sentEmails()->first()->getHeaders()->get('X-CTA')->getBody()) + ->fillField('New password', '1234') + ->fillField('Repeat Password', '1234') + ->click('Reset password') + ->assertOn('/reset-password/reset') + ->assertSee('Your password should be at least 6 characters') + ; + + $this->assertSame($currentPassword, UserFactory::find(['email' => 'john@example.com'])->getPassword()); + } + + /** + * @test + */ + public function cannot_reset_with_invalid_token(): void + { + $this->browser() + ->visit('/reset-password/reset/invalid-token') + ->assertOn('/') + ->assertSeeIn('.flash', 'The reset password link is invalid. Please try to reset your password again.') + ; + } + + /** + * @test + */ + public function can_use_old_token_even_after_requesting_another(): void + { + UserFactory::createOne(['email' => 'john@example.com']); + + $this->browser() + ->visit('/reset-password') + ->fillField('Email', 'john@example.com') + ->click('Send password reset email') + ->assertOn('/reset-password/check-email') + ->assertSuccessful() + ->assertSee('Password Reset Email Sent') + ->use(function() { + ResetPasswordRequestFactory::first() + ->forceSet('requestedAt', new \DateTimeImmutable('-16 minutes')) + ->save() + ; + }) + ->visit('/reset-password') + ->fillField('Email', 'john@example.com') + ->click('Send password reset email') + ->assertOn('/reset-password/check-email') + ->assertSuccessful() + ->assertSee('Password Reset Email Sent') + ->use(function() { + ResetPasswordRequestFactory::assert()->count(2); + }) + ->visit($this->mailer()->sentEmails()->first()->getHeaders()->get('X-CTA')->getBody()) + ->assertOn('/reset-password/reset') + ->fillField('New password', 'new-password') + ->fillField('Repeat Password', 'new-password') + ->click('Reset password') + ->assertOn('/') + ; + + ResetPasswordRequestFactory::assert()->empty(); + } + + /** + * @test + */ + public function reset_tokens_expire(): void + { + UserFactory::createOne(['email' => 'john@example.com']); + + $this->browser() + ->visit('/reset-password') + ->fillField('Email', 'john@example.com') + ->click('Send password reset email') + ->assertOn('/reset-password/check-email') + ->assertSuccessful() + ->assertSee('Password Reset Email Sent') + ->use(function() { + ResetPasswordRequestFactory::first() + ->forceSet('expiresAt', new \DateTimeImmutable('-10 minutes')) + ->save() + ; + }) + ->visit($this->mailer()->sentEmails()->first()->getHeaders()->get('X-CTA')->getBody()) + ->assertOn('/') + ->assertSuccessful() + ->assertSeeIn('.flash', 'The link in your email is expired. Please try to reset your password again.') + ; + } + + /** + * @test + */ + public function cannot_use_token_after_password_change(): void + { + UserFactory::createOne(['email' => 'john@example.com']); + + $this->browser() + ->visit('/reset-password') + ->assertSuccessful() + ->fillField('Email', 'john@example.com') + ->click('Send password reset email') + ->assertOn('/reset-password/check-email') + ->visit($resetUrl = $this->mailer()->sentEmails()->first()->getHeaders()->get('X-CTA')->getBody()) + ->fillField('New password', 'new-password') + ->fillField('Repeat Password', 'new-password') + ->click('Reset password') + ->assertOn('/') + ->assertSeeIn('.flash', 'Your password was successfully reset, you are now logged in.') + ->visit('/logout') + ->visit($resetUrl) + ->assertOn('/') + ->assertSuccessful() + ->assertSeeIn('.flash', 'The reset password link is invalid. Please try to reset your password again.') + ; + } + + /** + * @test + */ + public function old_tokens_are_garbage_collected(): void + { + $user = UserFactory::createOne(['email' => 'jane@example.com']); + + ResetPasswordRequestFactory::createOne([ + 'user' => $user, + 'selector' => 'selector', + 'hashedToken' => 'hash', + 'expiresAt' => new \DateTimeImmutable('-1 month'), + ]) + ->forceSet('requestedAt', new \DateTimeImmutable('-1 month')) + ->save() + ; + + ResetPasswordRequestFactory::assert()->exists(['selector' => 'selector']); + + $this->browser() + ->visit('/reset-password') + ->assertSuccessful() + ->fillField('Email', 'jane@example.com') + ->click('Send password reset email') + ->assertOn('/reset-password/check-email') + ; + + ResetPasswordRequestFactory::assert() + ->count(1) + ->notExists(['selector' => 'selector']) + ; + } +} diff --git a/src/Resources/scaffolds/6.0/user.json b/src/Resources/scaffolds/6.0/user.json new file mode 100644 index 000000000..8e5a009b7 --- /dev/null +++ b/src/Resources/scaffolds/6.0/user.json @@ -0,0 +1,9 @@ +{ + "description": "Create a basic user and unit test.", + "packages": { + "orm": "all", + "security": "all", + "phpunit": "dev", + "zenstruck/foundry": "dev" + } +} diff --git a/src/Resources/scaffolds/6.0/user/src/Entity/User.php b/src/Resources/scaffolds/6.0/user/src/Entity/User.php new file mode 100644 index 000000000..cc61e8592 --- /dev/null +++ b/src/Resources/scaffolds/6.0/user/src/Entity/User.php @@ -0,0 +1,111 @@ +id; + } + + public function getEmail(): ?string + { + return $this->email; + } + + public function setEmail(string $email): self + { + $this->email = $email; + + return $this; + } + + /** + * A visual identifier that represents this user. + * + * @see UserInterface + */ + public function getUserIdentifier(): string + { + return (string) $this->email; + } + + /** + * @see UserInterface + */ + public function getRoles(): array + { + $roles = $this->roles; + // guarantee every user at least has ROLE_USER + $roles[] = 'ROLE_USER'; + + return array_unique($roles); + } + + public function setRoles(array $roles): self + { + $this->roles = $roles; + + return $this; + } + + /** + * @see PasswordAuthenticatedUserInterface + */ + public function getPassword(): string + { + return $this->password; + } + + public function setPassword(string $password): self + { + $this->password = $password; + + return $this; + } + + /** + * @see UserInterface + */ + public function eraseCredentials() + { + // If you store any temporary, sensitive data on the user, clear it here + // $this->plainPassword = null; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(?string $name): self + { + $this->name = $name; + + return $this; + } +} diff --git a/src/Resources/scaffolds/6.0/user/src/Factory/UserFactory.php b/src/Resources/scaffolds/6.0/user/src/Factory/UserFactory.php new file mode 100644 index 000000000..8166c7203 --- /dev/null +++ b/src/Resources/scaffolds/6.0/user/src/Factory/UserFactory.php @@ -0,0 +1,61 @@ + + * + * @method static User|Proxy createOne(array $attributes = []) + * @method static User[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static User|Proxy find(object|array|mixed $criteria) + * @method static User|Proxy findOrCreate(array $attributes) + * @method static User|Proxy first(string $sortedField = 'id') + * @method static User|Proxy last(string $sortedField = 'id') + * @method static User|Proxy random(array $attributes = []) + * @method static User|Proxy randomOrCreate(array $attributes = []) + * @method static User[]|Proxy[] all() + * @method static User[]|Proxy[] findBy(array $attributes) + * @method static User[]|Proxy[] randomSet(int $number, array $attributes = []) + * @method static User[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @method static UserRepository|RepositoryProxy repository() + * @method User|Proxy create(array|callable $attributes = []) + */ +final class UserFactory extends ModelFactory +{ + public const DEFAULT_PASSWORD = '1234'; + + public function __construct(private UserPasswordHasherInterface $passwordHasher) + { + parent::__construct(); + } + + protected function getDefaults(): array + { + return [ + 'email' => self::faker()->unique()->safeEmail(), + 'name' => self::faker()->name(), + 'password' => self::DEFAULT_PASSWORD, + ]; + } + + protected function initialize(): self + { + return $this + ->afterInstantiate(function(User $user) { + $user->setPassword($this->passwordHasher->hashPassword($user, $user->getPassword())); + }) + ; + } + + protected static function getClass(): string + { + return User::class; + } +} diff --git a/src/Resources/scaffolds/6.0/user/src/Repository/UserRepository.php b/src/Resources/scaffolds/6.0/user/src/Repository/UserRepository.php new file mode 100644 index 000000000..0846acd19 --- /dev/null +++ b/src/Resources/scaffolds/6.0/user/src/Repository/UserRepository.php @@ -0,0 +1,43 @@ +setPassword($newHashedPassword); + $this->save($user); + } + + public function save(User $user): void + { + $this->_em->persist($user); + $this->_em->flush(); + } +} diff --git a/src/Resources/scaffolds/6.0/user/tests/Unit/Entity/UserTest.php b/src/Resources/scaffolds/6.0/user/tests/Unit/Entity/UserTest.php new file mode 100644 index 000000000..91212ffab --- /dev/null +++ b/src/Resources/scaffolds/6.0/user/tests/Unit/Entity/UserTest.php @@ -0,0 +1,22 @@ +assertSame(['ROLE_USER'], (new User())->getRoles()); + $this->assertSame(['ROLE_ADMIN', 'ROLE_USER'], (new User())->setRoles(['ROLE_ADMIN'])->getRoles()); + $this->assertSame( + ['ROLE_ADMIN', 'ROLE_USER'], + (new User())->setRoles(['ROLE_ADMIN', 'ROLE_USER'])->getRoles() + ); + } +} diff --git a/src/Test/MakerTestEnvironment.php b/src/Test/MakerTestEnvironment.php index 1cd63aa26..d9d34f577 100644 --- a/src/Test/MakerTestEnvironment.php +++ b/src/Test/MakerTestEnvironment.php @@ -345,7 +345,7 @@ public function createInteractiveCommandProcess(string $commandName, array $user [ 'SHELL_INTERACTIVE' => '1', ], - 10 + 20 ); if ($userInputs) { diff --git a/tests/Maker/MakeScaffoldTest.php b/tests/Maker/MakeScaffoldTest.php new file mode 100644 index 000000000..f7e29c666 --- /dev/null +++ b/tests/Maker/MakeScaffoldTest.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MakerBundle\Tests\Maker; + +use Symfony\Bundle\MakerBundle\Maker\MakeScaffold; +use Symfony\Bundle\MakerBundle\Test\MakerTestCase; +use Symfony\Bundle\MakerBundle\Test\MakerTestRunner; +use Symfony\Component\Finder\Finder; +use Symfony\Component\HttpKernel\Kernel; + +class MakeScaffoldTest extends MakerTestCase +{ + protected function setUp(): void + { + parent::setUp(); + + if (Kernel::MAJOR_VERSION < 6) { + $this->markTestSkipped('Only available on Symfony 6+.'); + } + } + + public function getTestDetails(): iterable + { + foreach (self::scaffoldProvider() as $name) { + yield $name => [$this->createMakerTest() + ->preRun(function (MakerTestRunner $runner) { + $runner->writeFile('.env.test.local', implode("\n", [ + 'DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"', + 'MAILER_DSN=null://null', + ])); + }) + ->addExtraDependencies( + 'process' + ) + ->run(function (MakerTestRunner $runner) use ($name) { + $runner->runMaker([$name]); + $runner->runTests(); + + $this->assertTrue(true); // successfully ran tests + }), + ]; + } + } + + protected function getMakerClass(): string + { + return MakeScaffold::class; + } + + private static function scaffoldProvider(): iterable + { + foreach (Finder::create()->in(__DIR__.'/../../src/Resources/scaffolds/6.0')->name('*.json') as $file) { + yield $file->getFilenameWithoutExtension(); + } + } +} From 010a2fad90a328510a347a34ca0a2f88eaa49018 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Wed, 30 Mar 2022 15:08:35 -0400 Subject: [PATCH 02/21] use php files for scaffold manifests --- src/Maker/MakeScaffold.php | 5 +++-- src/Resources/scaffolds/6.0/auth.json | 10 ---------- src/Resources/scaffolds/6.0/auth.php | 12 ++++++++++++ src/Resources/scaffolds/6.0/change-password.json | 10 ---------- src/Resources/scaffolds/6.0/change-password.php | 12 ++++++++++++ src/Resources/scaffolds/6.0/homepage.json | 8 -------- src/Resources/scaffolds/6.0/homepage.php | 10 ++++++++++ src/Resources/scaffolds/6.0/profile.json | 10 ---------- src/Resources/scaffolds/6.0/profile.php | 12 ++++++++++++ src/Resources/scaffolds/6.0/register.json | 10 ---------- src/Resources/scaffolds/6.0/register.php | 12 ++++++++++++ src/Resources/scaffolds/6.0/reset-password.json | 13 ------------- src/Resources/scaffolds/6.0/reset-password.php | 15 +++++++++++++++ src/Resources/scaffolds/6.0/user.json | 9 --------- src/Resources/scaffolds/6.0/user.php | 11 +++++++++++ src/Test/MakerTestEnvironment.php | 2 +- tests/Maker/MakeScaffoldTest.php | 2 +- 17 files changed, 89 insertions(+), 74 deletions(-) delete mode 100644 src/Resources/scaffolds/6.0/auth.json create mode 100644 src/Resources/scaffolds/6.0/auth.php delete mode 100644 src/Resources/scaffolds/6.0/change-password.json create mode 100644 src/Resources/scaffolds/6.0/change-password.php delete mode 100644 src/Resources/scaffolds/6.0/homepage.json create mode 100644 src/Resources/scaffolds/6.0/homepage.php delete mode 100644 src/Resources/scaffolds/6.0/profile.json create mode 100644 src/Resources/scaffolds/6.0/profile.php delete mode 100644 src/Resources/scaffolds/6.0/register.json create mode 100644 src/Resources/scaffolds/6.0/register.php delete mode 100644 src/Resources/scaffolds/6.0/reset-password.json create mode 100644 src/Resources/scaffolds/6.0/reset-password.php delete mode 100644 src/Resources/scaffolds/6.0/user.json create mode 100644 src/Resources/scaffolds/6.0/user.php diff --git a/src/Maker/MakeScaffold.php b/src/Maker/MakeScaffold.php index 6c9c31dfe..47e464e84 100644 --- a/src/Maker/MakeScaffold.php +++ b/src/Maker/MakeScaffold.php @@ -148,14 +148,15 @@ private function availableScaffolds(): array $finder = Finder::create() // todo, improve versioning system ->in(\sprintf('%s/../Resources/scaffolds/%s.0', __DIR__, Kernel::MAJOR_VERSION)) - ->name('*.json') + ->name('*.php') + ->depth(0) ; foreach ($finder as $file) { $name = $file->getFilenameWithoutExtension(); $this->availableScaffolds[$name] = array_merge( - json_decode(file_get_contents($file), true), + require $file, ['dir' => dirname($file->getRealPath()).'/'.$name] ); } diff --git a/src/Resources/scaffolds/6.0/auth.json b/src/Resources/scaffolds/6.0/auth.json deleted file mode 100644 index 810935c8c..000000000 --- a/src/Resources/scaffolds/6.0/auth.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "description": "Create login form and tests.", - "dependents": [ - "homepage", - "user" - ], - "packages": { - "profiler": "dev" - } -} diff --git a/src/Resources/scaffolds/6.0/auth.php b/src/Resources/scaffolds/6.0/auth.php new file mode 100644 index 000000000..5711fa641 --- /dev/null +++ b/src/Resources/scaffolds/6.0/auth.php @@ -0,0 +1,12 @@ + 'Create login form and tests.', + 'dependents' => [ + 'user', + 'homepage', + ], + 'packages' => [ + 'profiler' => 'dev', + ] +]; diff --git a/src/Resources/scaffolds/6.0/change-password.json b/src/Resources/scaffolds/6.0/change-password.json deleted file mode 100644 index 89c310704..000000000 --- a/src/Resources/scaffolds/6.0/change-password.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "description": "Create change password form and tests.", - "dependents": [ - "auth" - ], - "packages": { - "form": "all", - "validator": "all" - } -} diff --git a/src/Resources/scaffolds/6.0/change-password.php b/src/Resources/scaffolds/6.0/change-password.php new file mode 100644 index 000000000..8368c9620 --- /dev/null +++ b/src/Resources/scaffolds/6.0/change-password.php @@ -0,0 +1,12 @@ + 'Create change password form and tests.', + 'dependents' => [ + 'auth', + ], + 'packages' => [ + 'form' => 'all', + 'validator' => 'all', + ] +]; diff --git a/src/Resources/scaffolds/6.0/homepage.json b/src/Resources/scaffolds/6.0/homepage.json deleted file mode 100644 index 8eb83473f..000000000 --- a/src/Resources/scaffolds/6.0/homepage.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "description": "Create a basic homepage controller/template/test.", - "packages": { - "twig": "all", - "phpunit": "dev", - "zenstruck/browser": "dev" - } -} diff --git a/src/Resources/scaffolds/6.0/homepage.php b/src/Resources/scaffolds/6.0/homepage.php new file mode 100644 index 000000000..c82f9c120 --- /dev/null +++ b/src/Resources/scaffolds/6.0/homepage.php @@ -0,0 +1,10 @@ + 'Create a basic homepage controller/template/test.', + 'packages' => [ + 'twig' => 'all', + 'phpunit' => 'dev', + 'zenstruck/browser' => 'dev', + ] +]; diff --git a/src/Resources/scaffolds/6.0/profile.json b/src/Resources/scaffolds/6.0/profile.json deleted file mode 100644 index 7246dedc7..000000000 --- a/src/Resources/scaffolds/6.0/profile.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "description": "Create user profile form and tests.", - "dependents": [ - "auth" - ], - "packages": { - "form": "all", - "validator": "all" - } -} diff --git a/src/Resources/scaffolds/6.0/profile.php b/src/Resources/scaffolds/6.0/profile.php new file mode 100644 index 000000000..11b592b85 --- /dev/null +++ b/src/Resources/scaffolds/6.0/profile.php @@ -0,0 +1,12 @@ + 'Create user profile form and tests.', + 'dependents' => [ + 'auth', + ], + 'packages' => [ + 'form' => 'all', + 'validator' => 'all', + ] +]; diff --git a/src/Resources/scaffolds/6.0/register.json b/src/Resources/scaffolds/6.0/register.json deleted file mode 100644 index 015a2f497..000000000 --- a/src/Resources/scaffolds/6.0/register.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "description": "Create registration form and tests.", - "dependents": [ - "auth" - ], - "packages": { - "form": "all", - "validator": "all" - } -} diff --git a/src/Resources/scaffolds/6.0/register.php b/src/Resources/scaffolds/6.0/register.php new file mode 100644 index 000000000..9bb2e9953 --- /dev/null +++ b/src/Resources/scaffolds/6.0/register.php @@ -0,0 +1,12 @@ + 'Create registration form and tests.', + 'dependents' => [ + 'auth', + ], + 'packages' => [ + 'form' => 'all', + 'validator' => 'all', + ] +]; diff --git a/src/Resources/scaffolds/6.0/reset-password.json b/src/Resources/scaffolds/6.0/reset-password.json deleted file mode 100644 index 0626bd699..000000000 --- a/src/Resources/scaffolds/6.0/reset-password.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "description": "Reset password system using symfonycasts/reset-password-bundle with tests.", - "dependents": [ - "auth" - ], - "packages": { - "form": "all", - "validator": "all", - "mailer": "all", - "symfonycasts/reset-password-bundle": "all", - "zenstruck/mailer-test": "dev" - } -} diff --git a/src/Resources/scaffolds/6.0/reset-password.php b/src/Resources/scaffolds/6.0/reset-password.php new file mode 100644 index 000000000..36287e3ed --- /dev/null +++ b/src/Resources/scaffolds/6.0/reset-password.php @@ -0,0 +1,15 @@ + 'Reset password system using symfonycasts/reset-password-bundle with tests.', + 'dependents' => [ + 'auth', + ], + 'packages' => [ + 'form' => 'all', + 'validator' => 'all', + 'mailer' => 'all', + 'symfonycasts/reset-password-bundle' => 'all', + 'zenstruck/mailer-test' => 'dev', + ] +]; diff --git a/src/Resources/scaffolds/6.0/user.json b/src/Resources/scaffolds/6.0/user.json deleted file mode 100644 index 8e5a009b7..000000000 --- a/src/Resources/scaffolds/6.0/user.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "description": "Create a basic user and unit test.", - "packages": { - "orm": "all", - "security": "all", - "phpunit": "dev", - "zenstruck/foundry": "dev" - } -} diff --git a/src/Resources/scaffolds/6.0/user.php b/src/Resources/scaffolds/6.0/user.php new file mode 100644 index 000000000..dcd93d2ab --- /dev/null +++ b/src/Resources/scaffolds/6.0/user.php @@ -0,0 +1,11 @@ + 'Create a basic user and unit test.', + 'packages' => [ + 'orm' => 'all', + 'security' => 'all', + 'phpunit' => 'dev', + 'zenstruck/foundry' => 'dev', + ] +]; diff --git a/src/Test/MakerTestEnvironment.php b/src/Test/MakerTestEnvironment.php index d9d34f577..ee38b4d60 100644 --- a/src/Test/MakerTestEnvironment.php +++ b/src/Test/MakerTestEnvironment.php @@ -345,7 +345,7 @@ public function createInteractiveCommandProcess(string $commandName, array $user [ 'SHELL_INTERACTIVE' => '1', ], - 20 + 30 ); if ($userInputs) { diff --git a/tests/Maker/MakeScaffoldTest.php b/tests/Maker/MakeScaffoldTest.php index f7e29c666..18daee398 100644 --- a/tests/Maker/MakeScaffoldTest.php +++ b/tests/Maker/MakeScaffoldTest.php @@ -58,7 +58,7 @@ protected function getMakerClass(): string private static function scaffoldProvider(): iterable { - foreach (Finder::create()->in(__DIR__.'/../../src/Resources/scaffolds/6.0')->name('*.json') as $file) { + foreach (Finder::create()->in(__DIR__.'/../../src/Resources/scaffolds/6.0')->name('*.php')->depth(0) as $file) { yield $file->getFilenameWithoutExtension(); } } From 2e4b5c77fed8902f72890f31ea2d53166ba8f08a Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Wed, 30 Mar 2022 16:21:12 -0400 Subject: [PATCH 03/21] add scaffold configuration system to manipulate files --- src/Maker/MakeScaffold.php | 17 ++- src/Resources/config/makers.xml | 2 +- src/Resources/scaffolds/6.0/auth.php | 45 ++++++- .../6.0/auth/config/packages/security.yaml | 58 --------- .../scaffolds/6.0/auth/config/services.yaml | 28 ----- src/Resources/scaffolds/6.0/register.php | 25 +++- .../6.0/register/src/Entity/User.php | 113 ------------------ .../scaffolds/6.0/reset-password.php | 10 +- 8 files changed, 90 insertions(+), 208 deletions(-) delete mode 100644 src/Resources/scaffolds/6.0/auth/config/packages/security.yaml delete mode 100644 src/Resources/scaffolds/6.0/auth/config/services.yaml delete mode 100644 src/Resources/scaffolds/6.0/register/src/Entity/User.php diff --git a/src/Maker/MakeScaffold.php b/src/Maker/MakeScaffold.php index 47e464e84..fa6444de2 100644 --- a/src/Maker/MakeScaffold.php +++ b/src/Maker/MakeScaffold.php @@ -14,6 +14,7 @@ use Composer\InstalledVersions; use Symfony\Bundle\MakerBundle\ConsoleStyle; use Symfony\Bundle\MakerBundle\DependencyBuilder; +use Symfony\Bundle\MakerBundle\FileManager; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Component\Console\Command\Command; @@ -29,14 +30,14 @@ */ final class MakeScaffold extends AbstractMaker { - private $projectDir; + private $files; private $availableScaffolds; private $installedScaffolds = []; private $installedPackages = []; - public function __construct($projectDir) + public function __construct(FileManager $files) { - $this->projectDir = $projectDir; + $this->files = $files; } public static function getCommandName(): string @@ -116,7 +117,7 @@ private function generateScaffold(string $name, ConsoleStyle $io): void // todo composer bin detection $command = ['composer', 'require', '--no-scripts', 'dev' === $env ? '--dev' : null, $package]; - $process = new Process(array_filter($command), $this->projectDir); + $process = new Process(array_filter($command), $this->files->getRootDirectory()); $process->run(); @@ -130,7 +131,13 @@ private function generateScaffold(string $name, ConsoleStyle $io): void $io->text('Copying scaffold files...'); - (new Filesystem())->mirror($scaffold['dir'], $this->projectDir, null, ['override' => true]); + (new Filesystem())->mirror($scaffold['dir'], $this->files->getRootDirectory()); + + if (isset($scaffold['configure'])) { + $io->text('Executing configuration...'); + + $scaffold['configure']($this->files); + } $io->text("Successfully installed scaffold {$name}."); $io->newLine(); diff --git a/src/Resources/config/makers.xml b/src/Resources/config/makers.xml index 80b9f092a..f31124892 100644 --- a/src/Resources/config/makers.xml +++ b/src/Resources/config/makers.xml @@ -26,7 +26,7 @@
- %kernel.project_dir% + diff --git a/src/Resources/scaffolds/6.0/auth.php b/src/Resources/scaffolds/6.0/auth.php index 5711fa641..764141b38 100644 --- a/src/Resources/scaffolds/6.0/auth.php +++ b/src/Resources/scaffolds/6.0/auth.php @@ -1,5 +1,8 @@ 'Create login form and tests.', 'dependents' => [ @@ -8,5 +11,45 @@ ], 'packages' => [ 'profiler' => 'dev', - ] + ], + 'configure' => function(FileManager $files) { + $services = new YamlSourceManipulator($files->getFileContents('config/services.yaml')); + $data = $services->getData(); + $data['services']['App\Security\LoginUser']['$authenticator'] ='@security.authenticator.form_login.main'; + $services->setData($data); + $files->dumpFile('config/services.yaml', $services->getContents()); + + $security = new YamlSourceManipulator($files->getFileContents('config/packages/security.yaml')); + $data = $security->getData(); + $data['security']['providers'] = [ + 'app_user_provider' => [ + 'entity' => [ + 'class' => 'App\Entity\User', + 'property' => 'email', + ], + ], + ]; + $data['security']['firewalls']['main'] = [ + 'lazy' => true, + 'provider' => 'app_user_provider', + 'form_login' => [ + 'login_path' => 'login', + 'check_path' => 'login', + 'username_parameter' => 'email', + 'password_parameter' => 'password', + 'enable_csrf' => true, + ], + 'logout' => [ + 'path' => 'logout', + 'target' => 'homepage', + ], + 'remember_me' => [ + 'secret' => '%kernel.secret%', + 'secure' => 'auto', + 'samesite' => 'lax', + ], + ]; + $security->setData($data); + $files->dumpFile('config/packages/security.yaml', $security->getContents()); + }, ]; diff --git a/src/Resources/scaffolds/6.0/auth/config/packages/security.yaml b/src/Resources/scaffolds/6.0/auth/config/packages/security.yaml deleted file mode 100644 index f2a480054..000000000 --- a/src/Resources/scaffolds/6.0/auth/config/packages/security.yaml +++ /dev/null @@ -1,58 +0,0 @@ -security: - enable_authenticator_manager: true - # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords - password_hashers: - Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' - # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider - providers: - # used to reload user from session & other features (e.g. switch_user) - app_user_provider: - entity: - class: App\Entity\User - property: email - firewalls: - dev: - pattern: ^/(_(profiler|wdt)|css|images|js)/ - security: false - main: - lazy: true - provider: app_user_provider - form_login: - login_path: login - check_path: login - username_parameter: email - password_parameter: password - enable_csrf: true - logout: - path: logout - # where to redirect after logout - target: homepage - remember_me: - secret: '%kernel.secret%' - secure: auto - samesite: lax - - # activate different ways to authenticate - # https://symfony.com/doc/current/security.html#the-firewall - - # https://symfony.com/doc/current/security/impersonating_user.html - # switch_user: true - - # Easy way to control access for large sections of your site - # Note: Only the *first* access control that matches will be used - access_control: - # - { path: ^/admin, roles: ROLE_ADMIN } - # - { path: ^/profile, roles: ROLE_USER } - -when@test: - security: - password_hashers: - # By default, password hashers are resource intensive and take time. This is - # important to generate secure password hashes. In tests however, secure hashes - # are not important, waste resources and increase test times. The following - # reduces the work factor to the lowest possible values. - Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: - algorithm: auto - cost: 4 # Lowest possible value for bcrypt - time_cost: 3 # Lowest possible value for argon - memory_cost: 10 # Lowest possible value for argon diff --git a/src/Resources/scaffolds/6.0/auth/config/services.yaml b/src/Resources/scaffolds/6.0/auth/config/services.yaml deleted file mode 100644 index 198b98572..000000000 --- a/src/Resources/scaffolds/6.0/auth/config/services.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# This file is the entry point to configure your own services. -# Files in the packages/ subdirectory configure your dependencies. - -# Put parameters here that don't need to change on each machine where the app is deployed -# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration -parameters: - -services: - # default configuration for services in *this* file - _defaults: - autowire: true # Automatically injects dependencies in your services. - autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. - - # makes classes in src/ available to be used as services - # this creates a service per class whose id is the fully-qualified class name - App\: - resource: '../src/' - exclude: - - '../src/DependencyInjection/' - - '../src/Entity/' - - '../src/Kernel.php' - - # add more service definitions when explicit configuration is needed - # please note that last definitions always *replace* previous ones - - # programmatic user login service - App\Security\LoginUser: - $authenticator: '@security.authenticator.form_login.main' diff --git a/src/Resources/scaffolds/6.0/register.php b/src/Resources/scaffolds/6.0/register.php index 9bb2e9953..c1cef1e22 100644 --- a/src/Resources/scaffolds/6.0/register.php +++ b/src/Resources/scaffolds/6.0/register.php @@ -1,5 +1,7 @@ 'Create registration form and tests.', 'dependents' => [ @@ -8,5 +10,26 @@ 'packages' => [ 'form' => 'all', 'validator' => 'all', - ] + ], + 'configure' => function(FileManager $files) { + $userEntity = $files->getFileContents('src/Entity/User.php'); + + if (str_contains($userEntity, $attribute = "#[UniqueEntity(fields: ['email'], message: 'There is already an account with this email')]")) { + // unique constraint already there + return; + } + + $userEntity = str_replace( + [ + '#[ORM\Entity(repositoryClass: UserRepository::class)]', + 'use Doctrine\ORM\Mapping as ORM;' + ], + [ + "#[ORM\Entity(repositoryClass: UserRepository::class)]\n{$attribute}", + "use Doctrine\ORM\Mapping as ORM;\nuse Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;", + ], + $userEntity + ); + $files->dumpFile('src/Entity/User.php', $userEntity); + }, ]; diff --git a/src/Resources/scaffolds/6.0/register/src/Entity/User.php b/src/Resources/scaffolds/6.0/register/src/Entity/User.php deleted file mode 100644 index 5579a6115..000000000 --- a/src/Resources/scaffolds/6.0/register/src/Entity/User.php +++ /dev/null @@ -1,113 +0,0 @@ -id; - } - - public function getEmail(): ?string - { - return $this->email; - } - - public function setEmail(string $email): self - { - $this->email = $email; - - return $this; - } - - /** - * A visual identifier that represents this user. - * - * @see UserInterface - */ - public function getUserIdentifier(): string - { - return (string) $this->email; - } - - /** - * @see UserInterface - */ - public function getRoles(): array - { - $roles = $this->roles; - // guarantee every user at least has ROLE_USER - $roles[] = 'ROLE_USER'; - - return array_unique($roles); - } - - public function setRoles(array $roles): self - { - $this->roles = $roles; - - return $this; - } - - /** - * @see PasswordAuthenticatedUserInterface - */ - public function getPassword(): string - { - return $this->password; - } - - public function setPassword(string $password): self - { - $this->password = $password; - - return $this; - } - - /** - * @see UserInterface - */ - public function eraseCredentials() - { - // If you store any temporary, sensitive data on the user, clear it here - // $this->plainPassword = null; - } - - public function getName(): ?string - { - return $this->name; - } - - public function setName(?string $name): self - { - $this->name = $name; - - return $this; - } -} diff --git a/src/Resources/scaffolds/6.0/reset-password.php b/src/Resources/scaffolds/6.0/reset-password.php index 36287e3ed..02c52e343 100644 --- a/src/Resources/scaffolds/6.0/reset-password.php +++ b/src/Resources/scaffolds/6.0/reset-password.php @@ -1,5 +1,7 @@ 'Reset password system using symfonycasts/reset-password-bundle with tests.', 'dependents' => [ @@ -11,5 +13,11 @@ 'mailer' => 'all', 'symfonycasts/reset-password-bundle' => 'all', 'zenstruck/mailer-test' => 'dev', - ] + ], + 'configure' => function(FileManager $files) { + $files->dumpFile( + 'config/packages/reset_password.yaml', + file_get_contents(__DIR__.'/reset-password/config/packages/reset_password.yaml') + ); + }, ]; From 3fcdd30bbc5ee64455aef560f385b23390d50234 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Wed, 30 Mar 2022 18:53:36 -0400 Subject: [PATCH 04/21] add bootstrapcss scaffold --- src/Maker/MakeScaffold.php | 6 ++- .../6.0/auth/templates/login.html.twig | 51 +++++++++++-------- src/Resources/scaffolds/6.0/bootstrapcss.php | 34 +++++++++++++ .../templates/user/change_password.html.twig | 18 ++++--- .../Functional/User/ChangePasswordTest.php | 2 +- .../6.0/homepage/templates/index.html.twig | 2 +- .../profile/templates/user/profile.html.twig | 14 +++-- .../tests/Functional/User/ProfileTest.php | 2 +- .../src/Form/User/RegistrationFormType.php | 3 +- .../templates/user/register.html.twig | 22 ++++---- .../tests/Functional/User/RegisterTest.php | 2 +- .../reset_password/check_email.html.twig | 15 ++++-- .../reset_password/request.html.twig | 23 ++++----- .../templates/reset_password/reset.html.twig | 14 +++-- .../tests/Functional/ResetPasswordTest.php | 12 ++--- tests/Maker/MakeScaffoldTest.php | 8 ++- 16 files changed, 147 insertions(+), 81 deletions(-) create mode 100644 src/Resources/scaffolds/6.0/bootstrapcss.php diff --git a/src/Maker/MakeScaffold.php b/src/Maker/MakeScaffold.php index fa6444de2..043166489 100644 --- a/src/Maker/MakeScaffold.php +++ b/src/Maker/MakeScaffold.php @@ -129,9 +129,11 @@ private function generateScaffold(string $name, ConsoleStyle $io): void } } - $io->text('Copying scaffold files...'); + if (is_dir($scaffold['dir'])) { + $io->text('Copying scaffold files...'); - (new Filesystem())->mirror($scaffold['dir'], $this->files->getRootDirectory()); + (new Filesystem())->mirror($scaffold['dir'], $this->files->getRootDirectory()); + } if (isset($scaffold['configure'])) { $io->text('Executing configuration...'); diff --git a/src/Resources/scaffolds/6.0/auth/templates/login.html.twig b/src/Resources/scaffolds/6.0/auth/templates/login.html.twig index c0cb0c3b6..a37381471 100644 --- a/src/Resources/scaffolds/6.0/auth/templates/login.html.twig +++ b/src/Resources/scaffolds/6.0/auth/templates/login.html.twig @@ -3,29 +3,36 @@ {% block title %}Log in!{% endblock %} {% block body %} -
- {% if error %} -
{{ error.messageKey|trans(error.messageData, 'security') }}
- {% endif %} +
+ +

Please sign in

-

Please sign in

- - - - - -
- -
+ {% if error %} +
{{ error.messageKey|trans(error.messageData, 'security') }}
+ {% endif %} - {% if app.request.query.has('target') %} - - {% endif %} +
+ + +
+
+ + +
+
+ +
+ - - + {% if app.request.query.has('target') %} + + {% endif %} + + + +
{% endblock %} diff --git a/src/Resources/scaffolds/6.0/bootstrapcss.php b/src/Resources/scaffolds/6.0/bootstrapcss.php new file mode 100644 index 000000000..4d08da29b --- /dev/null +++ b/src/Resources/scaffolds/6.0/bootstrapcss.php @@ -0,0 +1,34 @@ + 'Add bootstrap css/js.', + 'packages' => [ + 'twig' => 'all', + 'encore' => 'all', + ], + 'configure' => function(FileManager $files) { + $packageJson = json_decode($files->getFileContents('package.json'), true); + $packageJson['devDependencies']['bootstrap'] = '^5.0.0'; + $packageJson['devDependencies']['@popperjs/core'] = '^2.0.0'; + $files->dumpFile('package.json', json_encode($packageJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + + $twig = new YamlSourceManipulator($files->getFileContents('config/packages/twig.yaml')); + $data = $twig->getData(); + $data['twig']['form_themes'] = ['bootstrap_5_layout.html.twig']; + $twig->setData($data); + $files->dumpFile('config/packages/twig.yaml', $twig->getContents()); + + $files->dumpFile('assets/styles/app.css', "@import \"~bootstrap/dist/css/bootstrap.css\";\n"); + + $appJs = $files->getFileContents('assets/app.js'); + + if (str_contains($appJs, "require('bootstrap');")) { + return; + } + + $files->dumpFile('assets/app.js', $appJs."require('bootstrap');\n"); + }, +]; diff --git a/src/Resources/scaffolds/6.0/change-password/templates/user/change_password.html.twig b/src/Resources/scaffolds/6.0/change-password/templates/user/change_password.html.twig index 99da0c874..f3a231ed8 100644 --- a/src/Resources/scaffolds/6.0/change-password/templates/user/change_password.html.twig +++ b/src/Resources/scaffolds/6.0/change-password/templates/user/change_password.html.twig @@ -3,13 +3,17 @@ {% block title %}Change Password{% endblock %} {% block body %} -

Change Password

+
+
+

Change Password

- {{ form_start(changePasswordForm) }} - {{ form_row(changePasswordForm.currentPassword, { label: 'Current Password' }) }} - {{ form_row(changePasswordForm.plainPassword.first, { label: 'New Password' }) }} - {{ form_row(changePasswordForm.plainPassword.second, { label: 'Repeat New Password' }) }} + {{ form_start(changePasswordForm) }} + {{ form_row(changePasswordForm.currentPassword, { label: 'Current Password' }) }} + {{ form_row(changePasswordForm.plainPassword.first, { label: 'New Password' }) }} + {{ form_row(changePasswordForm.plainPassword.second, { label: 'Repeat New Password' }) }} - - {{ form_end(changePasswordForm) }} + + {{ form_end(changePasswordForm) }} +
+
{% endblock %} diff --git a/src/Resources/scaffolds/6.0/change-password/tests/Functional/User/ChangePasswordTest.php b/src/Resources/scaffolds/6.0/change-password/tests/Functional/User/ChangePasswordTest.php index a96ced7f1..f9e4d6866 100644 --- a/src/Resources/scaffolds/6.0/change-password/tests/Functional/User/ChangePasswordTest.php +++ b/src/Resources/scaffolds/6.0/change-password/tests/Functional/User/ChangePasswordTest.php @@ -33,7 +33,7 @@ public function can_change_password(): void ->click('Change Password') ->assertSuccessful() ->assertOn('/') - ->assertSeeIn('.flash', 'You\'ve successfully changed your password.') + ->assertSeeIn('.alert', 'You\'ve successfully changed your password.') ->use(Authentication::assertAuthenticatedAs('mary@example.com')) ->visit('/logout') ->visit('/login') diff --git a/src/Resources/scaffolds/6.0/homepage/templates/index.html.twig b/src/Resources/scaffolds/6.0/homepage/templates/index.html.twig index c11b35c7b..9b2338c6c 100644 --- a/src/Resources/scaffolds/6.0/homepage/templates/index.html.twig +++ b/src/Resources/scaffolds/6.0/homepage/templates/index.html.twig @@ -7,7 +7,7 @@ {% for type, messages in app.flashes %} {% for message in messages %} -
+
{{ message }}
{% endfor %} diff --git a/src/Resources/scaffolds/6.0/profile/templates/user/profile.html.twig b/src/Resources/scaffolds/6.0/profile/templates/user/profile.html.twig index a2bae421f..98d3f82d1 100644 --- a/src/Resources/scaffolds/6.0/profile/templates/user/profile.html.twig +++ b/src/Resources/scaffolds/6.0/profile/templates/user/profile.html.twig @@ -3,11 +3,15 @@ {% block title %}Manage Profile{% endblock %} {% block body %} -

Manage Profile

+
+
+

Manage Profile

- {{ form_start(profileForm) }} - {{ form_row(profileForm.name) }} + {{ form_start(profileForm) }} + {{ form_row(profileForm.name) }} - - {{ form_end(profileForm) }} + + {{ form_end(profileForm) }} +
+
{% endblock %} diff --git a/src/Resources/scaffolds/6.0/profile/tests/Functional/User/ProfileTest.php b/src/Resources/scaffolds/6.0/profile/tests/Functional/User/ProfileTest.php index 26afef8c3..8b70a0063 100644 --- a/src/Resources/scaffolds/6.0/profile/tests/Functional/User/ProfileTest.php +++ b/src/Resources/scaffolds/6.0/profile/tests/Functional/User/ProfileTest.php @@ -32,7 +32,7 @@ public function can_update_profile(): void ->click('Save') ->assertOn('/') ->assertSuccessful() - ->assertSeeIn('.flash', 'You\'ve successfully updated your profile.') + ->assertSeeIn('.alert', 'You\'ve successfully updated your profile.') ; $this->assertSame('John Smith', $user->getName()); diff --git a/src/Resources/scaffolds/6.0/register/src/Form/User/RegistrationFormType.php b/src/Resources/scaffolds/6.0/register/src/Form/User/RegistrationFormType.php index a3a6f7e14..c26aadfc1 100644 --- a/src/Resources/scaffolds/6.0/register/src/Form/User/RegistrationFormType.php +++ b/src/Resources/scaffolds/6.0/register/src/Form/User/RegistrationFormType.php @@ -4,6 +4,7 @@ use App\Entity\User; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\PasswordType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -21,7 +22,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void new NotBlank(['message' => 'Name is required']), ], ]) - ->add('email', null, [ + ->add('email', EmailType::class, [ 'constraints' => [ new NotBlank(['message' => 'Email is required']), new Email(['message' => 'This is not a valid email address']), diff --git a/src/Resources/scaffolds/6.0/register/templates/user/register.html.twig b/src/Resources/scaffolds/6.0/register/templates/user/register.html.twig index 35b032945..584fd2716 100644 --- a/src/Resources/scaffolds/6.0/register/templates/user/register.html.twig +++ b/src/Resources/scaffolds/6.0/register/templates/user/register.html.twig @@ -3,15 +3,19 @@ {% block title %}Register{% endblock %} {% block body %} -

Register

+
+
+

Register

- {{ form_start(registrationForm) }} - {{ form_row(registrationForm.name) }} - {{ form_row(registrationForm.email) }} - {{ form_row(registrationForm.plainPassword, { - label: 'Password' - }) }} + {{ form_start(registrationForm) }} + {{ form_row(registrationForm.name) }} + {{ form_row(registrationForm.email) }} + {{ form_row(registrationForm.plainPassword, { + label: 'Password' + }) }} - - {{ form_end(registrationForm) }} + + {{ form_end(registrationForm) }} +
+
{% endblock %} diff --git a/src/Resources/scaffolds/6.0/register/tests/Functional/User/RegisterTest.php b/src/Resources/scaffolds/6.0/register/tests/Functional/User/RegisterTest.php index c213f1fa0..181058ce9 100644 --- a/src/Resources/scaffolds/6.0/register/tests/Functional/User/RegisterTest.php +++ b/src/Resources/scaffolds/6.0/register/tests/Functional/User/RegisterTest.php @@ -28,7 +28,7 @@ public function can_register(): void ->fillField('Password', 'password') ->click('Register') ->assertOn('/') - ->assertSeeIn('.flash', 'You\'ve successfully registered and are now logged in.') + ->assertSeeIn('.alert', 'You\'ve successfully registered and are now logged in.') ->use(Authentication::assertAuthenticatedAs('madison@example.com')) ->visit('/logout') ->use(Authentication::assertNotAuthenticated()) diff --git a/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/check_email.html.twig b/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/check_email.html.twig index 94fe9198f..567bd46ec 100644 --- a/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/check_email.html.twig +++ b/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/check_email.html.twig @@ -3,9 +3,14 @@ {% block title %}Password Reset Email Sent{% endblock %} {% block body %} -

- If an account matching your email exists, then an email was just sent that contains a link that you can use to reset your password. - This link will expire in {{ resetToken.expirationMessageKey|trans(resetToken.expirationMessageData, 'ResetPasswordBundle') }}. -

-

If you don't receive an email please check your spam folder or try again.

+
+
+

Password Reset Email Sent

+

+ If an account matching your email exists, then an email was just sent that contains a link that you can use to reset your password. + This link will expire in {{ resetToken.expirationMessageKey|trans(resetToken.expirationMessageData, 'ResetPasswordBundle') }}. +

+

If you don't receive an email please check your spam folder or try again.

+
+
{% endblock %} diff --git a/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/request.html.twig b/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/request.html.twig index 095903052..affab07aa 100644 --- a/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/request.html.twig +++ b/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/request.html.twig @@ -3,20 +3,15 @@ {% block title %}Reset your password{% endblock %} {% block body %} - {% for flash_error in app.flashes('reset_password_error') %} - - {% endfor %} -

Reset your password

+
+
+

Reset your password

- {{ form_start(requestForm) }} - {{ form_row(requestForm.email) }} -
- - Enter your email address and we will send you a - link to reset your password. - -
+ {{ form_start(requestForm) }} + {{ form_row(requestForm.email, { help: 'Enter your email address and we will send you a link to reset your password.' }) }} - - {{ form_end(requestForm) }} + + {{ form_end(requestForm) }} +
+
{% endblock %} diff --git a/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/reset.html.twig b/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/reset.html.twig index 799aa10f5..39ceff3c3 100644 --- a/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/reset.html.twig +++ b/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/reset.html.twig @@ -3,10 +3,14 @@ {% block title %}Reset your password{% endblock %} {% block body %} -

Reset your password

+
+
+

Reset your password

- {{ form_start(resetForm) }} - {{ form_row(resetForm.plainPassword) }} - - {{ form_end(resetForm) }} + {{ form_start(resetForm) }} + {{ form_row(resetForm.plainPassword) }} + + {{ form_end(resetForm) }} +
+
{% endblock %} diff --git a/src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php b/src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php index dc5b09d12..a137460e5 100644 --- a/src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php +++ b/src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php @@ -57,7 +57,7 @@ public function can_reset_password(): void ->fillField('Repeat Password', 'new-password') ->click('Reset password') ->assertOn('/') - ->assertSeeIn('.flash', 'Your password was successfully reset, you are now logged in.') + ->assertSeeIn('.alert', 'Your password was successfully reset, you are now logged in.') ->use(Authentication::assertAuthenticatedAs('john@example.com')) ->visit('/logout') ->visit('/login') @@ -118,7 +118,7 @@ public function requests_are_throttled(): void ->fillField('Email', 'john@example.com') ->click('Send password reset email') ->assertOn('/') - ->assertSeeIn('.flash', 'You have already requested a reset password email. Please check your email or try again soon.') + ->assertSeeIn('.alert', 'You have already requested a reset password email. Please check your email or try again soon.') ; $this->mailer()->assertSentEmailCount(1); @@ -254,7 +254,7 @@ public function cannot_reset_with_invalid_token(): void $this->browser() ->visit('/reset-password/reset/invalid-token') ->assertOn('/') - ->assertSeeIn('.flash', 'The reset password link is invalid. Please try to reset your password again.') + ->assertSeeIn('.alert', 'The reset password link is invalid. Please try to reset your password again.') ; } @@ -321,7 +321,7 @@ public function reset_tokens_expire(): void ->visit($this->mailer()->sentEmails()->first()->getHeaders()->get('X-CTA')->getBody()) ->assertOn('/') ->assertSuccessful() - ->assertSeeIn('.flash', 'The link in your email is expired. Please try to reset your password again.') + ->assertSeeIn('.alert', 'The link in your email is expired. Please try to reset your password again.') ; } @@ -343,12 +343,12 @@ public function cannot_use_token_after_password_change(): void ->fillField('Repeat Password', 'new-password') ->click('Reset password') ->assertOn('/') - ->assertSeeIn('.flash', 'Your password was successfully reset, you are now logged in.') + ->assertSeeIn('.alert', 'Your password was successfully reset, you are now logged in.') ->visit('/logout') ->visit($resetUrl) ->assertOn('/') ->assertSuccessful() - ->assertSeeIn('.flash', 'The reset password link is invalid. Please try to reset your password again.') + ->assertSeeIn('.alert', 'The reset password link is invalid. Please try to reset your password again.') ; } diff --git a/tests/Maker/MakeScaffoldTest.php b/tests/Maker/MakeScaffoldTest.php index 18daee398..53a6dbb66 100644 --- a/tests/Maker/MakeScaffoldTest.php +++ b/tests/Maker/MakeScaffoldTest.php @@ -58,8 +58,14 @@ protected function getMakerClass(): string private static function scaffoldProvider(): iterable { + $excluded = ['bootstrapcss', 'starter-kit']; + foreach (Finder::create()->in(__DIR__.'/../../src/Resources/scaffolds/6.0')->name('*.php')->depth(0) as $file) { - yield $file->getFilenameWithoutExtension(); + if (\in_array($name = $file->getFilenameWithoutExtension(), $excluded, true)) { + continue; + } + + yield $name; } } } From 738c07cc82f35793a849bc0f696a3e79da51ced4 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Wed, 30 Mar 2022 19:42:22 -0400 Subject: [PATCH 05/21] add starter-kit that includes all current scaffolds and and application shell --- src/Resources/scaffolds/6.0/bootstrapcss.php | 9 +++- src/Resources/scaffolds/6.0/starter-kit.php | 30 +++++++++++ .../6.0/starter-kit/templates/base.html.twig | 53 +++++++++++++++++++ tests/Maker/MakeScaffoldTest.php | 10 +++- 4 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 src/Resources/scaffolds/6.0/starter-kit.php create mode 100644 src/Resources/scaffolds/6.0/starter-kit/templates/base.html.twig diff --git a/src/Resources/scaffolds/6.0/bootstrapcss.php b/src/Resources/scaffolds/6.0/bootstrapcss.php index 4d08da29b..309332013 100644 --- a/src/Resources/scaffolds/6.0/bootstrapcss.php +++ b/src/Resources/scaffolds/6.0/bootstrapcss.php @@ -11,8 +11,13 @@ ], 'configure' => function(FileManager $files) { $packageJson = json_decode($files->getFileContents('package.json'), true); - $packageJson['devDependencies']['bootstrap'] = '^5.0.0'; - $packageJson['devDependencies']['@popperjs/core'] = '^2.0.0'; + $devDeps = $packageJson['devDependencies']; + $devDeps['bootstrap'] = '^5.0.0'; + $devDeps['@popperjs/core'] = '^2.0.0'; + + ksort($devDeps); + + $packageJson['devDependencies'] = $devDeps; $files->dumpFile('package.json', json_encode($packageJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); $twig = new YamlSourceManipulator($files->getFileContents('config/packages/twig.yaml')); diff --git a/src/Resources/scaffolds/6.0/starter-kit.php b/src/Resources/scaffolds/6.0/starter-kit.php new file mode 100644 index 000000000..a4c80e776 --- /dev/null +++ b/src/Resources/scaffolds/6.0/starter-kit.php @@ -0,0 +1,30 @@ + 'Starting kit with authentication, registration, password reset, user profile management with an application shell styled with Bootstrap CSS.', + 'dependents' => [ + 'bootstrapcss', + 'register', + 'reset-password', + 'change-password', + 'profile', + ], + 'configure' => function(FileManager $files) { + $files->dumpFile('templates/base.html.twig', file_get_contents(__DIR__.'/starter-kit/templates/base.html.twig')); + + $login = $files->getFileContents('templates/login.html.twig'); + $forgotPassword = "\n Forgot your password?"; + + if (str_contains($login, $forgotPassword)) { + return; + } + + $files->dumpFile('templates/login.html.twig', str_replace( + '', + $forgotPassword, + $login + )); + }, +]; diff --git a/src/Resources/scaffolds/6.0/starter-kit/templates/base.html.twig b/src/Resources/scaffolds/6.0/starter-kit/templates/base.html.twig new file mode 100644 index 000000000..fe81e352c --- /dev/null +++ b/src/Resources/scaffolds/6.0/starter-kit/templates/base.html.twig @@ -0,0 +1,53 @@ + + + + + {% block title %}Welcome!{% endblock %} + + {# Run `composer require symfony/webpack-encore-bundle` to start using Symfony UX #} + {% block stylesheets %} + {{ encore_entry_link_tags('app') }} + {% endblock %} + + {% block javascripts %} + {{ encore_entry_script_tags('app') }} + {% endblock %} + + + +
+ {% block body %}{% endblock %} +
+ + diff --git a/tests/Maker/MakeScaffoldTest.php b/tests/Maker/MakeScaffoldTest.php index 53a6dbb66..f1b49984c 100644 --- a/tests/Maker/MakeScaffoldTest.php +++ b/tests/Maker/MakeScaffoldTest.php @@ -19,6 +19,8 @@ class MakeScaffoldTest extends MakerTestCase { + private const STARTER_KITS = ['starter-kit']; + protected function setUp(): void { parent::setUp(); @@ -43,6 +45,12 @@ public function getTestDetails(): iterable ) ->run(function (MakerTestRunner $runner) use ($name) { $runner->runMaker([$name]); + + if (in_array($name, self::STARTER_KITS, true)) { + $runner->runProcess('yarn install'); + $runner->runProcess('yarn dev'); + } + $runner->runTests(); $this->assertTrue(true); // successfully ran tests @@ -58,7 +66,7 @@ protected function getMakerClass(): string private static function scaffoldProvider(): iterable { - $excluded = ['bootstrapcss', 'starter-kit']; + $excluded = ['bootstrapcss']; foreach (Finder::create()->in(__DIR__.'/../../src/Resources/scaffolds/6.0')->name('*.php')->depth(0) as $file) { if (\in_array($name = $file->getFilenameWithoutExtension(), $excluded, true)) { From 184f21a9252a510c6b9b514e05b901a989f4cf17 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Thu, 31 Mar 2022 08:00:28 -0400 Subject: [PATCH 06/21] move "forgot your password" link to reset-password scaffold --- src/Resources/scaffolds/6.0/reset-password.php | 13 +++++++++++++ src/Resources/scaffolds/6.0/starter-kit.php | 13 ------------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Resources/scaffolds/6.0/reset-password.php b/src/Resources/scaffolds/6.0/reset-password.php index 02c52e343..64692ae7c 100644 --- a/src/Resources/scaffolds/6.0/reset-password.php +++ b/src/Resources/scaffolds/6.0/reset-password.php @@ -19,5 +19,18 @@ 'config/packages/reset_password.yaml', file_get_contents(__DIR__.'/reset-password/config/packages/reset_password.yaml') ); + + $login = $files->getFileContents('templates/login.html.twig'); + $forgotPassword = "\n Forgot your password?"; + + if (str_contains($login, $forgotPassword)) { + return; + } + + $files->dumpFile('templates/login.html.twig', str_replace( + '', + $forgotPassword, + $login + )); }, ]; diff --git a/src/Resources/scaffolds/6.0/starter-kit.php b/src/Resources/scaffolds/6.0/starter-kit.php index a4c80e776..bac344a06 100644 --- a/src/Resources/scaffolds/6.0/starter-kit.php +++ b/src/Resources/scaffolds/6.0/starter-kit.php @@ -13,18 +13,5 @@ ], 'configure' => function(FileManager $files) { $files->dumpFile('templates/base.html.twig', file_get_contents(__DIR__.'/starter-kit/templates/base.html.twig')); - - $login = $files->getFileContents('templates/login.html.twig'); - $forgotPassword = "\n Forgot your password?"; - - if (str_contains($login, $forgotPassword)) { - return; - } - - $files->dumpFile('templates/login.html.twig', str_replace( - '', - $forgotPassword, - $login - )); }, ]; From df7509177ab09e4456741a144df526801bcf51fc Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Thu, 31 Mar 2022 09:36:56 -0400 Subject: [PATCH 07/21] auto install required js packages --- src/JsPackageManager.php | 90 ++++++++++++++++++++ src/Maker/MakeScaffold.php | 48 ++++++++++- src/Resources/scaffolds/6.0/bootstrapcss.php | 14 +-- src/Test/MakerTestEnvironment.php | 2 +- tests/Maker/MakeScaffoldTest.php | 8 -- 5 files changed, 139 insertions(+), 23 deletions(-) create mode 100644 src/JsPackageManager.php diff --git a/src/JsPackageManager.php b/src/JsPackageManager.php new file mode 100644 index 000000000..870c6214f --- /dev/null +++ b/src/JsPackageManager.php @@ -0,0 +1,90 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MakerBundle; + +use Symfony\Component\Process\ExecutableFinder; +use Symfony\Component\Process\Process; + +/** + * @author Kevin Bond + */ +final class JsPackageManager +{ + private $executableFinder; + private $files; + + public function __construct(FileManager $fileManager) + { + $this->executableFinder = new ExecutableFinder(); + $this->files = $fileManager; + } + + public function add(string $package, string $version): void + { + $packageWithVersion = "{$package}@{$version}"; + + if ($yarn = $this->executableFinder->find('yarn')) { + $command = [$yarn, 'add', $packageWithVersion, '--dev']; + } elseif ($npm = $this->executableFinder->find('npm')) { + $command = [$npm, 'install', $packageWithVersion, '--save-dev']; + } else { + $this->addToPackageJson($package, $version); + + return; + } + + (new Process($command, $this->files->getRootDirectory()))->run(); + } + + public function install(): void + { + (new Process([$this->bin(), 'install'], $this->files->getRootDirectory()))->run(); + } + + public function run(string $script): void + { + (new Process([$this->bin(), 'run', $script], $this->files->getRootDirectory()))->run(); + } + + public function isAvailable(): bool + { + try { + $this->bin(); + + return true; + } catch (\RuntimeException $e) { + return false; + } + } + + private function bin(): string + { + if (!$bin = $this->executableFinder->find('yarn') ?? $this->executableFinder->find('npm')) { + throw new \RuntimeException('Unable to find js package manager.'); + } + + return $bin; + } + + private function addToPackageJson(string $package, string $version): void + { + $packageJson = json_decode($this->files->getFileContents('package.json'), true); + $devDeps = $packageJson['devDependencies'] ?? []; + $devDeps[$package] = $version; + + ksort($devDeps); + + $packageJson['devDependencies'] = $devDeps; + + $this->files->dumpFile('package.json', json_encode($packageJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + } +} diff --git a/src/Maker/MakeScaffold.php b/src/Maker/MakeScaffold.php index 043166489..a7eb6b398 100644 --- a/src/Maker/MakeScaffold.php +++ b/src/Maker/MakeScaffold.php @@ -17,12 +17,14 @@ use Symfony\Bundle\MakerBundle\FileManager; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; +use Symfony\Bundle\MakerBundle\JsPackageManager; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Finder\Finder; use Symfony\Component\HttpKernel\Kernel; +use Symfony\Component\Process\ExecutableFinder; use Symfony\Component\Process\Process; /** @@ -31,13 +33,17 @@ final class MakeScaffold extends AbstractMaker { private $files; + private $jsPackageManager; private $availableScaffolds; + private $composerBin; private $installedScaffolds = []; private $installedPackages = []; + private $installedJsPackages = []; public function __construct(FileManager $files) { $this->files = $files; + $this->jsPackageManager = new JsPackageManager($files); } public static function getCommandName(): string @@ -75,6 +81,18 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen foreach ($names as $name) { $this->generateScaffold($name, $io); } + + if ($this->installedJsPackages) { + if ($this->jsPackageManager->isAvailable()) { + $io->comment('Installing JS packages...'); + $this->jsPackageManager->install(); + + $io->comment('Running Webpack Encore...'); + $this->jsPackageManager->run('dev'); + } else { + $io->warning('Unable to detect JS package manager, you need to run "yarn/npm install".'); + } + } } public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void @@ -108,15 +126,14 @@ private function generateScaffold(string $name, ConsoleStyle $io): void $this->generateScaffold($dependent, $io); } - $io->text("Generating {$name} Scaffold..."); + $io->text("Installing {$name} Scaffold..."); // install required packages foreach ($scaffold['packages'] ?? [] as $package => $env) { if (!$this->isPackageInstalled($package)) { - $io->text("Installing {$package}..."); + $io->text("Installing Composer package: {$package}..."); - // todo composer bin detection - $command = ['composer', 'require', '--no-scripts', 'dev' === $env ? '--dev' : null, $package]; + $command = [$this->composerBin(), 'require', '--no-scripts', 'dev' === $env ? '--dev' : null, $package]; $process = new Process(array_filter($command), $this->files->getRootDirectory()); $process->run(); @@ -129,6 +146,16 @@ private function generateScaffold(string $name, ConsoleStyle $io): void } } + // install required js packages + foreach ($scaffold['js_packages'] ?? [] as $package => $version) { + if (!\in_array($package, $this->installedJsPackages, true)) { + $io->text("Installing JS package: {$package}@{$version}..."); + + $this->jsPackageManager->add($package, $version); + $this->installedJsPackages[] = $package; + } + } + if (is_dir($scaffold['dir'])) { $io->text('Copying scaffold files...'); @@ -190,4 +217,17 @@ private function isScaffoldInstalled(string $name): bool { return \in_array($name, $this->installedScaffolds, true); } + + private function composerBin(): string + { + if ($this->composerBin) { + return $this->composerBin; + } + + if (!$this->composerBin = (new ExecutableFinder())->find('composer')) { + throw new \RuntimeException('Unable to detect composer binary.'); + } + + return $this->composerBin; + } } diff --git a/src/Resources/scaffolds/6.0/bootstrapcss.php b/src/Resources/scaffolds/6.0/bootstrapcss.php index 309332013..ba18b26f4 100644 --- a/src/Resources/scaffolds/6.0/bootstrapcss.php +++ b/src/Resources/scaffolds/6.0/bootstrapcss.php @@ -9,17 +9,11 @@ 'twig' => 'all', 'encore' => 'all', ], + 'js_packages' => [ + 'bootstrap' => '^5.0.0', + '@popperjs/core' => '^2.0.0', + ], 'configure' => function(FileManager $files) { - $packageJson = json_decode($files->getFileContents('package.json'), true); - $devDeps = $packageJson['devDependencies']; - $devDeps['bootstrap'] = '^5.0.0'; - $devDeps['@popperjs/core'] = '^2.0.0'; - - ksort($devDeps); - - $packageJson['devDependencies'] = $devDeps; - $files->dumpFile('package.json', json_encode($packageJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); - $twig = new YamlSourceManipulator($files->getFileContents('config/packages/twig.yaml')); $data = $twig->getData(); $data['twig']['form_themes'] = ['bootstrap_5_layout.html.twig']; diff --git a/src/Test/MakerTestEnvironment.php b/src/Test/MakerTestEnvironment.php index ee38b4d60..59e07b774 100644 --- a/src/Test/MakerTestEnvironment.php +++ b/src/Test/MakerTestEnvironment.php @@ -345,7 +345,7 @@ public function createInteractiveCommandProcess(string $commandName, array $user [ 'SHELL_INTERACTIVE' => '1', ], - 30 + 40 ); if ($userInputs) { diff --git a/tests/Maker/MakeScaffoldTest.php b/tests/Maker/MakeScaffoldTest.php index f1b49984c..2a7aad33d 100644 --- a/tests/Maker/MakeScaffoldTest.php +++ b/tests/Maker/MakeScaffoldTest.php @@ -19,8 +19,6 @@ class MakeScaffoldTest extends MakerTestCase { - private const STARTER_KITS = ['starter-kit']; - protected function setUp(): void { parent::setUp(); @@ -45,12 +43,6 @@ public function getTestDetails(): iterable ) ->run(function (MakerTestRunner $runner) use ($name) { $runner->runMaker([$name]); - - if (in_array($name, self::STARTER_KITS, true)) { - $runner->runProcess('yarn install'); - $runner->runProcess('yarn dev'); - } - $runner->runTests(); $this->assertTrue(true); // successfully ran tests From 263fd0b362e286d8216611855354d4da97c84cda Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Fri, 8 Apr 2022 11:55:15 -0400 Subject: [PATCH 08/21] break out required packs/aliases into actual packages --- src/Resources/scaffolds/6.0/auth.php | 3 ++- src/Resources/scaffolds/6.0/bootstrapcss.php | 4 ++-- src/Resources/scaffolds/6.0/change-password.php | 4 ++-- src/Resources/scaffolds/6.0/homepage.php | 5 +++-- src/Resources/scaffolds/6.0/profile.php | 4 ++-- src/Resources/scaffolds/6.0/register.php | 4 ++-- src/Resources/scaffolds/6.0/reset-password.php | 6 +++--- src/Resources/scaffolds/6.0/user.php | 8 +++++--- src/Resources/scaffolds/6.0/user/src/Entity/User.php | 2 +- 9 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/Resources/scaffolds/6.0/auth.php b/src/Resources/scaffolds/6.0/auth.php index 764141b38..248bdbe1c 100644 --- a/src/Resources/scaffolds/6.0/auth.php +++ b/src/Resources/scaffolds/6.0/auth.php @@ -10,7 +10,8 @@ 'homepage', ], 'packages' => [ - 'profiler' => 'dev', + 'symfony/web-profiler-bundle' => 'dev', + 'symfony/stopwatch' => 'dev', ], 'configure' => function(FileManager $files) { $services = new YamlSourceManipulator($files->getFileContents('config/services.yaml')); diff --git a/src/Resources/scaffolds/6.0/bootstrapcss.php b/src/Resources/scaffolds/6.0/bootstrapcss.php index ba18b26f4..47fc8dfdf 100644 --- a/src/Resources/scaffolds/6.0/bootstrapcss.php +++ b/src/Resources/scaffolds/6.0/bootstrapcss.php @@ -6,8 +6,8 @@ return [ 'description' => 'Add bootstrap css/js.', 'packages' => [ - 'twig' => 'all', - 'encore' => 'all', + 'symfony/twig-bundle' => 'all', + 'symfony/webpack-encore-bundle' => 'all', ], 'js_packages' => [ 'bootstrap' => '^5.0.0', diff --git a/src/Resources/scaffolds/6.0/change-password.php b/src/Resources/scaffolds/6.0/change-password.php index 8368c9620..00b445937 100644 --- a/src/Resources/scaffolds/6.0/change-password.php +++ b/src/Resources/scaffolds/6.0/change-password.php @@ -6,7 +6,7 @@ 'auth', ], 'packages' => [ - 'form' => 'all', - 'validator' => 'all', + 'symfony/form' => 'all', + 'symfony/validator' => 'all', ] ]; diff --git a/src/Resources/scaffolds/6.0/homepage.php b/src/Resources/scaffolds/6.0/homepage.php index c82f9c120..e086bffdb 100644 --- a/src/Resources/scaffolds/6.0/homepage.php +++ b/src/Resources/scaffolds/6.0/homepage.php @@ -3,8 +3,9 @@ return [ 'description' => 'Create a basic homepage controller/template/test.', 'packages' => [ - 'twig' => 'all', - 'phpunit' => 'dev', + 'symfony/twig-bundle' => 'all', + 'phpunit/phpunit' => 'dev', + 'symfony/phpunit-bridge' => 'dev', 'zenstruck/browser' => 'dev', ] ]; diff --git a/src/Resources/scaffolds/6.0/profile.php b/src/Resources/scaffolds/6.0/profile.php index 11b592b85..ace2aaefb 100644 --- a/src/Resources/scaffolds/6.0/profile.php +++ b/src/Resources/scaffolds/6.0/profile.php @@ -6,7 +6,7 @@ 'auth', ], 'packages' => [ - 'form' => 'all', - 'validator' => 'all', + 'symfony/form' => 'all', + 'symfony/validator' => 'all', ] ]; diff --git a/src/Resources/scaffolds/6.0/register.php b/src/Resources/scaffolds/6.0/register.php index c1cef1e22..60fce2802 100644 --- a/src/Resources/scaffolds/6.0/register.php +++ b/src/Resources/scaffolds/6.0/register.php @@ -8,8 +8,8 @@ 'auth', ], 'packages' => [ - 'form' => 'all', - 'validator' => 'all', + 'symfony/form' => 'all', + 'symfony/validator' => 'all', ], 'configure' => function(FileManager $files) { $userEntity = $files->getFileContents('src/Entity/User.php'); diff --git a/src/Resources/scaffolds/6.0/reset-password.php b/src/Resources/scaffolds/6.0/reset-password.php index 64692ae7c..3f1ddbd5e 100644 --- a/src/Resources/scaffolds/6.0/reset-password.php +++ b/src/Resources/scaffolds/6.0/reset-password.php @@ -8,9 +8,9 @@ 'auth', ], 'packages' => [ - 'form' => 'all', - 'validator' => 'all', - 'mailer' => 'all', + 'symfony/form' => 'all', + 'symfony/validator' => 'all', + 'symfony/mailer' => 'all', 'symfonycasts/reset-password-bundle' => 'all', 'zenstruck/mailer-test' => 'dev', ], diff --git a/src/Resources/scaffolds/6.0/user.php b/src/Resources/scaffolds/6.0/user.php index dcd93d2ab..8b0178750 100644 --- a/src/Resources/scaffolds/6.0/user.php +++ b/src/Resources/scaffolds/6.0/user.php @@ -3,9 +3,11 @@ return [ 'description' => 'Create a basic user and unit test.', 'packages' => [ - 'orm' => 'all', - 'security' => 'all', - 'phpunit' => 'dev', + 'doctrine/orm' => 'all', + 'doctrine/doctrine-bundle' => 'all', + 'symfony/security-bundle' => 'all', + 'phpunit/phpunit' => 'dev', + 'symfony/phpunit-bridge' => 'dev', 'zenstruck/foundry' => 'dev', ] ]; diff --git a/src/Resources/scaffolds/6.0/user/src/Entity/User.php b/src/Resources/scaffolds/6.0/user/src/Entity/User.php index cc61e8592..77dede2d5 100644 --- a/src/Resources/scaffolds/6.0/user/src/Entity/User.php +++ b/src/Resources/scaffolds/6.0/user/src/Entity/User.php @@ -91,7 +91,7 @@ public function setPassword(string $password): self /** * @see UserInterface */ - public function eraseCredentials() + public function eraseCredentials(): void { // If you store any temporary, sensitive data on the user, clear it here // $this->plainPassword = null; From c8ee93ffbe1966b5f0af857a460b72d2aa51beb6 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Fri, 8 Apr 2022 12:14:22 -0400 Subject: [PATCH 09/21] add `FileManager::manipulateYaml()` --- src/FileManager.php | 11 +++ src/Resources/scaffolds/6.0/auth.php | 75 ++++++++++---------- src/Resources/scaffolds/6.0/bootstrapcss.php | 14 ++-- 3 files changed, 57 insertions(+), 43 deletions(-) diff --git a/src/FileManager.php b/src/FileManager.php index f2d5299f7..c10b4e5a5 100644 --- a/src/FileManager.php +++ b/src/FileManager.php @@ -13,6 +13,7 @@ use Symfony\Bundle\MakerBundle\Util\AutoloaderUtil; use Symfony\Bundle\MakerBundle\Util\MakerFileLinkFormatter; +use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Finder\Finder; @@ -86,6 +87,16 @@ public function dumpFile(string $filename, string $content): void } } + /** + * @param callable(array):array $manipulator + */ + public function manipulateYaml(string $filename, callable $manipulator): void + { + $yaml = new YamlSourceManipulator($this->getFileContents($filename)); + $yaml->setData($manipulator($yaml->getData())); + $this->dumpFile($filename, $yaml->getContents()); + } + public function fileExists($path): bool { return file_exists($this->absolutizePath($path)); diff --git a/src/Resources/scaffolds/6.0/auth.php b/src/Resources/scaffolds/6.0/auth.php index 248bdbe1c..92cd95ca3 100644 --- a/src/Resources/scaffolds/6.0/auth.php +++ b/src/Resources/scaffolds/6.0/auth.php @@ -1,7 +1,6 @@ 'Create login form and tests.', @@ -14,43 +13,45 @@ 'symfony/stopwatch' => 'dev', ], 'configure' => function(FileManager $files) { - $services = new YamlSourceManipulator($files->getFileContents('config/services.yaml')); - $data = $services->getData(); - $data['services']['App\Security\LoginUser']['$authenticator'] ='@security.authenticator.form_login.main'; - $services->setData($data); - $files->dumpFile('config/services.yaml', $services->getContents()); + // add LoginUser service + $files->manipulateYaml('config/services.yaml', function(array $data) { + $data['services']['App\Security\LoginUser']['$authenticator'] ='@security.authenticator.form_login.main'; - $security = new YamlSourceManipulator($files->getFileContents('config/packages/security.yaml')); - $data = $security->getData(); - $data['security']['providers'] = [ - 'app_user_provider' => [ - 'entity' => [ - 'class' => 'App\Entity\User', - 'property' => 'email', + return $data; + }); + + // make security.yaml adjustments + $files->manipulateYaml('config/packages/security.yaml', function(array $data) { + $data['security']['providers'] = [ + 'app_user_provider' => [ + 'entity' => [ + 'class' => 'App\Entity\User', + 'property' => 'email', + ], + ], + ]; + $data['security']['firewalls']['main'] = [ + 'lazy' => true, + 'provider' => 'app_user_provider', + 'form_login' => [ + 'login_path' => 'login', + 'check_path' => 'login', + 'username_parameter' => 'email', + 'password_parameter' => 'password', + 'enable_csrf' => true, + ], + 'logout' => [ + 'path' => 'logout', + 'target' => 'homepage', ], - ], - ]; - $data['security']['firewalls']['main'] = [ - 'lazy' => true, - 'provider' => 'app_user_provider', - 'form_login' => [ - 'login_path' => 'login', - 'check_path' => 'login', - 'username_parameter' => 'email', - 'password_parameter' => 'password', - 'enable_csrf' => true, - ], - 'logout' => [ - 'path' => 'logout', - 'target' => 'homepage', - ], - 'remember_me' => [ - 'secret' => '%kernel.secret%', - 'secure' => 'auto', - 'samesite' => 'lax', - ], - ]; - $security->setData($data); - $files->dumpFile('config/packages/security.yaml', $security->getContents()); + 'remember_me' => [ + 'secret' => '%kernel.secret%', + 'secure' => 'auto', + 'samesite' => 'lax', + ], + ]; + + return $data; + }); }, ]; diff --git a/src/Resources/scaffolds/6.0/bootstrapcss.php b/src/Resources/scaffolds/6.0/bootstrapcss.php index 47fc8dfdf..ba6ebf897 100644 --- a/src/Resources/scaffolds/6.0/bootstrapcss.php +++ b/src/Resources/scaffolds/6.0/bootstrapcss.php @@ -1,7 +1,6 @@ 'Add bootstrap css/js.', @@ -14,14 +13,17 @@ '@popperjs/core' => '^2.0.0', ], 'configure' => function(FileManager $files) { - $twig = new YamlSourceManipulator($files->getFileContents('config/packages/twig.yaml')); - $data = $twig->getData(); - $data['twig']['form_themes'] = ['bootstrap_5_layout.html.twig']; - $twig->setData($data); - $files->dumpFile('config/packages/twig.yaml', $twig->getContents()); + // add bootstrap form theme + $files->manipulateYaml('config/packages/twig.yaml', function(array $data) { + $data['twig']['form_themes'] = ['bootstrap_5_layout.html.twig']; + return $data; + }); + + // add bootstrap to app.css $files->dumpFile('assets/styles/app.css', "@import \"~bootstrap/dist/css/bootstrap.css\";\n"); + // add bootstrap to app.js $appJs = $files->getFileContents('assets/app.js'); if (str_contains($appJs, "require('bootstrap');")) { From 5f7a732ca1b673943f6527c3d83ab0f61778dcb3 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Fri, 8 Apr 2022 12:22:48 -0400 Subject: [PATCH 10/21] cs fixes --- src/JsPackageManager.php | 2 +- src/Maker/MakeScaffold.php | 10 +-- src/Resources/scaffolds/6.0/auth.php | 17 +++- .../6.0/auth/tests/Browser/Authentication.php | 12 +-- .../tests/Functional/AuthenticationTest.php | 73 ++++------------ src/Resources/scaffolds/6.0/bootstrapcss.php | 13 ++- .../scaffolds/6.0/change-password.php | 11 ++- .../User/ChangePasswordController.php | 5 +- .../Functional/User/ChangePasswordTest.php | 39 +++------ src/Resources/scaffolds/6.0/homepage.php | 11 ++- .../tests/Functional/HomepageTest.php | 5 +- src/Resources/scaffolds/6.0/profile.php | 11 ++- .../tests/Functional/User/ProfileTest.php | 19 ++--- src/Resources/scaffolds/6.0/register.php | 13 ++- .../Controller/User/RegisterController.php | 3 +- .../tests/Functional/User/RegisterTest.php | 39 +++------ .../scaffolds/6.0/reset-password.php | 11 ++- .../Controller/ResetPasswordController.php | 2 +- .../Factory/ResetPasswordRequestFactory.php | 28 +++---- .../tests/Functional/ResetPasswordTest.php | 83 +++++-------------- src/Resources/scaffolds/6.0/starter-kit.php | 11 ++- src/Resources/scaffolds/6.0/user.php | 11 ++- .../6.0/user/src/Factory/UserFactory.php | 30 +++---- .../6.0/user/tests/Unit/Entity/UserTest.php | 5 +- 24 files changed, 208 insertions(+), 256 deletions(-) diff --git a/src/JsPackageManager.php b/src/JsPackageManager.php index 870c6214f..0ab82584e 100644 --- a/src/JsPackageManager.php +++ b/src/JsPackageManager.php @@ -85,6 +85,6 @@ private function addToPackageJson(string $package, string $version): void $packageJson['devDependencies'] = $devDeps; - $this->files->dumpFile('package.json', json_encode($packageJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + $this->files->dumpFile('package.json', json_encode($packageJson, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)); } } diff --git a/src/Maker/MakeScaffold.php b/src/Maker/MakeScaffold.php index a7eb6b398..454b9e0dd 100644 --- a/src/Maker/MakeScaffold.php +++ b/src/Maker/MakeScaffold.php @@ -59,7 +59,7 @@ public static function getCommandDescription(): string public function configureCommand(Command $command, InputConfiguration $inputConfig): void { $command - ->addArgument('name', InputArgument::OPTIONAL|InputArgument::IS_ARRAY, 'Scaffold name(s) to create') + ->addArgument('name', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Scaffold name(s) to create') ; $inputConfig->setArgumentAsNonInteractive('name'); @@ -103,7 +103,7 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma $availableScaffolds = array_combine( array_keys($this->availableScaffolds()), - array_map(fn(array $scaffold) => $scaffold['description'], $this->availableScaffolds()) + array_map(fn (array $scaffold) => $scaffold['description'], $this->availableScaffolds()) ); $input->setArgument('name', [$io->choice('Available scaffolds', $availableScaffolds)]); @@ -176,14 +176,14 @@ private function generateScaffold(string $name, ConsoleStyle $io): void private function availableScaffolds(): array { - if (is_array($this->availableScaffolds)) { + if (\is_array($this->availableScaffolds)) { return $this->availableScaffolds; } $this->availableScaffolds = []; $finder = Finder::create() // todo, improve versioning system - ->in(\sprintf('%s/../Resources/scaffolds/%s.0', __DIR__, Kernel::MAJOR_VERSION)) + ->in(sprintf('%s/../Resources/scaffolds/%s.0', __DIR__, Kernel::MAJOR_VERSION)) ->name('*.php') ->depth(0) ; @@ -193,7 +193,7 @@ private function availableScaffolds(): array $this->availableScaffolds[$name] = array_merge( require $file, - ['dir' => dirname($file->getRealPath()).'/'.$name] + ['dir' => \dirname($file->getRealPath()).'/'.$name] ); } diff --git a/src/Resources/scaffolds/6.0/auth.php b/src/Resources/scaffolds/6.0/auth.php index 92cd95ca3..83002a4bf 100644 --- a/src/Resources/scaffolds/6.0/auth.php +++ b/src/Resources/scaffolds/6.0/auth.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + use Symfony\Bundle\MakerBundle\FileManager; return [ @@ -12,16 +21,16 @@ 'symfony/web-profiler-bundle' => 'dev', 'symfony/stopwatch' => 'dev', ], - 'configure' => function(FileManager $files) { + 'configure' => function (FileManager $files) { // add LoginUser service - $files->manipulateYaml('config/services.yaml', function(array $data) { - $data['services']['App\Security\LoginUser']['$authenticator'] ='@security.authenticator.form_login.main'; + $files->manipulateYaml('config/services.yaml', function (array $data) { + $data['services']['App\Security\LoginUser']['$authenticator'] = '@security.authenticator.form_login.main'; return $data; }); // make security.yaml adjustments - $files->manipulateYaml('config/packages/security.yaml', function(array $data) { + $files->manipulateYaml('config/packages/security.yaml', function (array $data) { $data['security']['providers'] = [ 'app_user_provider' => [ 'entity' => [ diff --git a/src/Resources/scaffolds/6.0/auth/tests/Browser/Authentication.php b/src/Resources/scaffolds/6.0/auth/tests/Browser/Authentication.php index 576c31b81..fafe4beb6 100644 --- a/src/Resources/scaffolds/6.0/auth/tests/Browser/Authentication.php +++ b/src/Resources/scaffolds/6.0/auth/tests/Browser/Authentication.php @@ -12,14 +12,14 @@ class Authentication extends Component { public static function assertAuthenticated(): \Closure { - return static function(self $auth) { + return static function (self $auth) { Assert::assertTrue($auth->collector()->isAuthenticated()); }; } public static function assertAuthenticatedAs(string $email): \Closure { - return static function(self $auth) use ($email) { + return static function (self $auth) use ($email) { $collector = $auth->collector(); Assert::assertTrue($collector->isAuthenticated()); @@ -29,14 +29,14 @@ public static function assertAuthenticatedAs(string $email): \Closure public static function assertNotAuthenticated(): \Closure { - return static function(self $auth) { + return static function (self $auth) { Assert::assertFalse($auth->collector()->isAuthenticated()); }; } public static function expireSession(): \Closure { - return static function(CookieJar $cookies) { + return static function (CookieJar $cookies) { $cookies->expire('MOCKSESSID'); }; } @@ -45,7 +45,7 @@ private function collector(): SecurityDataCollector { $browser = $this->browser(); - assert($browser instanceof KernelBrowser); + \assert($browser instanceof KernelBrowser); $collector = $browser ->withProfiling() @@ -54,7 +54,7 @@ private function collector(): SecurityDataCollector ->getCollector('security') ; - assert($collector instanceof SecurityDataCollector); + \assert($collector instanceof SecurityDataCollector); return $collector; } diff --git a/src/Resources/scaffolds/6.0/auth/tests/Functional/AuthenticationTest.php b/src/Resources/scaffolds/6.0/auth/tests/Functional/AuthenticationTest.php index 2cd26ad0f..3781f7031 100644 --- a/src/Resources/scaffolds/6.0/auth/tests/Functional/AuthenticationTest.php +++ b/src/Resources/scaffolds/6.0/auth/tests/Functional/AuthenticationTest.php @@ -11,12 +11,11 @@ class AuthenticationTest extends KernelTestCase { - use HasBrowser, Factories, ResetDatabase; + use Factories; + use HasBrowser; + use ResetDatabase; - /** - * @test - */ - public function can_login_and_logout(): void + public function testCanLoginAndLogout(): void { UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); @@ -35,10 +34,7 @@ public function can_login_and_logout(): void ; } - /** - * @test - */ - public function login_with_target(): void + public function testLoginWithTarget(): void { UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); @@ -53,10 +49,7 @@ public function login_with_target(): void ; } - /** - * @test - */ - public function login_with_invalid_password(): void + public function testLoginWithInvalidPassword(): void { UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); @@ -73,10 +66,7 @@ public function login_with_invalid_password(): void ; } - /** - * @test - */ - public function login_with_invalid_email(): void + public function testLoginWithInvalidEmail(): void { $this->browser() ->visit('/login') @@ -91,10 +81,7 @@ public function login_with_invalid_email(): void ; } - /** - * @test - */ - public function login_with_invalid_csrf(): void + public function testLoginWithInvalidCsrf(): void { UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); @@ -108,10 +95,7 @@ public function login_with_invalid_csrf(): void ; } - /** - * @test - */ - public function remember_me_enabled_by_default(): void + public function testRememberMeEnabledByDefault(): void { UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); @@ -128,10 +112,7 @@ public function remember_me_enabled_by_default(): void ; } - /** - * @test - */ - public function can_disable_remember_me(): void + public function testCanDisableRememberMe(): void { UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); @@ -149,10 +130,7 @@ public function can_disable_remember_me(): void ; } - /** - * @test - */ - public function fully_authenticated_login_redirect(): void + public function testFullyAuthenticatedLoginRedirect(): void { UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); @@ -169,10 +147,7 @@ public function fully_authenticated_login_redirect(): void ; } - /** - * @test - */ - public function fully_authenticated_login_target(): void + public function testFullyAuthenticatedLoginTarget(): void { UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); @@ -189,10 +164,7 @@ public function fully_authenticated_login_target(): void ; } - /** - * @test - */ - public function can_fully_authenticate_if_only_remembered(): void + public function testCanFullyAuthenticateIfOnlyRemembered(): void { UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); @@ -213,10 +185,7 @@ public function can_fully_authenticate_if_only_remembered(): void ; } - /** - * @test - */ - public function legacy_password_hash_is_automatically_migrated_on_login(): void + public function testLegacyPasswordHashIsAutomaticallyMigratedOnLogin(): void { $user = UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); @@ -224,7 +193,7 @@ public function legacy_password_hash_is_automatically_migrated_on_login(): void $user->setPassword('$argon2id$v=19$m=10,t=3,p=1$K9AFR15goJiUD6AdpK0a6Q$RsP6y+FRnYUBovBmhVZO7wN6Caj2eI8dMTnm3+5aTxk'); $user->save(); - $this->assertSame(\PASSWORD_ARGON2ID, \password_get_info($user->getPassword())['algo']); + $this->assertSame(\PASSWORD_ARGON2ID, password_get_info($user->getPassword())['algo']); $this->browser() ->use(Authentication::assertNotAuthenticated()) @@ -237,22 +206,16 @@ public function legacy_password_hash_is_automatically_migrated_on_login(): void ->use(Authentication::assertAuthenticatedAs('mary@example.com')) ; - $this->assertSame(\PASSWORD_DEFAULT, \password_get_info($user->getPassword())['algo']); + $this->assertSame(\PASSWORD_DEFAULT, password_get_info($user->getPassword())['algo']); } - /** - * @test - */ - public function auto_redirected_to_authenticated_resource_after_login(): void + public function testAutoRedirectedToAuthenticatedResourceAfterLogin(): void { // complete this test when you have a page that requires authentication $this->markTestIncomplete(); } - /** - * @test - */ - public function auto_redirected_to_fully_authenticated_resource_after_fully_authenticated(): void + public function testAutoRedirectedToFullyAuthenticatedResourceAfterFullyAuthenticated(): void { // complete this test when/if you have a page that requires the user be "fully authenticated" $this->markTestIncomplete(); diff --git a/src/Resources/scaffolds/6.0/bootstrapcss.php b/src/Resources/scaffolds/6.0/bootstrapcss.php index ba6ebf897..f5d0a596a 100644 --- a/src/Resources/scaffolds/6.0/bootstrapcss.php +++ b/src/Resources/scaffolds/6.0/bootstrapcss.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + use Symfony\Bundle\MakerBundle\FileManager; return [ @@ -12,9 +21,9 @@ 'bootstrap' => '^5.0.0', '@popperjs/core' => '^2.0.0', ], - 'configure' => function(FileManager $files) { + 'configure' => function (FileManager $files) { // add bootstrap form theme - $files->manipulateYaml('config/packages/twig.yaml', function(array $data) { + $files->manipulateYaml('config/packages/twig.yaml', function (array $data) { $data['twig']['form_themes'] = ['bootstrap_5_layout.html.twig']; return $data; diff --git a/src/Resources/scaffolds/6.0/change-password.php b/src/Resources/scaffolds/6.0/change-password.php index 00b445937..39c2582ad 100644 --- a/src/Resources/scaffolds/6.0/change-password.php +++ b/src/Resources/scaffolds/6.0/change-password.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + return [ 'description' => 'Create change password form and tests.', 'dependents' => [ @@ -8,5 +17,5 @@ 'packages' => [ 'symfony/form' => 'all', 'symfony/validator' => 'all', - ] + ], ]; diff --git a/src/Resources/scaffolds/6.0/change-password/src/Controller/User/ChangePasswordController.php b/src/Resources/scaffolds/6.0/change-password/src/Controller/User/ChangePasswordController.php index 2d8084427..4ed825bdb 100644 --- a/src/Resources/scaffolds/6.0/change-password/src/Controller/User/ChangePasswordController.php +++ b/src/Resources/scaffolds/6.0/change-password/src/Controller/User/ChangePasswordController.php @@ -20,8 +20,7 @@ public function __invoke( UserPasswordHasherInterface $userPasswordHasher, UserRepository $userRepository, ?UserInterface $user = null, - ): Response - { + ): Response { if (!$user) { throw $this->createAccessDeniedException(); } @@ -49,7 +48,7 @@ public function __invoke( } return $this->render('user/change_password.html.twig', [ - 'changePasswordForm' => $form->createView() + 'changePasswordForm' => $form->createView(), ]); } } diff --git a/src/Resources/scaffolds/6.0/change-password/tests/Functional/User/ChangePasswordTest.php b/src/Resources/scaffolds/6.0/change-password/tests/Functional/User/ChangePasswordTest.php index f9e4d6866..e443f465b 100644 --- a/src/Resources/scaffolds/6.0/change-password/tests/Functional/User/ChangePasswordTest.php +++ b/src/Resources/scaffolds/6.0/change-password/tests/Functional/User/ChangePasswordTest.php @@ -14,12 +14,11 @@ */ final class ChangePasswordTest extends KernelTestCase { - use HasBrowser, Factories, ResetDatabase; + use Factories; + use HasBrowser; + use ResetDatabase; - /** - * @test - */ - public function can_change_password(): void + public function testCanChangePassword(): void { $user = UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); $currentPassword = $user->getPassword(); @@ -48,10 +47,7 @@ public function can_change_password(): void $this->assertNotSame($currentPassword, $user->getPassword()); } - /** - * @test - */ - public function current_password_must_be_correct(): void + public function testCurrentPasswordMustBeCorrect(): void { $user = UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); $currentPassword = $user->getPassword(); @@ -72,10 +68,7 @@ public function current_password_must_be_correct(): void $this->assertSame($currentPassword, $user->getPassword()); } - /** - * @test - */ - public function current_password_is_required(): void + public function testCurrentPasswordIsRequired(): void { $user = UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); $currentPassword = $user->getPassword(); @@ -95,10 +88,7 @@ public function current_password_is_required(): void $this->assertSame($currentPassword, $user->getPassword()); } - /** - * @test - */ - public function new_password_is_required(): void + public function testNewPasswordIsRequired(): void { $user = UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); $currentPassword = $user->getPassword(); @@ -117,10 +107,7 @@ public function new_password_is_required(): void $this->assertSame($currentPassword, $user->getPassword()); } - /** - * @test - */ - public function new_passwords_must_match(): void + public function testNewPasswordsMustMatch(): void { $user = UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); $currentPassword = $user->getPassword(); @@ -141,10 +128,7 @@ public function new_passwords_must_match(): void $this->assertSame($currentPassword, $user->getPassword()); } - /** - * @test - */ - public function new_password_must_be_min_length(): void + public function testNewPasswordMustBeMinLength(): void { $user = UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); $currentPassword = $user->getPassword(); @@ -165,10 +149,7 @@ public function new_password_must_be_min_length(): void $this->assertSame($currentPassword, $user->getPassword()); } - /** - * @test - */ - public function cannot_access_change_password_page_if_not_logged_in(): void + public function testCannotAccessChangePasswordPageIfNotLoggedIn(): void { $this->browser() ->visit('/user/change-password') diff --git a/src/Resources/scaffolds/6.0/homepage.php b/src/Resources/scaffolds/6.0/homepage.php index e086bffdb..05abde185 100644 --- a/src/Resources/scaffolds/6.0/homepage.php +++ b/src/Resources/scaffolds/6.0/homepage.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + return [ 'description' => 'Create a basic homepage controller/template/test.', 'packages' => [ @@ -7,5 +16,5 @@ 'phpunit/phpunit' => 'dev', 'symfony/phpunit-bridge' => 'dev', 'zenstruck/browser' => 'dev', - ] + ], ]; diff --git a/src/Resources/scaffolds/6.0/homepage/tests/Functional/HomepageTest.php b/src/Resources/scaffolds/6.0/homepage/tests/Functional/HomepageTest.php index 8faac80e3..dfd1b0221 100644 --- a/src/Resources/scaffolds/6.0/homepage/tests/Functional/HomepageTest.php +++ b/src/Resources/scaffolds/6.0/homepage/tests/Functional/HomepageTest.php @@ -9,10 +9,7 @@ class HomepageTest extends KernelTestCase { use HasBrowser; - /** - * @test - */ - public function visit_homepage(): void + public function testVisitHomepage(): void { $this->browser() ->visit('/') diff --git a/src/Resources/scaffolds/6.0/profile.php b/src/Resources/scaffolds/6.0/profile.php index ace2aaefb..ae9fc80b4 100644 --- a/src/Resources/scaffolds/6.0/profile.php +++ b/src/Resources/scaffolds/6.0/profile.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + return [ 'description' => 'Create user profile form and tests.', 'dependents' => [ @@ -8,5 +17,5 @@ 'packages' => [ 'symfony/form' => 'all', 'symfony/validator' => 'all', - ] + ], ]; diff --git a/src/Resources/scaffolds/6.0/profile/tests/Functional/User/ProfileTest.php b/src/Resources/scaffolds/6.0/profile/tests/Functional/User/ProfileTest.php index 8b70a0063..eb254373c 100644 --- a/src/Resources/scaffolds/6.0/profile/tests/Functional/User/ProfileTest.php +++ b/src/Resources/scaffolds/6.0/profile/tests/Functional/User/ProfileTest.php @@ -13,12 +13,11 @@ */ final class ProfileTest extends KernelTestCase { - use HasBrowser, Factories, ResetDatabase; + use Factories; + use HasBrowser; + use ResetDatabase; - /** - * @test - */ - public function can_update_profile(): void + public function testCanUpdateProfile(): void { $user = UserFactory::createOne(['name' => 'Mary Edwards']); @@ -38,10 +37,7 @@ public function can_update_profile(): void $this->assertSame('John Smith', $user->getName()); } - /** - * @test - */ - public function name_is_required(): void + public function testNameIsRequired(): void { $user = UserFactory::createOne(['name' => 'Mary Edwards']); @@ -60,10 +56,7 @@ public function name_is_required(): void UserFactory::assert()->exists(['name' => 'Mary Edwards']); } - /** - * @test - */ - public function cannot_access_profile_page_if_not_logged_in(): void + public function testCannotAccessProfilePageIfNotLoggedIn(): void { $this->browser() ->visit('/user') diff --git a/src/Resources/scaffolds/6.0/register.php b/src/Resources/scaffolds/6.0/register.php index 60fce2802..51fec0230 100644 --- a/src/Resources/scaffolds/6.0/register.php +++ b/src/Resources/scaffolds/6.0/register.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + use Symfony\Bundle\MakerBundle\FileManager; return [ @@ -11,7 +20,7 @@ 'symfony/form' => 'all', 'symfony/validator' => 'all', ], - 'configure' => function(FileManager $files) { + 'configure' => function (FileManager $files) { $userEntity = $files->getFileContents('src/Entity/User.php'); if (str_contains($userEntity, $attribute = "#[UniqueEntity(fields: ['email'], message: 'There is already an account with this email')]")) { @@ -22,7 +31,7 @@ $userEntity = str_replace( [ '#[ORM\Entity(repositoryClass: UserRepository::class)]', - 'use Doctrine\ORM\Mapping as ORM;' + 'use Doctrine\ORM\Mapping as ORM;', ], [ "#[ORM\Entity(repositoryClass: UserRepository::class)]\n{$attribute}", diff --git a/src/Resources/scaffolds/6.0/register/src/Controller/User/RegisterController.php b/src/Resources/scaffolds/6.0/register/src/Controller/User/RegisterController.php index b1b317011..c7748d207 100644 --- a/src/Resources/scaffolds/6.0/register/src/Controller/User/RegisterController.php +++ b/src/Resources/scaffolds/6.0/register/src/Controller/User/RegisterController.php @@ -20,8 +20,7 @@ public function __invoke( UserPasswordHasherInterface $userPasswordHasher, UserRepository $userRepository, LoginUser $login, - ): Response - { + ): Response { $user = new User(); $form = $this->createForm(RegistrationFormType::class, $user); $form->handleRequest($request); diff --git a/src/Resources/scaffolds/6.0/register/tests/Functional/User/RegisterTest.php b/src/Resources/scaffolds/6.0/register/tests/Functional/User/RegisterTest.php index 181058ce9..a28823591 100644 --- a/src/Resources/scaffolds/6.0/register/tests/Functional/User/RegisterTest.php +++ b/src/Resources/scaffolds/6.0/register/tests/Functional/User/RegisterTest.php @@ -11,12 +11,11 @@ class RegisterTest extends KernelTestCase { - use HasBrowser, Factories, ResetDatabase; + use Factories; + use HasBrowser; + use ResetDatabase; - /** - * @test - */ - public function can_register(): void + public function testCanRegister(): void { UserFactory::assert()->empty(); @@ -44,10 +43,7 @@ public function can_register(): void UserFactory::assert()->exists(['name' => 'Madison', 'email' => 'madison@example.com']); } - /** - * @test - */ - public function name_is_required(): void + public function testNameIsRequired(): void { $this->browser() ->throwExceptions() @@ -62,10 +58,7 @@ public function name_is_required(): void ; } - /** - * @test - */ - public function email_is_required(): void + public function testEmailIsRequired(): void { $this->browser() ->throwExceptions() @@ -80,10 +73,7 @@ public function email_is_required(): void ; } - /** - * @test - */ - public function email_must_be_email_address(): void + public function testEmailMustBeEmailAddress(): void { $this->browser() ->throwExceptions() @@ -99,10 +89,7 @@ public function email_must_be_email_address(): void ; } - /** - * @test - */ - public function email_must_be_unique(): void + public function testEmailMustBeUnique(): void { UserFactory::createOne(['email' => 'madison@example.com']); @@ -120,10 +107,7 @@ public function email_must_be_unique(): void ; } - /** - * @test - */ - public function password_is_required(): void + public function testPasswordIsRequired(): void { $this->browser() ->throwExceptions() @@ -138,10 +122,7 @@ public function password_is_required(): void ; } - /** - * @test - */ - public function password_must_be_min_length(): void + public function testPasswordMustBeMinLength(): void { $this->browser() ->throwExceptions() diff --git a/src/Resources/scaffolds/6.0/reset-password.php b/src/Resources/scaffolds/6.0/reset-password.php index 3f1ddbd5e..39559b691 100644 --- a/src/Resources/scaffolds/6.0/reset-password.php +++ b/src/Resources/scaffolds/6.0/reset-password.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + use Symfony\Bundle\MakerBundle\FileManager; return [ @@ -14,7 +23,7 @@ 'symfonycasts/reset-password-bundle' => 'all', 'zenstruck/mailer-test' => 'dev', ], - 'configure' => function(FileManager $files) { + 'configure' => function (FileManager $files) { $files->dumpFile( 'config/packages/reset_password.yaml', file_get_contents(__DIR__.'/reset-password/config/packages/reset_password.yaml') diff --git a/src/Resources/scaffolds/6.0/reset-password/src/Controller/ResetPasswordController.php b/src/Resources/scaffolds/6.0/reset-password/src/Controller/ResetPasswordController.php index 834c8c2f6..111684aa5 100644 --- a/src/Resources/scaffolds/6.0/reset-password/src/Controller/ResetPasswordController.php +++ b/src/Resources/scaffolds/6.0/reset-password/src/Controller/ResetPasswordController.php @@ -3,8 +3,8 @@ namespace App\Controller; use App\Entity\User; -use App\Form\ResetPassword\ResetPasswordFormType; use App\Form\ResetPassword\RequestResetFormType; +use App\Form\ResetPassword\ResetPasswordFormType; use App\Repository\UserRepository; use App\Security\LoginUser; use Symfony\Bridge\Twig\Mime\TemplatedEmail; diff --git a/src/Resources/scaffolds/6.0/reset-password/src/Factory/ResetPasswordRequestFactory.php b/src/Resources/scaffolds/6.0/reset-password/src/Factory/ResetPasswordRequestFactory.php index 5696a7499..d95d4425f 100644 --- a/src/Resources/scaffolds/6.0/reset-password/src/Factory/ResetPasswordRequestFactory.php +++ b/src/Resources/scaffolds/6.0/reset-password/src/Factory/ResetPasswordRequestFactory.php @@ -4,26 +4,26 @@ use App\Entity\ResetPasswordRequest; use App\Repository\ResetPasswordRequestRepository; -use Zenstruck\Foundry\RepositoryProxy; use Zenstruck\Foundry\ModelFactory; use Zenstruck\Foundry\Proxy; +use Zenstruck\Foundry\RepositoryProxy; /** * @extends ModelFactory * - * @method static ResetPasswordRequest|Proxy createOne(array $attributes = []) - * @method static ResetPasswordRequest[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static ResetPasswordRequest|Proxy find(object|array|mixed $criteria) - * @method static ResetPasswordRequest|Proxy findOrCreate(array $attributes) - * @method static ResetPasswordRequest|Proxy first(string $sortedField = 'id') - * @method static ResetPasswordRequest|Proxy last(string $sortedField = 'id') - * @method static ResetPasswordRequest|Proxy random(array $attributes = []) - * @method static ResetPasswordRequest|Proxy randomOrCreate(array $attributes = []) - * @method static ResetPasswordRequest[]|Proxy[] all() - * @method static ResetPasswordRequest[]|Proxy[] findBy(array $attributes) - * @method static ResetPasswordRequest[]|Proxy[] randomSet(int $number, array $attributes = []) - * @method static ResetPasswordRequest[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) - * @method static ResetPasswordRequestRepository|RepositoryProxy repository() + * @method static ResetPasswordRequest|Proxy createOne(array $attributes = []) + * @method static ResetPasswordRequest[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static ResetPasswordRequest|Proxy find(object|array|mixed $criteria) + * @method static ResetPasswordRequest|Proxy findOrCreate(array $attributes) + * @method static ResetPasswordRequest|Proxy first(string $sortedField = 'id') + * @method static ResetPasswordRequest|Proxy last(string $sortedField = 'id') + * @method static ResetPasswordRequest|Proxy random(array $attributes = []) + * @method static ResetPasswordRequest|Proxy randomOrCreate(array $attributes = []) + * @method static ResetPasswordRequest[]|Proxy[] all() + * @method static ResetPasswordRequest[]|Proxy[] findBy(array $attributes) + * @method static ResetPasswordRequest[]|Proxy[] randomSet(int $number, array $attributes = []) + * @method static ResetPasswordRequest[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @method static ResetPasswordRequestRepository|RepositoryProxy repository() * @method ResetPasswordRequest|Proxy create(array|callable $attributes = []) */ final class ResetPasswordRequestFactory extends ModelFactory diff --git a/src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php b/src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php index a137460e5..a03bb2500 100644 --- a/src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php +++ b/src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php @@ -16,12 +16,12 @@ */ final class ResetPasswordTest extends KernelTestCase { - use HasBrowser, Factories, ResetDatabase, InteractsWithMailer; + use Factories; + use HasBrowser; + use InteractsWithMailer; + use ResetDatabase; - /** - * @test - */ - public function can_reset_password(): void + public function testCanResetPassword(): void { UserFactory::createOne(['email' => 'john@example.com', 'name' => 'John', 'password' => '1234']); @@ -69,10 +69,7 @@ public function can_reset_password(): void ; } - /** - * @test - */ - public function request_email_is_required(): void + public function testRequestEmailIsRequired(): void { $this->browser() ->visit('/reset-password') @@ -84,10 +81,7 @@ public function request_email_is_required(): void $this->mailer()->assertNoEmailSent(); } - /** - * @test - */ - public function request_email_must_be_an_email(): void + public function testRequestEmailMustBeAnEmail(): void { $this->browser() ->visit('/reset-password') @@ -100,10 +94,7 @@ public function request_email_must_be_an_email(): void $this->mailer()->assertNoEmailSent(); } - /** - * @test - */ - public function requests_are_throttled(): void + public function testRequestsAreThrottled(): void { UserFactory::createOne(['email' => 'john@example.com']); @@ -126,10 +117,7 @@ public function requests_are_throttled(): void ResetPasswordRequestFactory::assert()->count(1); } - /** - * @test - */ - public function can_request_again_after_throttle_expires(): void + public function testCanRequestAgainAfterThrottleExpires(): void { UserFactory::createOne(['email' => 'john@example.com']); @@ -140,7 +128,7 @@ public function can_request_again_after_throttle_expires(): void ->assertOn('/reset-password/check-email') ->assertSuccessful() ->assertSee('Password Reset Email Sent') - ->use(function() { + ->use(function () { ResetPasswordRequestFactory::first() ->forceSet('requestedAt', new \DateTimeImmutable('-16 minutes')) ->save() @@ -159,10 +147,7 @@ public function can_request_again_after_throttle_expires(): void ResetPasswordRequestFactory::assert()->count(2); } - /** - * @test - */ - public function request_does_not_expose_if_user_was_not_found(): void + public function testRequestDoesNotExposeIfUserWasNotFound(): void { $this->browser() ->visit('/reset-password') @@ -176,10 +161,7 @@ public function request_does_not_expose_if_user_was_not_found(): void $this->mailer()->assertNoEmailSent(); } - /** - * @test - */ - public function reset_password_is_required(): void + public function testResetPasswordIsRequired(): void { $user = UserFactory::createOne(['email' => 'john@example.com']); $currentPassword = $user->getPassword(); @@ -198,10 +180,7 @@ public function reset_password_is_required(): void $this->assertSame($currentPassword, UserFactory::find(['email' => 'john@example.com'])->getPassword()); } - /** - * @test - */ - public function reset_passwords_must_match(): void + public function testResetPasswordsMustMatch(): void { $user = UserFactory::createOne(['email' => 'john@example.com']); $currentPassword = $user->getPassword(); @@ -222,10 +201,7 @@ public function reset_passwords_must_match(): void $this->assertSame($currentPassword, UserFactory::find(['email' => 'john@example.com'])->getPassword()); } - /** - * @test - */ - public function reset_password_must_be_min_length(): void + public function testResetPasswordMustBeMinLength(): void { $user = UserFactory::createOne(['email' => 'john@example.com']); $currentPassword = $user->getPassword(); @@ -246,10 +222,7 @@ public function reset_password_must_be_min_length(): void $this->assertSame($currentPassword, UserFactory::find(['email' => 'john@example.com'])->getPassword()); } - /** - * @test - */ - public function cannot_reset_with_invalid_token(): void + public function testCannotResetWithInvalidToken(): void { $this->browser() ->visit('/reset-password/reset/invalid-token') @@ -258,10 +231,7 @@ public function cannot_reset_with_invalid_token(): void ; } - /** - * @test - */ - public function can_use_old_token_even_after_requesting_another(): void + public function testCanUseOldTokenEvenAfterRequestingAnother(): void { UserFactory::createOne(['email' => 'john@example.com']); @@ -272,7 +242,7 @@ public function can_use_old_token_even_after_requesting_another(): void ->assertOn('/reset-password/check-email') ->assertSuccessful() ->assertSee('Password Reset Email Sent') - ->use(function() { + ->use(function () { ResetPasswordRequestFactory::first() ->forceSet('requestedAt', new \DateTimeImmutable('-16 minutes')) ->save() @@ -284,7 +254,7 @@ public function can_use_old_token_even_after_requesting_another(): void ->assertOn('/reset-password/check-email') ->assertSuccessful() ->assertSee('Password Reset Email Sent') - ->use(function() { + ->use(function () { ResetPasswordRequestFactory::assert()->count(2); }) ->visit($this->mailer()->sentEmails()->first()->getHeaders()->get('X-CTA')->getBody()) @@ -298,10 +268,7 @@ public function can_use_old_token_even_after_requesting_another(): void ResetPasswordRequestFactory::assert()->empty(); } - /** - * @test - */ - public function reset_tokens_expire(): void + public function testResetTokensExpire(): void { UserFactory::createOne(['email' => 'john@example.com']); @@ -312,7 +279,7 @@ public function reset_tokens_expire(): void ->assertOn('/reset-password/check-email') ->assertSuccessful() ->assertSee('Password Reset Email Sent') - ->use(function() { + ->use(function () { ResetPasswordRequestFactory::first() ->forceSet('expiresAt', new \DateTimeImmutable('-10 minutes')) ->save() @@ -325,10 +292,7 @@ public function reset_tokens_expire(): void ; } - /** - * @test - */ - public function cannot_use_token_after_password_change(): void + public function testCannotUseTokenAfterPasswordChange(): void { UserFactory::createOne(['email' => 'john@example.com']); @@ -352,10 +316,7 @@ public function cannot_use_token_after_password_change(): void ; } - /** - * @test - */ - public function old_tokens_are_garbage_collected(): void + public function testOldTokensAreGarbageCollected(): void { $user = UserFactory::createOne(['email' => 'jane@example.com']); diff --git a/src/Resources/scaffolds/6.0/starter-kit.php b/src/Resources/scaffolds/6.0/starter-kit.php index bac344a06..f449aa086 100644 --- a/src/Resources/scaffolds/6.0/starter-kit.php +++ b/src/Resources/scaffolds/6.0/starter-kit.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + use Symfony\Bundle\MakerBundle\FileManager; return [ @@ -11,7 +20,7 @@ 'change-password', 'profile', ], - 'configure' => function(FileManager $files) { + 'configure' => function (FileManager $files) { $files->dumpFile('templates/base.html.twig', file_get_contents(__DIR__.'/starter-kit/templates/base.html.twig')); }, ]; diff --git a/src/Resources/scaffolds/6.0/user.php b/src/Resources/scaffolds/6.0/user.php index 8b0178750..9510909aa 100644 --- a/src/Resources/scaffolds/6.0/user.php +++ b/src/Resources/scaffolds/6.0/user.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + return [ 'description' => 'Create a basic user and unit test.', 'packages' => [ @@ -9,5 +18,5 @@ 'phpunit/phpunit' => 'dev', 'symfony/phpunit-bridge' => 'dev', 'zenstruck/foundry' => 'dev', - ] + ], ]; diff --git a/src/Resources/scaffolds/6.0/user/src/Factory/UserFactory.php b/src/Resources/scaffolds/6.0/user/src/Factory/UserFactory.php index 8166c7203..f035cd4d4 100644 --- a/src/Resources/scaffolds/6.0/user/src/Factory/UserFactory.php +++ b/src/Resources/scaffolds/6.0/user/src/Factory/UserFactory.php @@ -5,26 +5,26 @@ use App\Entity\User; use App\Repository\UserRepository; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; -use Zenstruck\Foundry\RepositoryProxy; use Zenstruck\Foundry\ModelFactory; use Zenstruck\Foundry\Proxy; +use Zenstruck\Foundry\RepositoryProxy; /** * @extends ModelFactory * - * @method static User|Proxy createOne(array $attributes = []) - * @method static User[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static User|Proxy find(object|array|mixed $criteria) - * @method static User|Proxy findOrCreate(array $attributes) - * @method static User|Proxy first(string $sortedField = 'id') - * @method static User|Proxy last(string $sortedField = 'id') - * @method static User|Proxy random(array $attributes = []) - * @method static User|Proxy randomOrCreate(array $attributes = []) - * @method static User[]|Proxy[] all() - * @method static User[]|Proxy[] findBy(array $attributes) - * @method static User[]|Proxy[] randomSet(int $number, array $attributes = []) - * @method static User[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) - * @method static UserRepository|RepositoryProxy repository() + * @method static User|Proxy createOne(array $attributes = []) + * @method static User[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static User|Proxy find(object|array|mixed $criteria) + * @method static User|Proxy findOrCreate(array $attributes) + * @method static User|Proxy first(string $sortedField = 'id') + * @method static User|Proxy last(string $sortedField = 'id') + * @method static User|Proxy random(array $attributes = []) + * @method static User|Proxy randomOrCreate(array $attributes = []) + * @method static User[]|Proxy[] all() + * @method static User[]|Proxy[] findBy(array $attributes) + * @method static User[]|Proxy[] randomSet(int $number, array $attributes = []) + * @method static User[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @method static UserRepository|RepositoryProxy repository() * @method User|Proxy create(array|callable $attributes = []) */ final class UserFactory extends ModelFactory @@ -48,7 +48,7 @@ protected function getDefaults(): array protected function initialize(): self { return $this - ->afterInstantiate(function(User $user) { + ->afterInstantiate(function (User $user) { $user->setPassword($this->passwordHasher->hashPassword($user, $user->getPassword())); }) ; diff --git a/src/Resources/scaffolds/6.0/user/tests/Unit/Entity/UserTest.php b/src/Resources/scaffolds/6.0/user/tests/Unit/Entity/UserTest.php index 91212ffab..a503b269b 100644 --- a/src/Resources/scaffolds/6.0/user/tests/Unit/Entity/UserTest.php +++ b/src/Resources/scaffolds/6.0/user/tests/Unit/Entity/UserTest.php @@ -7,10 +7,7 @@ class UserTest extends TestCase { - /** - * @test - */ - public function always_has_role_user(): void + public function testAlwaysHasRoleUser(): void { $this->assertSame(['ROLE_USER'], (new User())->getRoles()); $this->assertSame(['ROLE_ADMIN', 'ROLE_USER'], (new User())->setRoles(['ROLE_ADMIN'])->getRoles()); From 00195cbace53565d1ea0ed831d119776e8a6039f Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Fri, 8 Apr 2022 12:32:20 -0400 Subject: [PATCH 11/21] flatten directories --- .../src/Controller/{User => }/ChangePasswordController.php | 4 ++-- .../src/Form/{User => }/ChangePasswordFormType.php | 2 +- .../tests/Functional/{User => }/ChangePasswordTest.php | 2 +- .../profile/src/Controller/{User => }/ProfileController.php | 4 ++-- .../6.0/profile/src/Form/{User => }/ProfileFormType.php | 2 +- .../6.0/profile/tests/Functional/{User => }/ProfileTest.php | 2 +- .../register/src/Controller/{User => }/RegisterController.php | 4 ++-- .../6.0/register/src/Form/{User => }/RegistrationFormType.php | 2 +- .../6.0/register/tests/Functional/{User => }/RegisterTest.php | 2 +- .../reset-password/src/Controller/ResetPasswordController.php | 4 ++-- .../src/Form/{ResetPassword => }/RequestResetFormType.php | 2 +- .../src/Form/{ResetPassword => }/ResetPasswordFormType.php | 2 +- 12 files changed, 16 insertions(+), 16 deletions(-) rename src/Resources/scaffolds/6.0/change-password/src/Controller/{User => }/ChangePasswordController.php (95%) rename src/Resources/scaffolds/6.0/change-password/src/Form/{User => }/ChangePasswordFormType.php (98%) rename src/Resources/scaffolds/6.0/change-password/tests/Functional/{User => }/ChangePasswordTest.php (99%) rename src/Resources/scaffolds/6.0/profile/src/Controller/{User => }/ProfileController.php (94%) rename src/Resources/scaffolds/6.0/profile/src/Form/{User => }/ProfileFormType.php (96%) rename src/Resources/scaffolds/6.0/profile/tests/Functional/{User => }/ProfileTest.php (97%) rename src/Resources/scaffolds/6.0/register/src/Controller/{User => }/RegisterController.php (95%) rename src/Resources/scaffolds/6.0/register/src/Form/{User => }/RegistrationFormType.php (98%) rename src/Resources/scaffolds/6.0/register/tests/Functional/{User => }/RegisterTest.php (99%) rename src/Resources/scaffolds/6.0/reset-password/src/Form/{ResetPassword => }/RequestResetFormType.php (96%) rename src/Resources/scaffolds/6.0/reset-password/src/Form/{ResetPassword => }/ResetPasswordFormType.php (98%) diff --git a/src/Resources/scaffolds/6.0/change-password/src/Controller/User/ChangePasswordController.php b/src/Resources/scaffolds/6.0/change-password/src/Controller/ChangePasswordController.php similarity index 95% rename from src/Resources/scaffolds/6.0/change-password/src/Controller/User/ChangePasswordController.php rename to src/Resources/scaffolds/6.0/change-password/src/Controller/ChangePasswordController.php index 4ed825bdb..aa97f683e 100644 --- a/src/Resources/scaffolds/6.0/change-password/src/Controller/User/ChangePasswordController.php +++ b/src/Resources/scaffolds/6.0/change-password/src/Controller/ChangePasswordController.php @@ -1,9 +1,9 @@ Date: Thu, 14 Apr 2022 15:08:37 -0400 Subject: [PATCH 12/21] simplify scaffold tests by using latest zenstruck/browser --- .../6.0/auth/tests/Browser/Authentication.php | 61 ------------------- .../tests/Functional/AuthenticationTest.php | 61 +++++++++++-------- .../tests/Functional/ChangePasswordTest.php | 15 +++-- .../tests/Functional/RegisterTest.php | 19 +++--- .../tests/Functional/ResetPasswordTest.php | 5 +- tests/Maker/MakeScaffoldTest.php | 2 +- 6 files changed, 55 insertions(+), 108 deletions(-) delete mode 100644 src/Resources/scaffolds/6.0/auth/tests/Browser/Authentication.php diff --git a/src/Resources/scaffolds/6.0/auth/tests/Browser/Authentication.php b/src/Resources/scaffolds/6.0/auth/tests/Browser/Authentication.php deleted file mode 100644 index fafe4beb6..000000000 --- a/src/Resources/scaffolds/6.0/auth/tests/Browser/Authentication.php +++ /dev/null @@ -1,61 +0,0 @@ -collector()->isAuthenticated()); - }; - } - - public static function assertAuthenticatedAs(string $email): \Closure - { - return static function (self $auth) use ($email) { - $collector = $auth->collector(); - - Assert::assertTrue($collector->isAuthenticated()); - Assert::assertSame($email, $collector->getUser()); - }; - } - - public static function assertNotAuthenticated(): \Closure - { - return static function (self $auth) { - Assert::assertFalse($auth->collector()->isAuthenticated()); - }; - } - - public static function expireSession(): \Closure - { - return static function (CookieJar $cookies) { - $cookies->expire('MOCKSESSID'); - }; - } - - private function collector(): SecurityDataCollector - { - $browser = $this->browser(); - - \assert($browser instanceof KernelBrowser); - - $collector = $browser - ->withProfiling() - ->visit('/') - ->profile() - ->getCollector('security') - ; - - \assert($collector instanceof SecurityDataCollector); - - return $collector; - } -} diff --git a/src/Resources/scaffolds/6.0/auth/tests/Functional/AuthenticationTest.php b/src/Resources/scaffolds/6.0/auth/tests/Functional/AuthenticationTest.php index 3781f7031..b310997f8 100644 --- a/src/Resources/scaffolds/6.0/auth/tests/Functional/AuthenticationTest.php +++ b/src/Resources/scaffolds/6.0/auth/tests/Functional/AuthenticationTest.php @@ -3,8 +3,8 @@ namespace App\Tests\Functional; use App\Factory\UserFactory; -use App\Tests\Browser\Authentication; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\Component\BrowserKit\CookieJar; use Zenstruck\Browser\Test\HasBrowser; use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; @@ -20,17 +20,17 @@ public function testCanLoginAndLogout(): void UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); $this->browser() - ->use(Authentication::assertNotAuthenticated()) + ->assertNotAuthenticated() ->visit('/login') ->fillField('Email', 'mary@example.com') ->fillField('Password', '1234') ->click('Sign in') ->assertOn('/') ->assertSuccessful() - ->use(Authentication::assertAuthenticatedAs('mary@example.com')) + ->assertAuthenticated('mary@example.com') ->visit('/logout') ->assertOn('/') - ->use(Authentication::assertNotAuthenticated()) + ->assertNotAuthenticated() ; } @@ -39,13 +39,14 @@ public function testLoginWithTarget(): void UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); $this->browser() - ->use(Authentication::assertNotAuthenticated()) + ->assertNotAuthenticated() ->visit('/login?target=/some/page') ->fillField('Email', 'mary@example.com') ->fillField('Password', '1234') ->click('Sign in') ->assertOn('/some/page') - ->use(Authentication::assertAuthenticatedAs('mary@example.com')) + ->visit('/') + ->assertAuthenticated('mary@example.com') ; } @@ -62,7 +63,7 @@ public function testLoginWithInvalidPassword(): void ->assertSuccessful() ->assertFieldEquals('Email', 'mary@example.com') ->assertSee('Invalid credentials.') - ->use(Authentication::assertNotAuthenticated()) + ->assertNotAuthenticated() ; } @@ -77,7 +78,7 @@ public function testLoginWithInvalidEmail(): void ->assertSuccessful() ->assertFieldEquals('Email', 'invalid@example.com') ->assertSee('Invalid credentials.') - ->use(Authentication::assertNotAuthenticated()) + ->assertNotAuthenticated() ; } @@ -86,12 +87,12 @@ public function testLoginWithInvalidCsrf(): void UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); $this->browser() - ->use(Authentication::assertNotAuthenticated()) + ->assertNotAuthenticated() ->post('/login', ['body' => ['email' => 'mary@example.com', 'password' => '1234']]) ->assertOn('/login') ->assertSuccessful() ->assertSee('Invalid CSRF token.') - ->use(Authentication::assertNotAuthenticated()) + ->assertNotAuthenticated() ; } @@ -106,9 +107,13 @@ public function testRememberMeEnabledByDefault(): void ->click('Sign in') ->assertOn('/') ->assertSuccessful() - ->use(Authentication::assertAuthenticatedAs('mary@example.com')) - ->use(Authentication::expireSession()) - ->use(Authentication::assertAuthenticatedAs('mary@example.com')) + ->assertAuthenticated('mary@example.com') + ->use(function(CookieJar $cookieJar) { + $cookieJar->expire('MOCKSESSID'); + }) + ->withProfiling() + ->visit('/') + ->assertAuthenticated('mary@example.com') ; } @@ -124,9 +129,12 @@ public function testCanDisableRememberMe(): void ->click('Sign in') ->assertOn('/') ->assertSuccessful() - ->use(Authentication::assertAuthenticatedAs('mary@example.com')) - ->use(Authentication::expireSession()) - ->use(Authentication::assertNotAuthenticated()) + ->assertAuthenticated('mary@example.com') + ->use(function(CookieJar $cookieJar) { + $cookieJar->expire('MOCKSESSID'); + }) + ->visit('/') + ->assertNotAuthenticated() ; } @@ -140,10 +148,10 @@ public function testFullyAuthenticatedLoginRedirect(): void ->fillField('Password', '1234') ->click('Sign in') ->assertOn('/') - ->use(Authentication::assertAuthenticated()) + ->assertAuthenticated() ->visit('/login') ->assertOn('/') - ->use(Authentication::assertAuthenticated()) + ->assertAuthenticated() ; } @@ -157,10 +165,11 @@ public function testFullyAuthenticatedLoginTarget(): void ->fillField('Password', '1234') ->click('Sign in') ->assertOn('/') - ->use(Authentication::assertAuthenticated()) + ->assertAuthenticated() ->visit('/login?target=/some/page') ->assertOn('/some/page') - ->use(Authentication::assertAuthenticated()) + ->visit('/') + ->assertAuthenticated() ; } @@ -174,14 +183,16 @@ public function testCanFullyAuthenticateIfOnlyRemembered(): void ->fillField('Password', '1234') ->click('Sign in') ->assertOn('/') - ->use(Authentication::assertAuthenticatedAs('mary@example.com')) - ->use(Authentication::expireSession()) + ->assertAuthenticated('mary@example.com') + ->use(function(CookieJar $cookieJar) { + $cookieJar->expire('MOCKSESSID'); + }) ->visit('/login') ->assertOn('/login') ->fillField('Password', '1234') ->click('Sign in') ->assertOn('/') - ->use(Authentication::assertAuthenticatedAs('mary@example.com')) + ->assertAuthenticated('mary@example.com') ; } @@ -196,14 +207,14 @@ public function testLegacyPasswordHashIsAutomaticallyMigratedOnLogin(): void $this->assertSame(\PASSWORD_ARGON2ID, password_get_info($user->getPassword())['algo']); $this->browser() - ->use(Authentication::assertNotAuthenticated()) + ->assertNotAuthenticated() ->visit('/login') ->fillField('Email', 'mary@example.com') ->fillField('Password', '1234') ->click('Sign in') ->assertOn('/') ->assertSuccessful() - ->use(Authentication::assertAuthenticatedAs('mary@example.com')) + ->assertAuthenticated('mary@example.com') ; $this->assertSame(\PASSWORD_DEFAULT, password_get_info($user->getPassword())['algo']); diff --git a/src/Resources/scaffolds/6.0/change-password/tests/Functional/ChangePasswordTest.php b/src/Resources/scaffolds/6.0/change-password/tests/Functional/ChangePasswordTest.php index 87de666a8..ad6b0e797 100644 --- a/src/Resources/scaffolds/6.0/change-password/tests/Functional/ChangePasswordTest.php +++ b/src/Resources/scaffolds/6.0/change-password/tests/Functional/ChangePasswordTest.php @@ -3,7 +3,6 @@ namespace App\Tests\Functional; use App\Factory\UserFactory; -use App\Tests\Browser\Authentication; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Browser\Test\HasBrowser; use Zenstruck\Foundry\Test\Factories; @@ -33,7 +32,7 @@ public function testCanChangePassword(): void ->assertSuccessful() ->assertOn('/') ->assertSeeIn('.alert', 'You\'ve successfully changed your password.') - ->use(Authentication::assertAuthenticatedAs('mary@example.com')) + ->assertAuthenticated('mary@example.com') ->visit('/logout') ->visit('/login') ->fillField('Email', 'mary@example.com') @@ -41,7 +40,7 @@ public function testCanChangePassword(): void ->click('Sign in') ->assertOn('/') ->assertSuccessful() - ->use(Authentication::assertAuthenticatedAs('mary@example.com')) + ->assertAuthenticated('mary@example.com') ; $this->assertNotSame($currentPassword, $user->getPassword()); @@ -62,7 +61,7 @@ public function testCurrentPasswordMustBeCorrect(): void ->assertSuccessful() ->assertOn('/user/change-password') ->assertSee('This is not your current password.') - ->use(Authentication::assertAuthenticatedAs('mary@example.com')) + ->assertAuthenticated('mary@example.com') ; $this->assertSame($currentPassword, $user->getPassword()); @@ -82,7 +81,7 @@ public function testCurrentPasswordIsRequired(): void ->assertSuccessful() ->assertOn('/user/change-password') ->assertSee('This is not your current password.') - ->use(Authentication::assertAuthenticatedAs('mary@example.com')) + ->assertAuthenticated('mary@example.com') ; $this->assertSame($currentPassword, $user->getPassword()); @@ -101,7 +100,7 @@ public function testNewPasswordIsRequired(): void ->assertSuccessful() ->assertOn('/user/change-password') ->assertSee('Please enter a password.') - ->use(Authentication::assertAuthenticatedAs('mary@example.com')) + ->assertAuthenticated('mary@example.com') ; $this->assertSame($currentPassword, $user->getPassword()); @@ -122,7 +121,7 @@ public function testNewPasswordsMustMatch(): void ->assertSuccessful() ->assertOn('/user/change-password') ->assertSee('The password fields must match.') - ->use(Authentication::assertAuthenticatedAs('mary@example.com')) + ->assertAuthenticated('mary@example.com') ; $this->assertSame($currentPassword, $user->getPassword()); @@ -143,7 +142,7 @@ public function testNewPasswordMustBeMinLength(): void ->assertSuccessful() ->assertOn('/user/change-password') ->assertSee('Your password should be at least 6 characters') - ->use(Authentication::assertAuthenticatedAs('mary@example.com')) + ->assertAuthenticated('mary@example.com') ; $this->assertSame($currentPassword, $user->getPassword()); diff --git a/src/Resources/scaffolds/6.0/register/tests/Functional/RegisterTest.php b/src/Resources/scaffolds/6.0/register/tests/Functional/RegisterTest.php index b59d244b0..0f1a8c4ee 100644 --- a/src/Resources/scaffolds/6.0/register/tests/Functional/RegisterTest.php +++ b/src/Resources/scaffolds/6.0/register/tests/Functional/RegisterTest.php @@ -3,7 +3,6 @@ namespace App\Tests\Functional; use App\Factory\UserFactory; -use App\Tests\Browser\Authentication; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Browser\Test\HasBrowser; use Zenstruck\Foundry\Test\Factories; @@ -28,15 +27,15 @@ public function testCanRegister(): void ->click('Register') ->assertOn('/') ->assertSeeIn('.alert', 'You\'ve successfully registered and are now logged in.') - ->use(Authentication::assertAuthenticatedAs('madison@example.com')) + ->assertAuthenticated('madison@example.com') ->visit('/logout') - ->use(Authentication::assertNotAuthenticated()) + ->assertNotAuthenticated() ->visit('/login') ->fillField('Email', 'madison@example.com') ->fillField('Password', 'password') ->click('Sign in') ->assertOn('/') - ->use(Authentication::assertAuthenticatedAs('madison@example.com')) + ->assertAuthenticated('madison@example.com') ; UserFactory::assert()->count(1); @@ -54,7 +53,7 @@ public function testNameIsRequired(): void ->click('Register') ->assertOn('/register') ->assertSee('Name is required') - ->use(Authentication::assertNotAuthenticated()) + ->assertNotAuthenticated() ; } @@ -69,7 +68,7 @@ public function testEmailIsRequired(): void ->click('Register') ->assertOn('/register') ->assertSee('Email is required') - ->use(Authentication::assertNotAuthenticated()) + ->assertNotAuthenticated() ; } @@ -85,7 +84,7 @@ public function testEmailMustBeEmailAddress(): void ->click('Register') ->assertOn('/register') ->assertSee('This is not a valid email address') - ->use(Authentication::assertNotAuthenticated()) + ->assertNotAuthenticated() ; } @@ -103,7 +102,7 @@ public function testEmailMustBeUnique(): void ->click('Register') ->assertOn('/register') ->assertSee('There is already an account with this email') - ->use(Authentication::assertNotAuthenticated()) + ->assertNotAuthenticated() ; } @@ -118,7 +117,7 @@ public function testPasswordIsRequired(): void ->click('Register') ->assertOn('/register') ->assertSee('Please enter a password') - ->use(Authentication::assertNotAuthenticated()) + ->assertNotAuthenticated() ; } @@ -134,7 +133,7 @@ public function testPasswordMustBeMinLength(): void ->click('Register') ->assertOn('/register') ->assertSee('Your password should be at least 6 characters') - ->use(Authentication::assertNotAuthenticated()) + ->assertNotAuthenticated() ; } } diff --git a/src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php b/src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php index a03bb2500..977e91482 100644 --- a/src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php +++ b/src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php @@ -4,7 +4,6 @@ use App\Factory\ResetPasswordRequestFactory; use App\Factory\UserFactory; -use App\Tests\Browser\Authentication; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Browser\Test\HasBrowser; use Zenstruck\Foundry\Test\Factories; @@ -58,14 +57,14 @@ public function testCanResetPassword(): void ->click('Reset password') ->assertOn('/') ->assertSeeIn('.alert', 'Your password was successfully reset, you are now logged in.') - ->use(Authentication::assertAuthenticatedAs('john@example.com')) + ->assertAuthenticated('john@example.com') ->visit('/logout') ->visit('/login') ->fillField('Email', 'john@example.com') ->fillField('Password', 'new-password') ->click('Sign in') ->assertOn('/') - ->use(Authentication::assertAuthenticatedAs('john@example.com')) + ->assertAuthenticated('john@example.com') ; } diff --git a/tests/Maker/MakeScaffoldTest.php b/tests/Maker/MakeScaffoldTest.php index 2a7aad33d..c8b5df229 100644 --- a/tests/Maker/MakeScaffoldTest.php +++ b/tests/Maker/MakeScaffoldTest.php @@ -23,7 +23,7 @@ protected function setUp(): void { parent::setUp(); - if (Kernel::MAJOR_VERSION < 6) { + if (Kernel::VERSION_ID < 60000) { $this->markTestSkipped('Only available on Symfony 6+.'); } } From 9bc88b23f65d4adab12294468c02325940f00409 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Thu, 14 Apr 2022 19:54:35 -0400 Subject: [PATCH 13/21] prune # of tests (slightly) --- .../tests/Functional/ChangePasswordTest.php | 21 -- .../profile/tests/Functional/ProfileTest.php | 2 +- .../tests/Functional/RegisterTest.php | 52 +--- .../Factory/ResetPasswordRequestFactory.php | 40 --- .../tests/Functional/ResetPasswordTest.php | 278 ------------------ 5 files changed, 4 insertions(+), 389 deletions(-) delete mode 100644 src/Resources/scaffolds/6.0/reset-password/src/Factory/ResetPasswordRequestFactory.php diff --git a/src/Resources/scaffolds/6.0/change-password/tests/Functional/ChangePasswordTest.php b/src/Resources/scaffolds/6.0/change-password/tests/Functional/ChangePasswordTest.php index ad6b0e797..9d1d0612d 100644 --- a/src/Resources/scaffolds/6.0/change-password/tests/Functional/ChangePasswordTest.php +++ b/src/Resources/scaffolds/6.0/change-password/tests/Functional/ChangePasswordTest.php @@ -127,27 +127,6 @@ public function testNewPasswordsMustMatch(): void $this->assertSame($currentPassword, $user->getPassword()); } - public function testNewPasswordMustBeMinLength(): void - { - $user = UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); - $currentPassword = $user->getPassword(); - - $this->browser() - ->actingAs($user->object()) - ->visit('/user/change-password') - ->fillField('Current Password', '1234') - ->fillField('New Password', '4321') - ->fillField('Repeat New Password', '4321') - ->click('Change Password') - ->assertSuccessful() - ->assertOn('/user/change-password') - ->assertSee('Your password should be at least 6 characters') - ->assertAuthenticated('mary@example.com') - ; - - $this->assertSame($currentPassword, $user->getPassword()); - } - public function testCannotAccessChangePasswordPageIfNotLoggedIn(): void { $this->browser() diff --git a/src/Resources/scaffolds/6.0/profile/tests/Functional/ProfileTest.php b/src/Resources/scaffolds/6.0/profile/tests/Functional/ProfileTest.php index 10fdd95f5..84f17b3a6 100644 --- a/src/Resources/scaffolds/6.0/profile/tests/Functional/ProfileTest.php +++ b/src/Resources/scaffolds/6.0/profile/tests/Functional/ProfileTest.php @@ -37,7 +37,7 @@ public function testCanUpdateProfile(): void $this->assertSame('John Smith', $user->getName()); } - public function testNameIsRequired(): void + public function testValidation(): void { $user = UserFactory::createOne(['name' => 'Mary Edwards']); diff --git a/src/Resources/scaffolds/6.0/register/tests/Functional/RegisterTest.php b/src/Resources/scaffolds/6.0/register/tests/Functional/RegisterTest.php index 0f1a8c4ee..a3c872b80 100644 --- a/src/Resources/scaffolds/6.0/register/tests/Functional/RegisterTest.php +++ b/src/Resources/scaffolds/6.0/register/tests/Functional/RegisterTest.php @@ -42,32 +42,17 @@ public function testCanRegister(): void UserFactory::assert()->exists(['name' => 'Madison', 'email' => 'madison@example.com']); } - public function testNameIsRequired(): void + public function testValidation(): void { $this->browser() ->throwExceptions() ->visit('/register') ->assertSuccessful() - ->fillField('Email', 'madison@example.com') - ->fillField('Password', 'password') - ->click('Register') - ->assertOn('/register') - ->assertSee('Name is required') - ->assertNotAuthenticated() - ; - } - - public function testEmailIsRequired(): void - { - $this->browser() - ->throwExceptions() - ->visit('/register') - ->assertSuccessful() - ->fillField('Name', 'Madison') - ->fillField('Password', 'password') ->click('Register') ->assertOn('/register') ->assertSee('Email is required') + ->assertSee('Please enter a password') + ->assertSee('Name is required') ->assertNotAuthenticated() ; } @@ -105,35 +90,4 @@ public function testEmailMustBeUnique(): void ->assertNotAuthenticated() ; } - - public function testPasswordIsRequired(): void - { - $this->browser() - ->throwExceptions() - ->visit('/register') - ->assertSuccessful() - ->fillField('Name', 'Madison') - ->fillField('Email', 'madison@example.com') - ->click('Register') - ->assertOn('/register') - ->assertSee('Please enter a password') - ->assertNotAuthenticated() - ; - } - - public function testPasswordMustBeMinLength(): void - { - $this->browser() - ->throwExceptions() - ->visit('/register') - ->assertSuccessful() - ->fillField('Name', 'Madison') - ->fillField('Email', 'madison@example.com') - ->fillField('Password', '1234') - ->click('Register') - ->assertOn('/register') - ->assertSee('Your password should be at least 6 characters') - ->assertNotAuthenticated() - ; - } } diff --git a/src/Resources/scaffolds/6.0/reset-password/src/Factory/ResetPasswordRequestFactory.php b/src/Resources/scaffolds/6.0/reset-password/src/Factory/ResetPasswordRequestFactory.php deleted file mode 100644 index d95d4425f..000000000 --- a/src/Resources/scaffolds/6.0/reset-password/src/Factory/ResetPasswordRequestFactory.php +++ /dev/null @@ -1,40 +0,0 @@ - - * - * @method static ResetPasswordRequest|Proxy createOne(array $attributes = []) - * @method static ResetPasswordRequest[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static ResetPasswordRequest|Proxy find(object|array|mixed $criteria) - * @method static ResetPasswordRequest|Proxy findOrCreate(array $attributes) - * @method static ResetPasswordRequest|Proxy first(string $sortedField = 'id') - * @method static ResetPasswordRequest|Proxy last(string $sortedField = 'id') - * @method static ResetPasswordRequest|Proxy random(array $attributes = []) - * @method static ResetPasswordRequest|Proxy randomOrCreate(array $attributes = []) - * @method static ResetPasswordRequest[]|Proxy[] all() - * @method static ResetPasswordRequest[]|Proxy[] findBy(array $attributes) - * @method static ResetPasswordRequest[]|Proxy[] randomSet(int $number, array $attributes = []) - * @method static ResetPasswordRequest[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) - * @method static ResetPasswordRequestRepository|RepositoryProxy repository() - * @method ResetPasswordRequest|Proxy create(array|callable $attributes = []) - */ -final class ResetPasswordRequestFactory extends ModelFactory -{ - protected function getDefaults(): array - { - return []; - } - - protected static function getClass(): string - { - return ResetPasswordRequest::class; - } -} diff --git a/src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php b/src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php index 977e91482..7cd605523 100644 --- a/src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php +++ b/src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php @@ -2,7 +2,6 @@ namespace App\Tests\Functional; -use App\Factory\ResetPasswordRequestFactory; use App\Factory\UserFactory; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Browser\Test\HasBrowser; @@ -67,281 +66,4 @@ public function testCanResetPassword(): void ->assertAuthenticated('john@example.com') ; } - - public function testRequestEmailIsRequired(): void - { - $this->browser() - ->visit('/reset-password') - ->click('Send password reset email') - ->assertOn('/reset-password') - ->assertSee('Please enter your email') - ; - - $this->mailer()->assertNoEmailSent(); - } - - public function testRequestEmailMustBeAnEmail(): void - { - $this->browser() - ->visit('/reset-password') - ->fillField('Email', 'invalid') - ->click('Send password reset email') - ->assertOn('/reset-password') - ->assertSee('This is not a valid email address') - ; - - $this->mailer()->assertNoEmailSent(); - } - - public function testRequestsAreThrottled(): void - { - UserFactory::createOne(['email' => 'john@example.com']); - - $this->browser() - ->visit('/reset-password') - ->fillField('Email', 'john@example.com') - ->click('Send password reset email') - ->assertOn('/reset-password/check-email') - ->assertSuccessful() - ->assertSee('Password Reset Email Sent') - ->visit('/reset-password') - ->fillField('Email', 'john@example.com') - ->click('Send password reset email') - ->assertOn('/') - ->assertSeeIn('.alert', 'You have already requested a reset password email. Please check your email or try again soon.') - ; - - $this->mailer()->assertSentEmailCount(1); - - ResetPasswordRequestFactory::assert()->count(1); - } - - public function testCanRequestAgainAfterThrottleExpires(): void - { - UserFactory::createOne(['email' => 'john@example.com']); - - $this->browser() - ->visit('/reset-password') - ->fillField('Email', 'john@example.com') - ->click('Send password reset email') - ->assertOn('/reset-password/check-email') - ->assertSuccessful() - ->assertSee('Password Reset Email Sent') - ->use(function () { - ResetPasswordRequestFactory::first() - ->forceSet('requestedAt', new \DateTimeImmutable('-16 minutes')) - ->save() - ; - }) - ->visit('/reset-password') - ->fillField('Email', 'john@example.com') - ->click('Send password reset email') - ->assertOn('/reset-password/check-email') - ->assertSuccessful() - ->assertSee('Password Reset Email Sent') - ; - - $this->mailer()->assertSentEmailCount(2); - - ResetPasswordRequestFactory::assert()->count(2); - } - - public function testRequestDoesNotExposeIfUserWasNotFound(): void - { - $this->browser() - ->visit('/reset-password') - ->fillField('Email', 'john@example.com') - ->click('Send password reset email') - ->assertOn('/reset-password/check-email') - ->assertSuccessful() - ->assertSee('Password Reset Email Sent') - ; - - $this->mailer()->assertNoEmailSent(); - } - - public function testResetPasswordIsRequired(): void - { - $user = UserFactory::createOne(['email' => 'john@example.com']); - $currentPassword = $user->getPassword(); - - $this->browser() - ->visit('/reset-password') - ->assertSuccessful() - ->fillField('Email', 'john@example.com') - ->click('Send password reset email') - ->visit($this->mailer()->sentEmails()->first()->getHeaders()->get('X-CTA')->getBody()) - ->click('Reset password') - ->assertOn('/reset-password/reset') - ->assertSee('Please enter a password') - ; - - $this->assertSame($currentPassword, UserFactory::find(['email' => 'john@example.com'])->getPassword()); - } - - public function testResetPasswordsMustMatch(): void - { - $user = UserFactory::createOne(['email' => 'john@example.com']); - $currentPassword = $user->getPassword(); - - $this->browser() - ->visit('/reset-password') - ->assertSuccessful() - ->fillField('Email', 'john@example.com') - ->click('Send password reset email') - ->visit($this->mailer()->sentEmails()->first()->getHeaders()->get('X-CTA')->getBody()) - ->fillField('New password', 'new-password') - ->fillField('Repeat Password', 'mismatch-password') - ->click('Reset password') - ->assertOn('/reset-password/reset') - ->assertSee('The password fields must match.') - ; - - $this->assertSame($currentPassword, UserFactory::find(['email' => 'john@example.com'])->getPassword()); - } - - public function testResetPasswordMustBeMinLength(): void - { - $user = UserFactory::createOne(['email' => 'john@example.com']); - $currentPassword = $user->getPassword(); - - $this->browser() - ->visit('/reset-password') - ->assertSuccessful() - ->fillField('Email', 'john@example.com') - ->click('Send password reset email') - ->visit($this->mailer()->sentEmails()->first()->getHeaders()->get('X-CTA')->getBody()) - ->fillField('New password', '1234') - ->fillField('Repeat Password', '1234') - ->click('Reset password') - ->assertOn('/reset-password/reset') - ->assertSee('Your password should be at least 6 characters') - ; - - $this->assertSame($currentPassword, UserFactory::find(['email' => 'john@example.com'])->getPassword()); - } - - public function testCannotResetWithInvalidToken(): void - { - $this->browser() - ->visit('/reset-password/reset/invalid-token') - ->assertOn('/') - ->assertSeeIn('.alert', 'The reset password link is invalid. Please try to reset your password again.') - ; - } - - public function testCanUseOldTokenEvenAfterRequestingAnother(): void - { - UserFactory::createOne(['email' => 'john@example.com']); - - $this->browser() - ->visit('/reset-password') - ->fillField('Email', 'john@example.com') - ->click('Send password reset email') - ->assertOn('/reset-password/check-email') - ->assertSuccessful() - ->assertSee('Password Reset Email Sent') - ->use(function () { - ResetPasswordRequestFactory::first() - ->forceSet('requestedAt', new \DateTimeImmutable('-16 minutes')) - ->save() - ; - }) - ->visit('/reset-password') - ->fillField('Email', 'john@example.com') - ->click('Send password reset email') - ->assertOn('/reset-password/check-email') - ->assertSuccessful() - ->assertSee('Password Reset Email Sent') - ->use(function () { - ResetPasswordRequestFactory::assert()->count(2); - }) - ->visit($this->mailer()->sentEmails()->first()->getHeaders()->get('X-CTA')->getBody()) - ->assertOn('/reset-password/reset') - ->fillField('New password', 'new-password') - ->fillField('Repeat Password', 'new-password') - ->click('Reset password') - ->assertOn('/') - ; - - ResetPasswordRequestFactory::assert()->empty(); - } - - public function testResetTokensExpire(): void - { - UserFactory::createOne(['email' => 'john@example.com']); - - $this->browser() - ->visit('/reset-password') - ->fillField('Email', 'john@example.com') - ->click('Send password reset email') - ->assertOn('/reset-password/check-email') - ->assertSuccessful() - ->assertSee('Password Reset Email Sent') - ->use(function () { - ResetPasswordRequestFactory::first() - ->forceSet('expiresAt', new \DateTimeImmutable('-10 minutes')) - ->save() - ; - }) - ->visit($this->mailer()->sentEmails()->first()->getHeaders()->get('X-CTA')->getBody()) - ->assertOn('/') - ->assertSuccessful() - ->assertSeeIn('.alert', 'The link in your email is expired. Please try to reset your password again.') - ; - } - - public function testCannotUseTokenAfterPasswordChange(): void - { - UserFactory::createOne(['email' => 'john@example.com']); - - $this->browser() - ->visit('/reset-password') - ->assertSuccessful() - ->fillField('Email', 'john@example.com') - ->click('Send password reset email') - ->assertOn('/reset-password/check-email') - ->visit($resetUrl = $this->mailer()->sentEmails()->first()->getHeaders()->get('X-CTA')->getBody()) - ->fillField('New password', 'new-password') - ->fillField('Repeat Password', 'new-password') - ->click('Reset password') - ->assertOn('/') - ->assertSeeIn('.alert', 'Your password was successfully reset, you are now logged in.') - ->visit('/logout') - ->visit($resetUrl) - ->assertOn('/') - ->assertSuccessful() - ->assertSeeIn('.alert', 'The reset password link is invalid. Please try to reset your password again.') - ; - } - - public function testOldTokensAreGarbageCollected(): void - { - $user = UserFactory::createOne(['email' => 'jane@example.com']); - - ResetPasswordRequestFactory::createOne([ - 'user' => $user, - 'selector' => 'selector', - 'hashedToken' => 'hash', - 'expiresAt' => new \DateTimeImmutable('-1 month'), - ]) - ->forceSet('requestedAt', new \DateTimeImmutable('-1 month')) - ->save() - ; - - ResetPasswordRequestFactory::assert()->exists(['selector' => 'selector']); - - $this->browser() - ->visit('/reset-password') - ->assertSuccessful() - ->fillField('Email', 'jane@example.com') - ->click('Send password reset email') - ->assertOn('/reset-password/check-email') - ; - - ResetPasswordRequestFactory::assert() - ->count(1) - ->notExists(['selector' => 'selector']) - ; - } } From c9d17da73f640447956aed7d1411ac2849e87fdf Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Thu, 14 Apr 2022 20:20:52 -0400 Subject: [PATCH 14/21] convert scaffold files to .tpl --- src/Maker/MakeScaffold.php | 8 ++++++-- ...{SecurityController.php => SecurityController.php.tpl} | 0 .../src/Security/{LoginUser.php => LoginUser.php.tpl} | 0 .../templates/{login.html.twig => login.html.twig.tpl} | 0 ...{AuthenticationTest.php => AuthenticationTest.php.tpl} | 0 ...ordController.php => ChangePasswordController.php.tpl} | 0 ...asswordFormType.php => ChangePasswordFormType.php.tpl} | 0 ...e_password.html.twig => change_password.html.twig.tpl} | 0 ...{ChangePasswordTest.php => ChangePasswordTest.php.tpl} | 0 ...{HomepageController.php => HomepageController.php.tpl} | 0 .../templates/{index.html.twig => index.html.twig.tpl} | 0 .../Functional/{HomepageTest.php => HomepageTest.php.tpl} | 0 .../{ProfileController.php => ProfileController.php.tpl} | 0 .../Form/{ProfileFormType.php => ProfileFormType.php.tpl} | 0 .../user/{profile.html.twig => profile.html.twig.tpl} | 0 .../Functional/{ProfileTest.php => ProfileTest.php.tpl} | 0 ...{RegisterController.php => RegisterController.php.tpl} | 0 ...istrationFormType.php => RegistrationFormType.php.tpl} | 0 .../user/{register.html.twig => register.html.twig.tpl} | 0 .../Functional/{RegisterTest.php => RegisterTest.php.tpl} | 0 src/Resources/scaffolds/6.0/reset-password.php | 5 ----- .../{reset_password.yaml => reset_password.yaml.tpl} | 0 ...wordController.php => ResetPasswordController.php.tpl} | 0 ...etPasswordRequest.php => ResetPasswordRequest.php.tpl} | 0 ...uestResetFormType.php => RequestResetFormType.php.tpl} | 0 ...PasswordFormType.php => ResetPasswordFormType.php.tpl} | 0 ...ository.php => ResetPasswordRequestRepository.php.tpl} | 0 .../{check_email.html.twig => check_email.html.twig.tpl} | 0 .../{email.html.twig => email.html.twig.tpl} | 0 .../{request.html.twig => request.html.twig.tpl} | 0 .../{reset.html.twig => reset.html.twig.tpl} | 0 .../{ResetPasswordTest.php => ResetPasswordTest.php.tpl} | 0 src/Resources/scaffolds/6.0/starter-kit.php | 5 ----- .../templates/{base.html.twig => base.html.twig.tpl} | 0 .../6.0/user/src/Entity/{User.php => User.php.tpl} | 0 .../src/Factory/{UserFactory.php => UserFactory.php.tpl} | 0 .../{UserRepository.php => UserRepository.php.tpl} | 0 .../tests/Unit/Entity/{UserTest.php => UserTest.php.tpl} | 0 38 files changed, 6 insertions(+), 12 deletions(-) rename src/Resources/scaffolds/6.0/auth/src/Controller/{SecurityController.php => SecurityController.php.tpl} (100%) rename src/Resources/scaffolds/6.0/auth/src/Security/{LoginUser.php => LoginUser.php.tpl} (100%) rename src/Resources/scaffolds/6.0/auth/templates/{login.html.twig => login.html.twig.tpl} (100%) rename src/Resources/scaffolds/6.0/auth/tests/Functional/{AuthenticationTest.php => AuthenticationTest.php.tpl} (100%) rename src/Resources/scaffolds/6.0/change-password/src/Controller/{ChangePasswordController.php => ChangePasswordController.php.tpl} (100%) rename src/Resources/scaffolds/6.0/change-password/src/Form/{ChangePasswordFormType.php => ChangePasswordFormType.php.tpl} (100%) rename src/Resources/scaffolds/6.0/change-password/templates/user/{change_password.html.twig => change_password.html.twig.tpl} (100%) rename src/Resources/scaffolds/6.0/change-password/tests/Functional/{ChangePasswordTest.php => ChangePasswordTest.php.tpl} (100%) rename src/Resources/scaffolds/6.0/homepage/src/Controller/{HomepageController.php => HomepageController.php.tpl} (100%) rename src/Resources/scaffolds/6.0/homepage/templates/{index.html.twig => index.html.twig.tpl} (100%) rename src/Resources/scaffolds/6.0/homepage/tests/Functional/{HomepageTest.php => HomepageTest.php.tpl} (100%) rename src/Resources/scaffolds/6.0/profile/src/Controller/{ProfileController.php => ProfileController.php.tpl} (100%) rename src/Resources/scaffolds/6.0/profile/src/Form/{ProfileFormType.php => ProfileFormType.php.tpl} (100%) rename src/Resources/scaffolds/6.0/profile/templates/user/{profile.html.twig => profile.html.twig.tpl} (100%) rename src/Resources/scaffolds/6.0/profile/tests/Functional/{ProfileTest.php => ProfileTest.php.tpl} (100%) rename src/Resources/scaffolds/6.0/register/src/Controller/{RegisterController.php => RegisterController.php.tpl} (100%) rename src/Resources/scaffolds/6.0/register/src/Form/{RegistrationFormType.php => RegistrationFormType.php.tpl} (100%) rename src/Resources/scaffolds/6.0/register/templates/user/{register.html.twig => register.html.twig.tpl} (100%) rename src/Resources/scaffolds/6.0/register/tests/Functional/{RegisterTest.php => RegisterTest.php.tpl} (100%) rename src/Resources/scaffolds/6.0/reset-password/config/packages/{reset_password.yaml => reset_password.yaml.tpl} (100%) rename src/Resources/scaffolds/6.0/reset-password/src/Controller/{ResetPasswordController.php => ResetPasswordController.php.tpl} (100%) rename src/Resources/scaffolds/6.0/reset-password/src/Entity/{ResetPasswordRequest.php => ResetPasswordRequest.php.tpl} (100%) rename src/Resources/scaffolds/6.0/reset-password/src/Form/{RequestResetFormType.php => RequestResetFormType.php.tpl} (100%) rename src/Resources/scaffolds/6.0/reset-password/src/Form/{ResetPasswordFormType.php => ResetPasswordFormType.php.tpl} (100%) rename src/Resources/scaffolds/6.0/reset-password/src/Repository/{ResetPasswordRequestRepository.php => ResetPasswordRequestRepository.php.tpl} (100%) rename src/Resources/scaffolds/6.0/reset-password/templates/reset_password/{check_email.html.twig => check_email.html.twig.tpl} (100%) rename src/Resources/scaffolds/6.0/reset-password/templates/reset_password/{email.html.twig => email.html.twig.tpl} (100%) rename src/Resources/scaffolds/6.0/reset-password/templates/reset_password/{request.html.twig => request.html.twig.tpl} (100%) rename src/Resources/scaffolds/6.0/reset-password/templates/reset_password/{reset.html.twig => reset.html.twig.tpl} (100%) rename src/Resources/scaffolds/6.0/reset-password/tests/Functional/{ResetPasswordTest.php => ResetPasswordTest.php.tpl} (100%) rename src/Resources/scaffolds/6.0/starter-kit/templates/{base.html.twig => base.html.twig.tpl} (100%) rename src/Resources/scaffolds/6.0/user/src/Entity/{User.php => User.php.tpl} (100%) rename src/Resources/scaffolds/6.0/user/src/Factory/{UserFactory.php => UserFactory.php.tpl} (100%) rename src/Resources/scaffolds/6.0/user/src/Repository/{UserRepository.php => UserRepository.php.tpl} (100%) rename src/Resources/scaffolds/6.0/user/tests/Unit/Entity/{UserTest.php => UserTest.php.tpl} (100%) diff --git a/src/Maker/MakeScaffold.php b/src/Maker/MakeScaffold.php index 454b9e0dd..12e79b619 100644 --- a/src/Maker/MakeScaffold.php +++ b/src/Maker/MakeScaffold.php @@ -21,7 +21,6 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Finder\Finder; use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Process\ExecutableFinder; @@ -159,7 +158,12 @@ private function generateScaffold(string $name, ConsoleStyle $io): void if (is_dir($scaffold['dir'])) { $io->text('Copying scaffold files...'); - (new Filesystem())->mirror($scaffold['dir'], $this->files->getRootDirectory()); + foreach (Finder::create()->files()->in($scaffold['dir']) as $file) { + $this->files->dumpFile( + "{$file->getRelativePath()}/{$file->getFilenameWithoutExtension()}", + $file->getContents() + ); + } } if (isset($scaffold['configure'])) { diff --git a/src/Resources/scaffolds/6.0/auth/src/Controller/SecurityController.php b/src/Resources/scaffolds/6.0/auth/src/Controller/SecurityController.php.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/auth/src/Controller/SecurityController.php rename to src/Resources/scaffolds/6.0/auth/src/Controller/SecurityController.php.tpl diff --git a/src/Resources/scaffolds/6.0/auth/src/Security/LoginUser.php b/src/Resources/scaffolds/6.0/auth/src/Security/LoginUser.php.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/auth/src/Security/LoginUser.php rename to src/Resources/scaffolds/6.0/auth/src/Security/LoginUser.php.tpl diff --git a/src/Resources/scaffolds/6.0/auth/templates/login.html.twig b/src/Resources/scaffolds/6.0/auth/templates/login.html.twig.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/auth/templates/login.html.twig rename to src/Resources/scaffolds/6.0/auth/templates/login.html.twig.tpl diff --git a/src/Resources/scaffolds/6.0/auth/tests/Functional/AuthenticationTest.php b/src/Resources/scaffolds/6.0/auth/tests/Functional/AuthenticationTest.php.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/auth/tests/Functional/AuthenticationTest.php rename to src/Resources/scaffolds/6.0/auth/tests/Functional/AuthenticationTest.php.tpl diff --git a/src/Resources/scaffolds/6.0/change-password/src/Controller/ChangePasswordController.php b/src/Resources/scaffolds/6.0/change-password/src/Controller/ChangePasswordController.php.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/change-password/src/Controller/ChangePasswordController.php rename to src/Resources/scaffolds/6.0/change-password/src/Controller/ChangePasswordController.php.tpl diff --git a/src/Resources/scaffolds/6.0/change-password/src/Form/ChangePasswordFormType.php b/src/Resources/scaffolds/6.0/change-password/src/Form/ChangePasswordFormType.php.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/change-password/src/Form/ChangePasswordFormType.php rename to src/Resources/scaffolds/6.0/change-password/src/Form/ChangePasswordFormType.php.tpl diff --git a/src/Resources/scaffolds/6.0/change-password/templates/user/change_password.html.twig b/src/Resources/scaffolds/6.0/change-password/templates/user/change_password.html.twig.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/change-password/templates/user/change_password.html.twig rename to src/Resources/scaffolds/6.0/change-password/templates/user/change_password.html.twig.tpl diff --git a/src/Resources/scaffolds/6.0/change-password/tests/Functional/ChangePasswordTest.php b/src/Resources/scaffolds/6.0/change-password/tests/Functional/ChangePasswordTest.php.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/change-password/tests/Functional/ChangePasswordTest.php rename to src/Resources/scaffolds/6.0/change-password/tests/Functional/ChangePasswordTest.php.tpl diff --git a/src/Resources/scaffolds/6.0/homepage/src/Controller/HomepageController.php b/src/Resources/scaffolds/6.0/homepage/src/Controller/HomepageController.php.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/homepage/src/Controller/HomepageController.php rename to src/Resources/scaffolds/6.0/homepage/src/Controller/HomepageController.php.tpl diff --git a/src/Resources/scaffolds/6.0/homepage/templates/index.html.twig b/src/Resources/scaffolds/6.0/homepage/templates/index.html.twig.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/homepage/templates/index.html.twig rename to src/Resources/scaffolds/6.0/homepage/templates/index.html.twig.tpl diff --git a/src/Resources/scaffolds/6.0/homepage/tests/Functional/HomepageTest.php b/src/Resources/scaffolds/6.0/homepage/tests/Functional/HomepageTest.php.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/homepage/tests/Functional/HomepageTest.php rename to src/Resources/scaffolds/6.0/homepage/tests/Functional/HomepageTest.php.tpl diff --git a/src/Resources/scaffolds/6.0/profile/src/Controller/ProfileController.php b/src/Resources/scaffolds/6.0/profile/src/Controller/ProfileController.php.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/profile/src/Controller/ProfileController.php rename to src/Resources/scaffolds/6.0/profile/src/Controller/ProfileController.php.tpl diff --git a/src/Resources/scaffolds/6.0/profile/src/Form/ProfileFormType.php b/src/Resources/scaffolds/6.0/profile/src/Form/ProfileFormType.php.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/profile/src/Form/ProfileFormType.php rename to src/Resources/scaffolds/6.0/profile/src/Form/ProfileFormType.php.tpl diff --git a/src/Resources/scaffolds/6.0/profile/templates/user/profile.html.twig b/src/Resources/scaffolds/6.0/profile/templates/user/profile.html.twig.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/profile/templates/user/profile.html.twig rename to src/Resources/scaffolds/6.0/profile/templates/user/profile.html.twig.tpl diff --git a/src/Resources/scaffolds/6.0/profile/tests/Functional/ProfileTest.php b/src/Resources/scaffolds/6.0/profile/tests/Functional/ProfileTest.php.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/profile/tests/Functional/ProfileTest.php rename to src/Resources/scaffolds/6.0/profile/tests/Functional/ProfileTest.php.tpl diff --git a/src/Resources/scaffolds/6.0/register/src/Controller/RegisterController.php b/src/Resources/scaffolds/6.0/register/src/Controller/RegisterController.php.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/register/src/Controller/RegisterController.php rename to src/Resources/scaffolds/6.0/register/src/Controller/RegisterController.php.tpl diff --git a/src/Resources/scaffolds/6.0/register/src/Form/RegistrationFormType.php b/src/Resources/scaffolds/6.0/register/src/Form/RegistrationFormType.php.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/register/src/Form/RegistrationFormType.php rename to src/Resources/scaffolds/6.0/register/src/Form/RegistrationFormType.php.tpl diff --git a/src/Resources/scaffolds/6.0/register/templates/user/register.html.twig b/src/Resources/scaffolds/6.0/register/templates/user/register.html.twig.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/register/templates/user/register.html.twig rename to src/Resources/scaffolds/6.0/register/templates/user/register.html.twig.tpl diff --git a/src/Resources/scaffolds/6.0/register/tests/Functional/RegisterTest.php b/src/Resources/scaffolds/6.0/register/tests/Functional/RegisterTest.php.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/register/tests/Functional/RegisterTest.php rename to src/Resources/scaffolds/6.0/register/tests/Functional/RegisterTest.php.tpl diff --git a/src/Resources/scaffolds/6.0/reset-password.php b/src/Resources/scaffolds/6.0/reset-password.php index 39559b691..4143f14be 100644 --- a/src/Resources/scaffolds/6.0/reset-password.php +++ b/src/Resources/scaffolds/6.0/reset-password.php @@ -24,11 +24,6 @@ 'zenstruck/mailer-test' => 'dev', ], 'configure' => function (FileManager $files) { - $files->dumpFile( - 'config/packages/reset_password.yaml', - file_get_contents(__DIR__.'/reset-password/config/packages/reset_password.yaml') - ); - $login = $files->getFileContents('templates/login.html.twig'); $forgotPassword = "\n Forgot your password?"; diff --git a/src/Resources/scaffolds/6.0/reset-password/config/packages/reset_password.yaml b/src/Resources/scaffolds/6.0/reset-password/config/packages/reset_password.yaml.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/reset-password/config/packages/reset_password.yaml rename to src/Resources/scaffolds/6.0/reset-password/config/packages/reset_password.yaml.tpl diff --git a/src/Resources/scaffolds/6.0/reset-password/src/Controller/ResetPasswordController.php b/src/Resources/scaffolds/6.0/reset-password/src/Controller/ResetPasswordController.php.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/reset-password/src/Controller/ResetPasswordController.php rename to src/Resources/scaffolds/6.0/reset-password/src/Controller/ResetPasswordController.php.tpl diff --git a/src/Resources/scaffolds/6.0/reset-password/src/Entity/ResetPasswordRequest.php b/src/Resources/scaffolds/6.0/reset-password/src/Entity/ResetPasswordRequest.php.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/reset-password/src/Entity/ResetPasswordRequest.php rename to src/Resources/scaffolds/6.0/reset-password/src/Entity/ResetPasswordRequest.php.tpl diff --git a/src/Resources/scaffolds/6.0/reset-password/src/Form/RequestResetFormType.php b/src/Resources/scaffolds/6.0/reset-password/src/Form/RequestResetFormType.php.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/reset-password/src/Form/RequestResetFormType.php rename to src/Resources/scaffolds/6.0/reset-password/src/Form/RequestResetFormType.php.tpl diff --git a/src/Resources/scaffolds/6.0/reset-password/src/Form/ResetPasswordFormType.php b/src/Resources/scaffolds/6.0/reset-password/src/Form/ResetPasswordFormType.php.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/reset-password/src/Form/ResetPasswordFormType.php rename to src/Resources/scaffolds/6.0/reset-password/src/Form/ResetPasswordFormType.php.tpl diff --git a/src/Resources/scaffolds/6.0/reset-password/src/Repository/ResetPasswordRequestRepository.php b/src/Resources/scaffolds/6.0/reset-password/src/Repository/ResetPasswordRequestRepository.php.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/reset-password/src/Repository/ResetPasswordRequestRepository.php rename to src/Resources/scaffolds/6.0/reset-password/src/Repository/ResetPasswordRequestRepository.php.tpl diff --git a/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/check_email.html.twig b/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/check_email.html.twig.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/reset-password/templates/reset_password/check_email.html.twig rename to src/Resources/scaffolds/6.0/reset-password/templates/reset_password/check_email.html.twig.tpl diff --git a/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/email.html.twig b/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/email.html.twig.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/reset-password/templates/reset_password/email.html.twig rename to src/Resources/scaffolds/6.0/reset-password/templates/reset_password/email.html.twig.tpl diff --git a/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/request.html.twig b/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/request.html.twig.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/reset-password/templates/reset_password/request.html.twig rename to src/Resources/scaffolds/6.0/reset-password/templates/reset_password/request.html.twig.tpl diff --git a/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/reset.html.twig b/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/reset.html.twig.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/reset-password/templates/reset_password/reset.html.twig rename to src/Resources/scaffolds/6.0/reset-password/templates/reset_password/reset.html.twig.tpl diff --git a/src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php b/src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php rename to src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php.tpl diff --git a/src/Resources/scaffolds/6.0/starter-kit.php b/src/Resources/scaffolds/6.0/starter-kit.php index f449aa086..ac007e5b6 100644 --- a/src/Resources/scaffolds/6.0/starter-kit.php +++ b/src/Resources/scaffolds/6.0/starter-kit.php @@ -9,8 +9,6 @@ * file that was distributed with this source code. */ -use Symfony\Bundle\MakerBundle\FileManager; - return [ 'description' => 'Starting kit with authentication, registration, password reset, user profile management with an application shell styled with Bootstrap CSS.', 'dependents' => [ @@ -20,7 +18,4 @@ 'change-password', 'profile', ], - 'configure' => function (FileManager $files) { - $files->dumpFile('templates/base.html.twig', file_get_contents(__DIR__.'/starter-kit/templates/base.html.twig')); - }, ]; diff --git a/src/Resources/scaffolds/6.0/starter-kit/templates/base.html.twig b/src/Resources/scaffolds/6.0/starter-kit/templates/base.html.twig.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/starter-kit/templates/base.html.twig rename to src/Resources/scaffolds/6.0/starter-kit/templates/base.html.twig.tpl diff --git a/src/Resources/scaffolds/6.0/user/src/Entity/User.php b/src/Resources/scaffolds/6.0/user/src/Entity/User.php.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/user/src/Entity/User.php rename to src/Resources/scaffolds/6.0/user/src/Entity/User.php.tpl diff --git a/src/Resources/scaffolds/6.0/user/src/Factory/UserFactory.php b/src/Resources/scaffolds/6.0/user/src/Factory/UserFactory.php.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/user/src/Factory/UserFactory.php rename to src/Resources/scaffolds/6.0/user/src/Factory/UserFactory.php.tpl diff --git a/src/Resources/scaffolds/6.0/user/src/Repository/UserRepository.php b/src/Resources/scaffolds/6.0/user/src/Repository/UserRepository.php.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/user/src/Repository/UserRepository.php rename to src/Resources/scaffolds/6.0/user/src/Repository/UserRepository.php.tpl diff --git a/src/Resources/scaffolds/6.0/user/tests/Unit/Entity/UserTest.php b/src/Resources/scaffolds/6.0/user/tests/Unit/Entity/UserTest.php.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/user/tests/Unit/Entity/UserTest.php rename to src/Resources/scaffolds/6.0/user/tests/Unit/Entity/UserTest.php.tpl From 653dce3a7799dd698dddea774e0bee827c05d0b1 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Thu, 14 Apr 2022 20:32:32 -0400 Subject: [PATCH 15/21] fix scaffold file cs --- .../6.0/auth/tests/Functional/AuthenticationTest.php.tpl | 6 +++--- .../templates/user/change_password.html.twig.tpl | 6 +++--- .../scaffolds/6.0/homepage/templates/index.html.twig.tpl | 2 +- .../templates/reset_password/request.html.twig.tpl | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Resources/scaffolds/6.0/auth/tests/Functional/AuthenticationTest.php.tpl b/src/Resources/scaffolds/6.0/auth/tests/Functional/AuthenticationTest.php.tpl index b310997f8..243fa69c3 100644 --- a/src/Resources/scaffolds/6.0/auth/tests/Functional/AuthenticationTest.php.tpl +++ b/src/Resources/scaffolds/6.0/auth/tests/Functional/AuthenticationTest.php.tpl @@ -108,7 +108,7 @@ class AuthenticationTest extends KernelTestCase ->assertOn('/') ->assertSuccessful() ->assertAuthenticated('mary@example.com') - ->use(function(CookieJar $cookieJar) { + ->use(function (CookieJar $cookieJar) { $cookieJar->expire('MOCKSESSID'); }) ->withProfiling() @@ -130,7 +130,7 @@ class AuthenticationTest extends KernelTestCase ->assertOn('/') ->assertSuccessful() ->assertAuthenticated('mary@example.com') - ->use(function(CookieJar $cookieJar) { + ->use(function (CookieJar $cookieJar) { $cookieJar->expire('MOCKSESSID'); }) ->visit('/') @@ -184,7 +184,7 @@ class AuthenticationTest extends KernelTestCase ->click('Sign in') ->assertOn('/') ->assertAuthenticated('mary@example.com') - ->use(function(CookieJar $cookieJar) { + ->use(function (CookieJar $cookieJar) { $cookieJar->expire('MOCKSESSID'); }) ->visit('/login') diff --git a/src/Resources/scaffolds/6.0/change-password/templates/user/change_password.html.twig.tpl b/src/Resources/scaffolds/6.0/change-password/templates/user/change_password.html.twig.tpl index f3a231ed8..b5df057e4 100644 --- a/src/Resources/scaffolds/6.0/change-password/templates/user/change_password.html.twig.tpl +++ b/src/Resources/scaffolds/6.0/change-password/templates/user/change_password.html.twig.tpl @@ -8,9 +8,9 @@

Change Password

{{ form_start(changePasswordForm) }} - {{ form_row(changePasswordForm.currentPassword, { label: 'Current Password' }) }} - {{ form_row(changePasswordForm.plainPassword.first, { label: 'New Password' }) }} - {{ form_row(changePasswordForm.plainPassword.second, { label: 'Repeat New Password' }) }} + {{ form_row(changePasswordForm.currentPassword, {label: 'Current Password'}) }} + {{ form_row(changePasswordForm.plainPassword.first, {label: 'New Password'}) }} + {{ form_row(changePasswordForm.plainPassword.second, {label: 'Repeat New Password'}) }} {{ form_end(changePasswordForm) }} diff --git a/src/Resources/scaffolds/6.0/homepage/templates/index.html.twig.tpl b/src/Resources/scaffolds/6.0/homepage/templates/index.html.twig.tpl index 9b2338c6c..09d96df90 100644 --- a/src/Resources/scaffolds/6.0/homepage/templates/index.html.twig.tpl +++ b/src/Resources/scaffolds/6.0/homepage/templates/index.html.twig.tpl @@ -7,7 +7,7 @@ {% for type, messages in app.flashes %} {% for message in messages %} -
+
{{ message }}
{% endfor %} diff --git a/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/request.html.twig.tpl b/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/request.html.twig.tpl index affab07aa..004df2a79 100644 --- a/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/request.html.twig.tpl +++ b/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/request.html.twig.tpl @@ -8,7 +8,7 @@

Reset your password

{{ form_start(requestForm) }} - {{ form_row(requestForm.email, { help: 'Enter your email address and we will send you a link to reset your password.' }) }} + {{ form_row(requestForm.email, {help: 'Enter your email address and we will send you a link to reset your password.'}) }} {{ form_end(requestForm) }} From b1581ca97983523a3cd0956d1d90bcaff9ae0137 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Fri, 29 Apr 2022 15:14:55 -0400 Subject: [PATCH 16/21] changes from review --- src/JsPackageManager.php | 15 +++++++- src/Maker/MakeScaffold.php | 17 +++++---- src/Resources/scaffolds/6.0/auth.php | 2 -- .../6.0/auth/src/Security/LoginUser.php.tpl | 3 ++ .../ChangePasswordController.php.tpl | 14 ++++---- .../src/Form/ChangePasswordFormType.php.tpl | 2 -- .../Functional/ChangePasswordTest.php.tpl | 8 ++--- .../src/Controller/HomepageController.php.tpl | 4 +-- ...x.html.twig.tpl => homepage.html.twig.tpl} | 0 .../src/Controller/ProfileController.php.tpl | 2 +- .../src/Controller/RegisterController.php.tpl | 6 ++-- .../ResetPasswordController.php.tpl | 14 ++++---- .../src/Entity/ResetPasswordRequest.php.tpl | 14 ++++---- .../src/Form/ResetPasswordFormType.php.tpl | 2 -- .../reset_password/reset.html.twig.tpl | 3 +- .../Functional/ResetPasswordTest.php.tpl | 4 +-- .../6.0/user/src/Entity/User.php.tpl | 18 +++++----- .../src/Repository/UserRepository.php.tpl | 35 +++++++++++++++---- 18 files changed, 99 insertions(+), 64 deletions(-) rename src/Resources/scaffolds/6.0/homepage/templates/{index.html.twig.tpl => homepage.html.twig.tpl} (100%) diff --git a/src/JsPackageManager.php b/src/JsPackageManager.php index 0ab82584e..f95b80edc 100644 --- a/src/JsPackageManager.php +++ b/src/JsPackageManager.php @@ -28,6 +28,14 @@ public function __construct(FileManager $fileManager) $this->files = $fileManager; } + public function isInstalled(string $package): bool + { + $packageJson = $this->packageJson(); + $deps = \array_merge($packageJson['dependencies'] ?? [], $packageJson['devDependencies'] ?? []); + + return \array_key_exists($package, $deps); + } + public function add(string $package, string $version): void { $packageWithVersion = "{$package}@{$version}"; @@ -77,7 +85,7 @@ private function bin(): string private function addToPackageJson(string $package, string $version): void { - $packageJson = json_decode($this->files->getFileContents('package.json'), true); + $packageJson = $this->packageJson(); $devDeps = $packageJson['devDependencies'] ?? []; $devDeps[$package] = $version; @@ -87,4 +95,9 @@ private function addToPackageJson(string $package, string $version): void $this->files->dumpFile('package.json', json_encode($packageJson, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)); } + + private function packageJson(): array + { + return json_decode($this->files->getFileContents('package.json'), true); + } } diff --git a/src/Maker/MakeScaffold.php b/src/Maker/MakeScaffold.php index 12e79b619..9031c0e67 100644 --- a/src/Maker/MakeScaffold.php +++ b/src/Maker/MakeScaffold.php @@ -31,7 +31,7 @@ */ final class MakeScaffold extends AbstractMaker { - private $files; + private $fileManager; private $jsPackageManager; private $availableScaffolds; private $composerBin; @@ -41,7 +41,7 @@ final class MakeScaffold extends AbstractMaker public function __construct(FileManager $files) { - $this->files = $files; + $this->fileManager = $files; $this->jsPackageManager = new JsPackageManager($files); } @@ -133,7 +133,7 @@ private function generateScaffold(string $name, ConsoleStyle $io): void $io->text("Installing Composer package: {$package}..."); $command = [$this->composerBin(), 'require', '--no-scripts', 'dev' === $env ? '--dev' : null, $package]; - $process = new Process(array_filter($command), $this->files->getRootDirectory()); + $process = new Process(array_filter($command), $this->fileManager->getRootDirectory()); $process->run(); @@ -147,7 +147,7 @@ private function generateScaffold(string $name, ConsoleStyle $io): void // install required js packages foreach ($scaffold['js_packages'] ?? [] as $package => $version) { - if (!\in_array($package, $this->installedJsPackages, true)) { + if (!$this->isJsPackageInstalled($package)) { $io->text("Installing JS package: {$package}@{$version}..."); $this->jsPackageManager->add($package, $version); @@ -159,7 +159,7 @@ private function generateScaffold(string $name, ConsoleStyle $io): void $io->text('Copying scaffold files...'); foreach (Finder::create()->files()->in($scaffold['dir']) as $file) { - $this->files->dumpFile( + $this->fileManager->dumpFile( "{$file->getRelativePath()}/{$file->getFilenameWithoutExtension()}", $file->getContents() ); @@ -169,7 +169,7 @@ private function generateScaffold(string $name, ConsoleStyle $io): void if (isset($scaffold['configure'])) { $io->text('Executing configuration...'); - $scaffold['configure']($this->files); + $scaffold['configure']($this->fileManager); } $io->text("Successfully installed scaffold {$name}."); @@ -213,6 +213,11 @@ private function isPackageInstalled(string $package): bool return InstalledVersions::isInstalled($package) || \in_array($package, $this->installedPackages, true); } + private function isJsPackageInstalled(string $package): bool + { + return $this->jsPackageManager->isInstalled($package) || \in_array($package, $this->installedJsPackages, true); + } + /** * Detect if package is installed in the same process (when installing * multiple scaffolds at once). diff --git a/src/Resources/scaffolds/6.0/auth.php b/src/Resources/scaffolds/6.0/auth.php index 83002a4bf..5240d30e1 100644 --- a/src/Resources/scaffolds/6.0/auth.php +++ b/src/Resources/scaffolds/6.0/auth.php @@ -55,8 +55,6 @@ ], 'remember_me' => [ 'secret' => '%kernel.secret%', - 'secure' => 'auto', - 'samesite' => 'lax', ], ]; diff --git a/src/Resources/scaffolds/6.0/auth/src/Security/LoginUser.php.tpl b/src/Resources/scaffolds/6.0/auth/src/Security/LoginUser.php.tpl index 7eca75d72..7eb4a3a84 100644 --- a/src/Resources/scaffolds/6.0/auth/src/Security/LoginUser.php.tpl +++ b/src/Resources/scaffolds/6.0/auth/src/Security/LoginUser.php.tpl @@ -7,6 +7,9 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +/** + * Service to programatically login a user. + */ class LoginUser { public function __construct( diff --git a/src/Resources/scaffolds/6.0/change-password/src/Controller/ChangePasswordController.php.tpl b/src/Resources/scaffolds/6.0/change-password/src/Controller/ChangePasswordController.php.tpl index aa97f683e..edf63b967 100644 --- a/src/Resources/scaffolds/6.0/change-password/src/Controller/ChangePasswordController.php.tpl +++ b/src/Resources/scaffolds/6.0/change-password/src/Controller/ChangePasswordController.php.tpl @@ -10,7 +10,6 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Routing\Annotation\Route; -use Symfony\Component\Security\Core\User\UserInterface; #[Route('/user/change-password', name: 'change_password')] class ChangePasswordController extends AbstractController @@ -19,11 +18,10 @@ class ChangePasswordController extends AbstractController Request $request, UserPasswordHasherInterface $userPasswordHasher, UserRepository $userRepository, - ?UserInterface $user = null, ): Response { - if (!$user) { - throw $this->createAccessDeniedException(); - } + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED'); + + $user = $this->getUser(); if (!$user instanceof User) { throw new \LogicException('Invalid user type.'); @@ -41,14 +39,14 @@ class ChangePasswordController extends AbstractController ) ); - $userRepository->save($user); + $userRepository->add($user); $this->addFlash('success', 'You\'ve successfully changed your password.'); return $this->redirectToRoute('homepage'); } - return $this->render('user/change_password.html.twig', [ - 'changePasswordForm' => $form->createView(), + return $this->renderForm('user/change_password.html.twig', [ + 'changePasswordForm' => $form, ]); } } diff --git a/src/Resources/scaffolds/6.0/change-password/src/Form/ChangePasswordFormType.php.tpl b/src/Resources/scaffolds/6.0/change-password/src/Form/ChangePasswordFormType.php.tpl index 580193eb8..8de10fcac 100644 --- a/src/Resources/scaffolds/6.0/change-password/src/Form/ChangePasswordFormType.php.tpl +++ b/src/Resources/scaffolds/6.0/change-password/src/Form/ChangePasswordFormType.php.tpl @@ -37,11 +37,9 @@ class ChangePasswordFormType extends AbstractType 'max' => 4096, ]), ], - 'label' => 'New password', ], 'second_options' => [ 'attr' => ['autocomplete' => 'new-password'], - 'label' => 'Repeat Password', ], 'invalid_message' => 'The password fields must match.', // Instead of being set onto the object directly, diff --git a/src/Resources/scaffolds/6.0/change-password/tests/Functional/ChangePasswordTest.php.tpl b/src/Resources/scaffolds/6.0/change-password/tests/Functional/ChangePasswordTest.php.tpl index 9d1d0612d..999294a11 100644 --- a/src/Resources/scaffolds/6.0/change-password/tests/Functional/ChangePasswordTest.php.tpl +++ b/src/Resources/scaffolds/6.0/change-password/tests/Functional/ChangePasswordTest.php.tpl @@ -58,7 +58,7 @@ final class ChangePasswordTest extends KernelTestCase ->fillField('New Password', 'new-password') ->fillField('Repeat New Password', 'new-password') ->click('Change Password') - ->assertSuccessful() + ->assertStatus(422) ->assertOn('/user/change-password') ->assertSee('This is not your current password.') ->assertAuthenticated('mary@example.com') @@ -78,7 +78,7 @@ final class ChangePasswordTest extends KernelTestCase ->fillField('New Password', 'new-password') ->fillField('Repeat New Password', 'new-password') ->click('Change Password') - ->assertSuccessful() + ->assertStatus(422) ->assertOn('/user/change-password') ->assertSee('This is not your current password.') ->assertAuthenticated('mary@example.com') @@ -97,7 +97,7 @@ final class ChangePasswordTest extends KernelTestCase ->visit('/user/change-password') ->fillField('Current Password', '1234') ->click('Change Password') - ->assertSuccessful() + ->assertStatus(422) ->assertOn('/user/change-password') ->assertSee('Please enter a password.') ->assertAuthenticated('mary@example.com') @@ -118,7 +118,7 @@ final class ChangePasswordTest extends KernelTestCase ->fillField('New Password', 'new-password') ->fillField('Repeat New Password', 'different-new-password') ->click('Change Password') - ->assertSuccessful() + ->assertStatus(422) ->assertOn('/user/change-password') ->assertSee('The password fields must match.') ->assertAuthenticated('mary@example.com') diff --git a/src/Resources/scaffolds/6.0/homepage/src/Controller/HomepageController.php.tpl b/src/Resources/scaffolds/6.0/homepage/src/Controller/HomepageController.php.tpl index fd9ace081..ea73c2dbc 100644 --- a/src/Resources/scaffolds/6.0/homepage/src/Controller/HomepageController.php.tpl +++ b/src/Resources/scaffolds/6.0/homepage/src/Controller/HomepageController.php.tpl @@ -6,11 +6,11 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; -#[Route('/', name: 'homepage')] class HomepageController extends AbstractController { + #[Route('/', name: 'homepage')] public function __invoke(): Response { - return $this->render('index.html.twig'); + return $this->render('homepage.html.twig'); } } diff --git a/src/Resources/scaffolds/6.0/homepage/templates/index.html.twig.tpl b/src/Resources/scaffolds/6.0/homepage/templates/homepage.html.twig.tpl similarity index 100% rename from src/Resources/scaffolds/6.0/homepage/templates/index.html.twig.tpl rename to src/Resources/scaffolds/6.0/homepage/templates/homepage.html.twig.tpl diff --git a/src/Resources/scaffolds/6.0/profile/src/Controller/ProfileController.php.tpl b/src/Resources/scaffolds/6.0/profile/src/Controller/ProfileController.php.tpl index 8f0e24f02..01fa8dfee 100644 --- a/src/Resources/scaffolds/6.0/profile/src/Controller/ProfileController.php.tpl +++ b/src/Resources/scaffolds/6.0/profile/src/Controller/ProfileController.php.tpl @@ -28,7 +28,7 @@ class ProfileController extends AbstractController $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - $userRepository->save($user); + $userRepository->add($user); $this->addFlash('success', 'You\'ve successfully updated your profile.'); return $this->redirectToRoute('homepage'); diff --git a/src/Resources/scaffolds/6.0/register/src/Controller/RegisterController.php.tpl b/src/Resources/scaffolds/6.0/register/src/Controller/RegisterController.php.tpl index e6f01381f..32cebb31d 100644 --- a/src/Resources/scaffolds/6.0/register/src/Controller/RegisterController.php.tpl +++ b/src/Resources/scaffolds/6.0/register/src/Controller/RegisterController.php.tpl @@ -19,7 +19,7 @@ class RegisterController extends AbstractController Request $request, UserPasswordHasherInterface $userPasswordHasher, UserRepository $userRepository, - LoginUser $login, + LoginUser $loginUser, ): Response { $user = new User(); $form = $this->createForm(RegistrationFormType::class, $user); @@ -34,11 +34,11 @@ class RegisterController extends AbstractController ) ); - $userRepository->save($user); + $userRepository->add($user); // do anything else you need here, like send an email // authenticate the user - $login($user, $request); + $loginUser($user, $request); $this->addFlash('success', 'You\'ve successfully registered and are now logged in.'); return $this->redirectToRoute('homepage'); diff --git a/src/Resources/scaffolds/6.0/reset-password/src/Controller/ResetPasswordController.php.tpl b/src/Resources/scaffolds/6.0/reset-password/src/Controller/ResetPasswordController.php.tpl index 7320e183e..f978b85fd 100644 --- a/src/Resources/scaffolds/6.0/reset-password/src/Controller/ResetPasswordController.php.tpl +++ b/src/Resources/scaffolds/6.0/reset-password/src/Controller/ResetPasswordController.php.tpl @@ -50,8 +50,8 @@ class ResetPasswordController extends AbstractController ); } - return $this->render('reset_password/request.html.twig', [ - 'requestForm' => $form->createView(), + return $this->renderForm('reset_password/request.html.twig', [ + 'requestForm' => $form, ]); } @@ -76,7 +76,7 @@ class ResetPasswordController extends AbstractController * Validates and process the reset URL that the user clicked in their email. */ #[Route('/reset/{token}', name: 'reset_password')] - public function reset(Request $request, LoginUser $login, UserPasswordHasherInterface $userPasswordHasher, string $token = null): Response + public function reset(Request $request, LoginUser $loginUser, UserPasswordHasherInterface $userPasswordHasher, string $token = null): Response { if ($token) { // We store the token in session and remove it from the URL, to avoid the URL being @@ -110,21 +110,21 @@ class ResetPasswordController extends AbstractController // Encode(hash) the plain password, and set it. $user->setPassword($userPasswordHasher->hashPassword($user, $form->get('plainPassword')->getData())); - $this->userRepository->save($user); + $this->userRepository->add($user); // The session is cleaned up after the password has been changed. $this->cleanSessionAfterReset(); // programmatic user login - $login($user, $request); + $loginUser($user, $request); $this->addFlash('success', 'Your password was successfully reset, you are now logged in.'); return $this->redirectToRoute('homepage'); } - return $this->render('reset_password/reset.html.twig', [ - 'resetForm' => $form->createView(), + return $this->renderForm('reset_password/reset.html.twig', [ + 'resetForm' => $form, ]); } diff --git a/src/Resources/scaffolds/6.0/reset-password/src/Entity/ResetPasswordRequest.php.tpl b/src/Resources/scaffolds/6.0/reset-password/src/Entity/ResetPasswordRequest.php.tpl index 2676ef571..a94bacd4d 100644 --- a/src/Resources/scaffolds/6.0/reset-password/src/Entity/ResetPasswordRequest.php.tpl +++ b/src/Resources/scaffolds/6.0/reset-password/src/Entity/ResetPasswordRequest.php.tpl @@ -14,14 +14,14 @@ class ResetPasswordRequest implements ResetPasswordRequestInterface #[ORM\Id] #[ORM\GeneratedValue] - #[ORM\Column(type: 'integer')] - private $id; + #[ORM\Column] + private ?int $id = null; - #[ORM\ManyToOne(targetEntity: User::class)] - #[ORM\JoinColumn(nullable: false)] - private $user; + #[ORM\ManyToOne] + #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] + private User $user; - public function __construct(object $user, \DateTimeInterface $expiresAt, string $selector, string $hashedToken) + public function __construct(User $user, \DateTimeInterface $expiresAt, string $selector, string $hashedToken) { $this->user = $user; $this->initialize($expiresAt, $selector, $hashedToken); @@ -32,7 +32,7 @@ class ResetPasswordRequest implements ResetPasswordRequestInterface return $this->id; } - public function getUser(): object + public function getUser(): User { return $this->user; } diff --git a/src/Resources/scaffolds/6.0/reset-password/src/Form/ResetPasswordFormType.php.tpl b/src/Resources/scaffolds/6.0/reset-password/src/Form/ResetPasswordFormType.php.tpl index 432f9c318..ee77c0515 100644 --- a/src/Resources/scaffolds/6.0/reset-password/src/Form/ResetPasswordFormType.php.tpl +++ b/src/Resources/scaffolds/6.0/reset-password/src/Form/ResetPasswordFormType.php.tpl @@ -30,11 +30,9 @@ class ResetPasswordFormType extends AbstractType 'max' => 4096, ]), ], - 'label' => 'New password', ], 'second_options' => [ 'attr' => ['autocomplete' => 'new-password'], - 'label' => 'Repeat Password', ], 'invalid_message' => 'The password fields must match.', // Instead of being set onto the object directly, diff --git a/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/reset.html.twig.tpl b/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/reset.html.twig.tpl index 39ceff3c3..54d044644 100644 --- a/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/reset.html.twig.tpl +++ b/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/reset.html.twig.tpl @@ -8,7 +8,8 @@

Reset your password

{{ form_start(resetForm) }} - {{ form_row(resetForm.plainPassword) }} + {{ form_row(resetForm.plainPassword.first, {label: 'New Password'}) }} + {{ form_row(resetForm.plainPassword.second, {label: 'Repeat New Password'}) }} {{ form_end(resetForm) }}
diff --git a/src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php.tpl b/src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php.tpl index 7cd605523..61403d372 100644 --- a/src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php.tpl +++ b/src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php.tpl @@ -51,8 +51,8 @@ final class ResetPasswordTest extends KernelTestCase $this->browser() ->visit($resetUrl) - ->fillField('New password', 'new-password') - ->fillField('Repeat Password', 'new-password') + ->fillField('New Password', 'new-password') + ->fillField('Repeat New Password', 'new-password') ->click('Reset password') ->assertOn('/') ->assertSeeIn('.alert', 'Your password was successfully reset, you are now logged in.') diff --git a/src/Resources/scaffolds/6.0/user/src/Entity/User.php.tpl b/src/Resources/scaffolds/6.0/user/src/Entity/User.php.tpl index 77dede2d5..51b5799ea 100644 --- a/src/Resources/scaffolds/6.0/user/src/Entity/User.php.tpl +++ b/src/Resources/scaffolds/6.0/user/src/Entity/User.php.tpl @@ -12,20 +12,20 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface { #[ORM\Id] #[ORM\GeneratedValue] - #[ORM\Column(type: 'integer')] - private $id; + #[ORM\Column] + private ?int $id = null; - #[ORM\Column(type: 'string', length: 180, unique: true)] - private $email; + #[ORM\Column(unique: true)] + private ?string $email = null; - #[ORM\Column(type: 'string', length: 255)] - private $name; + #[ORM\Column] + private ?string $name = null; #[ORM\Column(type: 'json')] - private $roles = []; + private array $roles = []; - #[ORM\Column(type: 'string')] - private $password; + #[ORM\Column()] + private ?string $password = null; public function getId(): ?int { diff --git a/src/Resources/scaffolds/6.0/user/src/Repository/UserRepository.php.tpl b/src/Resources/scaffolds/6.0/user/src/Repository/UserRepository.php.tpl index 0846acd19..9df0b83cb 100644 --- a/src/Resources/scaffolds/6.0/user/src/Repository/UserRepository.php.tpl +++ b/src/Resources/scaffolds/6.0/user/src/Repository/UserRepository.php.tpl @@ -4,6 +4,8 @@ namespace App\Repository; use App\Entity\User; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\ORM\OptimisticLockException; +use Doctrine\ORM\ORMException; use Doctrine\Persistence\ManagerRegistry; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; @@ -22,6 +24,31 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader parent::__construct($registry, User::class); } + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function add(User $entity, bool $flush = true): void + { + $this->_em->persist($entity); + + if ($flush) { + $this->_em->flush(); + } + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function remove(User $entity, bool $flush = true): void + { + $this->_em->remove($entity); + if ($flush) { + $this->_em->flush(); + } + } + /** * Used to upgrade (rehash) the user's password automatically over time. */ @@ -32,12 +59,6 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader } $user->setPassword($newHashedPassword); - $this->save($user); - } - - public function save(User $user): void - { - $this->_em->persist($user); - $this->_em->flush(); + $this->add($user); } } From 062542a42bfe216bb109c79a58ac24917dea54c8 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Mon, 2 May 2022 10:25:15 -0400 Subject: [PATCH 17/21] adjustments from review --- src/Resources/scaffolds/6.0/bootstrapcss.php | 3 -- .../bootstrapcss/assets/styles/app.css.tpl | 1 + .../scaffolds/6.0/change-password.php | 1 - .../ChangePasswordController.php.tpl | 2 +- .../src/Form/ChangePasswordFormType.php.tpl | 36 +++++++------------ .../user/change_password.html.twig.tpl | 3 +- .../Functional/ChangePasswordTest.php.tpl | 24 ------------- src/Resources/scaffolds/6.0/profile.php | 1 - .../profile/src/Form/ProfileFormType.php.tpl | 7 +--- src/Resources/scaffolds/6.0/register.php | 24 ------------- .../src/Form/RegistrationFormType.php.tpl | 14 ++------ .../scaffolds/6.0/reset-password.php | 1 - .../src/Form/ResetPasswordFormType.php.tpl | 36 +++++++------------ .../reset_password/reset.html.twig.tpl | 3 +- .../Functional/ResetPasswordTest.php.tpl | 1 - src/Resources/scaffolds/6.0/user.php | 1 + .../6.0/user/src/Entity/User.php.tpl | 6 ++++ 17 files changed, 38 insertions(+), 126 deletions(-) create mode 100644 src/Resources/scaffolds/6.0/bootstrapcss/assets/styles/app.css.tpl diff --git a/src/Resources/scaffolds/6.0/bootstrapcss.php b/src/Resources/scaffolds/6.0/bootstrapcss.php index f5d0a596a..42601232e 100644 --- a/src/Resources/scaffolds/6.0/bootstrapcss.php +++ b/src/Resources/scaffolds/6.0/bootstrapcss.php @@ -29,9 +29,6 @@ return $data; }); - // add bootstrap to app.css - $files->dumpFile('assets/styles/app.css', "@import \"~bootstrap/dist/css/bootstrap.css\";\n"); - // add bootstrap to app.js $appJs = $files->getFileContents('assets/app.js'); diff --git a/src/Resources/scaffolds/6.0/bootstrapcss/assets/styles/app.css.tpl b/src/Resources/scaffolds/6.0/bootstrapcss/assets/styles/app.css.tpl new file mode 100644 index 000000000..327bd375d --- /dev/null +++ b/src/Resources/scaffolds/6.0/bootstrapcss/assets/styles/app.css.tpl @@ -0,0 +1 @@ +@import "~bootstrap/dist/css/bootstrap.css"; diff --git a/src/Resources/scaffolds/6.0/change-password.php b/src/Resources/scaffolds/6.0/change-password.php index 39c2582ad..31acbd56f 100644 --- a/src/Resources/scaffolds/6.0/change-password.php +++ b/src/Resources/scaffolds/6.0/change-password.php @@ -16,6 +16,5 @@ ], 'packages' => [ 'symfony/form' => 'all', - 'symfony/validator' => 'all', ], ]; diff --git a/src/Resources/scaffolds/6.0/change-password/src/Controller/ChangePasswordController.php.tpl b/src/Resources/scaffolds/6.0/change-password/src/Controller/ChangePasswordController.php.tpl index edf63b967..d0d4cc9d6 100644 --- a/src/Resources/scaffolds/6.0/change-password/src/Controller/ChangePasswordController.php.tpl +++ b/src/Resources/scaffolds/6.0/change-password/src/Controller/ChangePasswordController.php.tpl @@ -27,7 +27,7 @@ class ChangePasswordController extends AbstractController throw new \LogicException('Invalid user type.'); } - $form = $this->createForm(ChangePasswordFormType::class, $user); + $form = $this->createForm(ChangePasswordFormType::class); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { diff --git a/src/Resources/scaffolds/6.0/change-password/src/Form/ChangePasswordFormType.php.tpl b/src/Resources/scaffolds/6.0/change-password/src/Form/ChangePasswordFormType.php.tpl index 8de10fcac..fb8982ece 100644 --- a/src/Resources/scaffolds/6.0/change-password/src/Form/ChangePasswordFormType.php.tpl +++ b/src/Resources/scaffolds/6.0/change-password/src/Form/ChangePasswordFormType.php.tpl @@ -4,7 +4,6 @@ namespace App\Form; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\PasswordType; -use Symfony\Component\Form\Extension\Core\Type\RepeatedType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Security\Core\Validator\Constraints\UserPassword; @@ -20,31 +19,20 @@ class ChangePasswordFormType extends AbstractType 'constraints' => [ new UserPassword(['message' => 'This is not your current password.']), ], - 'mapped' => false, ]) - ->add('plainPassword', RepeatedType::class, [ - 'type' => PasswordType::class, - 'first_options' => [ - 'attr' => ['autocomplete' => 'new-password'], - 'constraints' => [ - new NotBlank([ - 'message' => 'Please enter a password.', - ]), - new Length([ - 'min' => 6, - 'minMessage' => 'Your password should be at least {{ limit }} characters', - // max length allowed by Symfony for security reasons - 'max' => 4096, - ]), - ], - ], - 'second_options' => [ - 'attr' => ['autocomplete' => 'new-password'], + ->add('plainPassword', PasswordType::class, [ + 'attr' => ['autocomplete' => 'new-password'], + 'constraints' => [ + new NotBlank([ + 'message' => 'Please enter a password.', + ]), + new Length([ + 'min' => 6, + 'minMessage' => 'Your password should be at least {{ limit }} characters', + // max length allowed by Symfony for security reasons + 'max' => 4096, + ]), ], - 'invalid_message' => 'The password fields must match.', - // Instead of being set onto the object directly, - // this is read and encoded in the controller - 'mapped' => false, ]) ; } diff --git a/src/Resources/scaffolds/6.0/change-password/templates/user/change_password.html.twig.tpl b/src/Resources/scaffolds/6.0/change-password/templates/user/change_password.html.twig.tpl index b5df057e4..15308beb7 100644 --- a/src/Resources/scaffolds/6.0/change-password/templates/user/change_password.html.twig.tpl +++ b/src/Resources/scaffolds/6.0/change-password/templates/user/change_password.html.twig.tpl @@ -9,8 +9,7 @@ {{ form_start(changePasswordForm) }} {{ form_row(changePasswordForm.currentPassword, {label: 'Current Password'}) }} - {{ form_row(changePasswordForm.plainPassword.first, {label: 'New Password'}) }} - {{ form_row(changePasswordForm.plainPassword.second, {label: 'Repeat New Password'}) }} + {{ form_row(changePasswordForm.plainPassword, {label: 'New Password'}) }} {{ form_end(changePasswordForm) }} diff --git a/src/Resources/scaffolds/6.0/change-password/tests/Functional/ChangePasswordTest.php.tpl b/src/Resources/scaffolds/6.0/change-password/tests/Functional/ChangePasswordTest.php.tpl index 999294a11..bc55c227f 100644 --- a/src/Resources/scaffolds/6.0/change-password/tests/Functional/ChangePasswordTest.php.tpl +++ b/src/Resources/scaffolds/6.0/change-password/tests/Functional/ChangePasswordTest.php.tpl @@ -27,7 +27,6 @@ final class ChangePasswordTest extends KernelTestCase ->visit('/user/change-password') ->fillField('Current Password', '1234') ->fillField('New Password', 'new-password') - ->fillField('Repeat New Password', 'new-password') ->click('Change Password') ->assertSuccessful() ->assertOn('/') @@ -56,7 +55,6 @@ final class ChangePasswordTest extends KernelTestCase ->visit('/user/change-password') ->fillField('Current Password', 'invalid') ->fillField('New Password', 'new-password') - ->fillField('Repeat New Password', 'new-password') ->click('Change Password') ->assertStatus(422) ->assertOn('/user/change-password') @@ -76,7 +74,6 @@ final class ChangePasswordTest extends KernelTestCase ->actingAs($user->object()) ->visit('/user/change-password') ->fillField('New Password', 'new-password') - ->fillField('Repeat New Password', 'new-password') ->click('Change Password') ->assertStatus(422) ->assertOn('/user/change-password') @@ -106,27 +103,6 @@ final class ChangePasswordTest extends KernelTestCase $this->assertSame($currentPassword, $user->getPassword()); } - public function testNewPasswordsMustMatch(): void - { - $user = UserFactory::createOne(['email' => 'mary@example.com', 'password' => '1234']); - $currentPassword = $user->getPassword(); - - $this->browser() - ->actingAs($user->object()) - ->visit('/user/change-password') - ->fillField('Current Password', '1234') - ->fillField('New Password', 'new-password') - ->fillField('Repeat New Password', 'different-new-password') - ->click('Change Password') - ->assertStatus(422) - ->assertOn('/user/change-password') - ->assertSee('The password fields must match.') - ->assertAuthenticated('mary@example.com') - ; - - $this->assertSame($currentPassword, $user->getPassword()); - } - public function testCannotAccessChangePasswordPageIfNotLoggedIn(): void { $this->browser() diff --git a/src/Resources/scaffolds/6.0/profile.php b/src/Resources/scaffolds/6.0/profile.php index ae9fc80b4..20d72f2bb 100644 --- a/src/Resources/scaffolds/6.0/profile.php +++ b/src/Resources/scaffolds/6.0/profile.php @@ -16,6 +16,5 @@ ], 'packages' => [ 'symfony/form' => 'all', - 'symfony/validator' => 'all', ], ]; diff --git a/src/Resources/scaffolds/6.0/profile/src/Form/ProfileFormType.php.tpl b/src/Resources/scaffolds/6.0/profile/src/Form/ProfileFormType.php.tpl index cee45e281..077166489 100644 --- a/src/Resources/scaffolds/6.0/profile/src/Form/ProfileFormType.php.tpl +++ b/src/Resources/scaffolds/6.0/profile/src/Form/ProfileFormType.php.tpl @@ -6,18 +6,13 @@ use App\Entity\User; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Validator\Constraints\NotBlank; class ProfileFormType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void { $builder - ->add('name', null, [ - 'constraints' => [ - new NotBlank(['message' => 'Name is required']), - ], - ]) + ->add('name') ; } diff --git a/src/Resources/scaffolds/6.0/register.php b/src/Resources/scaffolds/6.0/register.php index 51fec0230..cdc8f50f6 100644 --- a/src/Resources/scaffolds/6.0/register.php +++ b/src/Resources/scaffolds/6.0/register.php @@ -9,8 +9,6 @@ * file that was distributed with this source code. */ -use Symfony\Bundle\MakerBundle\FileManager; - return [ 'description' => 'Create registration form and tests.', 'dependents' => [ @@ -18,27 +16,5 @@ ], 'packages' => [ 'symfony/form' => 'all', - 'symfony/validator' => 'all', ], - 'configure' => function (FileManager $files) { - $userEntity = $files->getFileContents('src/Entity/User.php'); - - if (str_contains($userEntity, $attribute = "#[UniqueEntity(fields: ['email'], message: 'There is already an account with this email')]")) { - // unique constraint already there - return; - } - - $userEntity = str_replace( - [ - '#[ORM\Entity(repositoryClass: UserRepository::class)]', - 'use Doctrine\ORM\Mapping as ORM;', - ], - [ - "#[ORM\Entity(repositoryClass: UserRepository::class)]\n{$attribute}", - "use Doctrine\ORM\Mapping as ORM;\nuse Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;", - ], - $userEntity - ); - $files->dumpFile('src/Entity/User.php', $userEntity); - }, ]; diff --git a/src/Resources/scaffolds/6.0/register/src/Form/RegistrationFormType.php.tpl b/src/Resources/scaffolds/6.0/register/src/Form/RegistrationFormType.php.tpl index 16fe83e07..5b792cf0a 100644 --- a/src/Resources/scaffolds/6.0/register/src/Form/RegistrationFormType.php.tpl +++ b/src/Resources/scaffolds/6.0/register/src/Form/RegistrationFormType.php.tpl @@ -8,7 +8,6 @@ use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\PasswordType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Validator\Constraints\Email; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\NotBlank; @@ -17,17 +16,8 @@ class RegistrationFormType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options): void { $builder - ->add('name', null, [ - 'constraints' => [ - new NotBlank(['message' => 'Name is required']), - ], - ]) - ->add('email', EmailType::class, [ - 'constraints' => [ - new NotBlank(['message' => 'Email is required']), - new Email(['message' => 'This is not a valid email address']), - ], - ]) + ->add('name') + ->add('email', EmailType::class) ->add('plainPassword', PasswordType::class, [ // instead of being set onto the object directly, // this is read and encoded in the controller diff --git a/src/Resources/scaffolds/6.0/reset-password.php b/src/Resources/scaffolds/6.0/reset-password.php index 4143f14be..f8b1f1b47 100644 --- a/src/Resources/scaffolds/6.0/reset-password.php +++ b/src/Resources/scaffolds/6.0/reset-password.php @@ -18,7 +18,6 @@ ], 'packages' => [ 'symfony/form' => 'all', - 'symfony/validator' => 'all', 'symfony/mailer' => 'all', 'symfonycasts/reset-password-bundle' => 'all', 'zenstruck/mailer-test' => 'dev', diff --git a/src/Resources/scaffolds/6.0/reset-password/src/Form/ResetPasswordFormType.php.tpl b/src/Resources/scaffolds/6.0/reset-password/src/Form/ResetPasswordFormType.php.tpl index ee77c0515..9410d7769 100644 --- a/src/Resources/scaffolds/6.0/reset-password/src/Form/ResetPasswordFormType.php.tpl +++ b/src/Resources/scaffolds/6.0/reset-password/src/Form/ResetPasswordFormType.php.tpl @@ -4,7 +4,6 @@ namespace App\Form; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\PasswordType; -use Symfony\Component\Form\Extension\Core\Type\RepeatedType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Validator\Constraints\Length; @@ -15,35 +14,24 @@ class ResetPasswordFormType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options): void { $builder - ->add('plainPassword', RepeatedType::class, [ - 'type' => PasswordType::class, - 'first_options' => [ - 'attr' => ['autocomplete' => 'new-password'], - 'constraints' => [ - new NotBlank([ - 'message' => 'Please enter a password', - ]), - new Length([ - 'min' => 6, - 'minMessage' => 'Your password should be at least {{ limit }} characters', - // max length allowed by Symfony for security reasons - 'max' => 4096, - ]), - ], + ->add('plainPassword', PasswordType::class, [ + 'attr' => ['autocomplete' => 'new-password'], + 'constraints' => [ + new NotBlank([ + 'message' => 'Please enter a password', + ]), + new Length([ + 'min' => 6, + 'minMessage' => 'Your password should be at least {{ limit }} characters', + // max length allowed by Symfony for security reasons + 'max' => 4096, + ]), ], - 'second_options' => [ - 'attr' => ['autocomplete' => 'new-password'], - ], - 'invalid_message' => 'The password fields must match.', - // Instead of being set onto the object directly, - // this is read and encoded in the controller - 'mapped' => false, ]) ; } public function configureOptions(OptionsResolver $resolver): void { - $resolver->setDefaults([]); } } diff --git a/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/reset.html.twig.tpl b/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/reset.html.twig.tpl index 54d044644..d64046a3b 100644 --- a/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/reset.html.twig.tpl +++ b/src/Resources/scaffolds/6.0/reset-password/templates/reset_password/reset.html.twig.tpl @@ -8,8 +8,7 @@

Reset your password

{{ form_start(resetForm) }} - {{ form_row(resetForm.plainPassword.first, {label: 'New Password'}) }} - {{ form_row(resetForm.plainPassword.second, {label: 'Repeat New Password'}) }} + {{ form_row(resetForm.plainPassword, {label: 'New Password'}) }} {{ form_end(resetForm) }}
diff --git a/src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php.tpl b/src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php.tpl index 61403d372..ec9f7962c 100644 --- a/src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php.tpl +++ b/src/Resources/scaffolds/6.0/reset-password/tests/Functional/ResetPasswordTest.php.tpl @@ -52,7 +52,6 @@ final class ResetPasswordTest extends KernelTestCase $this->browser() ->visit($resetUrl) ->fillField('New Password', 'new-password') - ->fillField('Repeat New Password', 'new-password') ->click('Reset password') ->assertOn('/') ->assertSeeIn('.alert', 'Your password was successfully reset, you are now logged in.') diff --git a/src/Resources/scaffolds/6.0/user.php b/src/Resources/scaffolds/6.0/user.php index 9510909aa..5c662458f 100644 --- a/src/Resources/scaffolds/6.0/user.php +++ b/src/Resources/scaffolds/6.0/user.php @@ -15,6 +15,7 @@ 'doctrine/orm' => 'all', 'doctrine/doctrine-bundle' => 'all', 'symfony/security-bundle' => 'all', + 'symfony/validator' => 'all', 'phpunit/phpunit' => 'dev', 'symfony/phpunit-bridge' => 'dev', 'zenstruck/foundry' => 'dev', diff --git a/src/Resources/scaffolds/6.0/user/src/Entity/User.php.tpl b/src/Resources/scaffolds/6.0/user/src/Entity/User.php.tpl index 51b5799ea..1b57e0710 100644 --- a/src/Resources/scaffolds/6.0/user/src/Entity/User.php.tpl +++ b/src/Resources/scaffolds/6.0/user/src/Entity/User.php.tpl @@ -4,10 +4,13 @@ namespace App\Entity; use App\Repository\UserRepository; use Doctrine\ORM\Mapping as ORM; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity(repositoryClass: UserRepository::class)] +#[UniqueEntity(fields: ['email'], message: 'There is already an account with this email')] class User implements UserInterface, PasswordAuthenticatedUserInterface { #[ORM\Id] @@ -16,9 +19,12 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface private ?int $id = null; #[ORM\Column(unique: true)] + #[Assert\NotBlank(message: 'Email is required')] + #[Assert\Email(message: 'This is not a valid email address')] private ?string $email = null; #[ORM\Column] + #[Assert\NotBlank(message: 'Name is required')] private ?string $name = null; #[ORM\Column(type: 'json')] From 0ea3bae44cba9deb1b718caa1d36dcd9ae184eab Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Mon, 2 May 2022 11:09:39 -0400 Subject: [PATCH 18/21] confirm before overwriting files --- src/Maker/MakeScaffold.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Maker/MakeScaffold.php b/src/Maker/MakeScaffold.php index 9031c0e67..8a79e14f9 100644 --- a/src/Maker/MakeScaffold.php +++ b/src/Maker/MakeScaffold.php @@ -159,10 +159,11 @@ private function generateScaffold(string $name, ConsoleStyle $io): void $io->text('Copying scaffold files...'); foreach (Finder::create()->files()->in($scaffold['dir']) as $file) { - $this->fileManager->dumpFile( - "{$file->getRelativePath()}/{$file->getFilenameWithoutExtension()}", - $file->getContents() - ); + $filename = "{$file->getRelativePath()}/{$file->getFilenameWithoutExtension()}"; + + if (!$this->fileManager->fileExists($filename) || $io->confirm("Overwrite \"{$filename}\"?")) { + $this->fileManager->dumpFile($filename, $file->getContents()); + } } } From 69b8b905426d31f39805eb13fa46649c351de6a2 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Mon, 2 May 2022 11:34:29 -0400 Subject: [PATCH 19/21] add interactive menu --- src/Maker/MakeScaffold.php | 45 +++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/src/Maker/MakeScaffold.php b/src/Maker/MakeScaffold.php index 8a79e14f9..81f9e258b 100644 --- a/src/Maker/MakeScaffold.php +++ b/src/Maker/MakeScaffold.php @@ -100,12 +100,47 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma return; } - $availableScaffolds = array_combine( - array_keys($this->availableScaffolds()), - array_map(fn (array $scaffold) => $scaffold['description'], $this->availableScaffolds()) - ); + $scaffolds = $this->availableScaffolds(); + + while (true) { + $name = $io->choice('Available scaffolds', array_combine( + array_keys($scaffolds), + array_map(fn (array $scaffold) => $scaffold['description'], $scaffolds) + )); + $scaffold = $scaffolds[$name]; + + $io->title($name); + $io->text($scaffold['description']); + $io->newLine(); + + if ($scaffold['dependents'] ?? []) { + $io->text('This scaffold will also install the following scaffolds:'); + $io->newLine(); + $io->listing(\array_map(fn($s) => \sprintf('%s - %s', $s, $scaffolds[$s]['description']), $scaffold['dependents'])); + } + + if ($scaffold['packages'] ?? []) { + $io->text('This scaffold will install the following composer packages:'); + $io->newLine(); + $io->listing(\array_keys($scaffold['packages'])); + } + + if ($scaffold['js_packages'] ?? []) { + $io->text('This scaffold will install the following node packages:'); + $io->newLine(); + $io->listing(\array_keys($scaffold['js_packages'])); + } + + if (!$io->confirm("Would your like to create the \"{$name}\" scaffold?")) { + $io->text('Going back to main menu...'); - $input->setArgument('name', [$io->choice('Available scaffolds', $availableScaffolds)]); + continue; + } + + $input->setArgument('name', [$name]); + + return; + } } private function generateScaffold(string $name, ConsoleStyle $io): void From 0fae83e9925ae11503170d20c3aaf22ef12ab1e6 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Mon, 2 May 2022 13:30:40 -0400 Subject: [PATCH 20/21] add deprecation notes to make:user/make:reset-password/make:registration-form --- src/Maker/MakeRegistrationForm.php | 5 +++++ src/Maker/MakeResetPassword.php | 5 +++++ src/Maker/MakeUser.php | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/src/Maker/MakeRegistrationForm.php b/src/Maker/MakeRegistrationForm.php index 55bba0ec8..31efc04ac 100644 --- a/src/Maker/MakeRegistrationForm.php +++ b/src/Maker/MakeRegistrationForm.php @@ -42,6 +42,7 @@ use Symfony\Component\Form\Extension\Core\Type\PasswordType; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mime\Address; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; @@ -112,6 +113,10 @@ public function configureCommand(Command $command, InputConfiguration $inputConf public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void { + if (Kernel::VERSION_ID >= 60000) { + $io->warning('make:registration-form is deprecated in favor of "make:scaffold register".'); + } + $interactiveSecurityHelper = new InteractiveSecurityHelper(); if (!$this->fileManager->fileExists($path = 'config/packages/security.yaml')) { diff --git a/src/Maker/MakeResetPassword.php b/src/Maker/MakeResetPassword.php index b84a75977..c41c1a0de 100644 --- a/src/Maker/MakeResetPassword.php +++ b/src/Maker/MakeResetPassword.php @@ -39,6 +39,7 @@ use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mime\Address; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; @@ -127,6 +128,10 @@ public function configureDependencies(DependencyBuilder $dependencies): void public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void { + if (Kernel::VERSION_ID >= 60000) { + $io->warning('make:reset-password is deprecated in favor or "make:scaffold reset-password".'); + } + $io->title('Let\'s make a password reset feature!'); $interactiveSecurityHelper = new InteractiveSecurityHelper(); diff --git a/src/Maker/MakeUser.php b/src/Maker/MakeUser.php index 06ad85f57..4267ae659 100644 --- a/src/Maker/MakeUser.php +++ b/src/Maker/MakeUser.php @@ -32,6 +32,7 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Security\Core\Exception\UserNotFoundException; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; @@ -88,6 +89,10 @@ public function configureCommand(Command $command, InputConfiguration $inputConf public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void { + if (Kernel::VERSION_ID >= 60000) { + $io->warning('make:user is deprecated in favor of "make:scaffold user".'); + } + if (null === $input->getArgument('name')) { $name = $io->ask( $command->getDefinition()->getArgument('name')->getDescription(), From c98e95e8a85f548b1485beca7675b0e312806daf Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Mon, 2 May 2022 13:39:12 -0400 Subject: [PATCH 21/21] cs fixes --- src/JsPackageManager.php | 2 +- src/Maker/MakeScaffold.php | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/JsPackageManager.php b/src/JsPackageManager.php index f95b80edc..62cac933c 100644 --- a/src/JsPackageManager.php +++ b/src/JsPackageManager.php @@ -31,7 +31,7 @@ public function __construct(FileManager $fileManager) public function isInstalled(string $package): bool { $packageJson = $this->packageJson(); - $deps = \array_merge($packageJson['dependencies'] ?? [], $packageJson['devDependencies'] ?? []); + $deps = array_merge($packageJson['dependencies'] ?? [], $packageJson['devDependencies'] ?? []); return \array_key_exists($package, $deps); } diff --git a/src/Maker/MakeScaffold.php b/src/Maker/MakeScaffold.php index 81f9e258b..8bfa3c296 100644 --- a/src/Maker/MakeScaffold.php +++ b/src/Maker/MakeScaffold.php @@ -105,7 +105,12 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma while (true) { $name = $io->choice('Available scaffolds', array_combine( array_keys($scaffolds), - array_map(fn (array $scaffold) => $scaffold['description'], $scaffolds) + array_map( + function (array $scaffold) { + return $scaffold['description']; + }, + $scaffolds + ) )); $scaffold = $scaffolds[$name]; @@ -116,19 +121,24 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma if ($scaffold['dependents'] ?? []) { $io->text('This scaffold will also install the following scaffolds:'); $io->newLine(); - $io->listing(\array_map(fn($s) => \sprintf('%s - %s', $s, $scaffolds[$s]['description']), $scaffold['dependents'])); + $io->listing(array_map( + function ($s) use ($scaffolds) { + return sprintf('%s - %s', $s, $scaffolds[$s]['description']); + }, + $scaffold['dependents']) + ); } if ($scaffold['packages'] ?? []) { $io->text('This scaffold will install the following composer packages:'); $io->newLine(); - $io->listing(\array_keys($scaffold['packages'])); + $io->listing(array_keys($scaffold['packages'])); } if ($scaffold['js_packages'] ?? []) { $io->text('This scaffold will install the following node packages:'); $io->newLine(); - $io->listing(\array_keys($scaffold['js_packages'])); + $io->listing(array_keys($scaffold['js_packages'])); } if (!$io->confirm("Would your like to create the \"{$name}\" scaffold?")) {