Skip to content
Snippets Groups Projects
Commit b8b638a5 authored by Fabrice Gangler's avatar Fabrice Gangler :art:
Browse files

feat(signup): add controller + tests (v1)

parent 1221d0fe
No related branches found
No related tags found
No related merge requests found
Pipeline #91312 failed
<?php
/*
* This file is part of the Comptoir-du-Libre software.
* <https://gitlab.adullact.net/Comptoir/comptoir-du-libre>
*
* Copyright (c) ADULLACT <https://adullact.org>
* Association des Développeurs et Utilisateurs de Logiciels Libres
* pour les Administrations et les Collectivités Territoriales
*
* Comptoir-du-Libre is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* You should have received a copy of the GNU Affero General Public License
* along with this software. If not, see <https://www.gnu.org/licenses/agpl-3.0.en.html>.
*/
declare(strict_types=1);
namespace App\Controller\User;
use App\Entity\User;
use App\Form\User\SignupFormType;
use App\Repository\UserRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
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\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
#[\Symfony\Component\Routing\Attribute\Route(
requirements: [
'_locale' => 'en|fr',
],
)]
class SignupController extends AbstractController
{
#[Route(
path: '/{_locale}/account/signup',
name: 'app_account_signup_init',
methods: ['GET']
)]
public function signupInit(
Request $request,
TokenGeneratorInterface $tokenGenerator,
AuthorizationCheckerInterface $authChecker,
): RedirectResponse {
if (true === $authChecker->isGranted('ROLE_USER')) {
return $this->redirectToRoute(route: 'app_user_account', status: Response::HTTP_SEE_OTHER);
}
$session = $request->getSession();
if ($session->has('signup_url_token') === true) {
$signupAntispamToken = $session->get('signup_url_token');
} else {
$signupAntispamToken = $tokenGenerator->generateToken();
$session->set('signup_url_token', $signupAntispamToken);
$session->set('signup_form_token', $tokenGenerator->generateToken());
}
return $this->redirectToRoute(
route: 'app_account_signup',
parameters: ['signupAntispamToken' => $signupAntispamToken],
status: Response::HTTP_SEE_OTHER
);
}
#[Route(
path: '/{_locale}/account/signup/t/{signupAntispamToken}',
name: 'app_account_signup',
methods: ['GET', 'POST']
)]
public function signup(
string $signupAntispamToken,
Request $request,
FormFactoryInterface $formFactory,
UserRepository $userRepository,
UserPasswordHasherInterface $userPasswordHasher,
TranslatorInterface $translator,
AuthorizationCheckerInterface $authChecker,
): Response|RedirectResponse {
if (true === $authChecker->isGranted('ROLE_USER')) {
return $this->redirectToRoute(route: 'app_user_account', status: Response::HTTP_SEE_OTHER);
}
// Check session signup_*_token
$session = $request->getSession();
$validTokenInbUrl = true;
if ($session->has('signup_url_token') === false) {
$validTokenInbUrl = false;
} elseif ($session->get('signup_url_token') !== $signupAntispamToken) {
$validTokenInbUrl = false;
$session->remove('signup_url_token');
$session->remove('signup_form_token');
}
// If signup_url_token is not valid
if ($validTokenInbUrl === false) {
return $this->redirectToRoute(route: 'app_account_signup_init', status: Response::HTTP_SEE_OTHER);
}
// Generate form
$signupFormToken = $session->get('signup_form_token');
$minPasswordLength = $this->getParameter('app.user_config.min_password_length');
$form = $formFactory->createNamed(
name: "signup_$signupFormToken",
type: SignupFormType::class,
options: ['min_password_length' => $minPasswordLength]
);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$userEmail = $form->get('login')->getData();
// Check if the user already exists in the database
$user = $userRepository->findOneByEmail($form->get('login')->getData());
if ($user instanceof User) { // TODO improve this case
$session->remove('signup_url_token'); // Cleanup session data
$session->remove('signup_form_token'); // Cleanup session data
$errorMsgId = 'signup.error.account_already_exists';
$this->addFlash('danger', $translator->trans("$errorMsgId"));
return $this->redirectToRoute(route: 'app_account_login', status: Response::HTTP_SEE_OTHER);
}
// Add new user
$now = new \DateTimeImmutable();
$user = new User();
$user->setUpdatedAt($now);
$user->setCreatedAt($now);
$user->setLastPassowrdUpdate($now);
$user->setEmail($userEmail);
$user->setPassword($userPasswordHasher->hashPassword($user, $form->get('plainPassword')->getData()));
$userRepository->save($user, true); // TODO add new User property [ enabled account ]
// Cleanup session data
$session->remove('signup_url_token');
$session->remove('signup_form_token');
// Redirect user and display success message
$this->addFlash('success', $translator->trans('signup.success'));
return $this->redirectToRoute('app_account_login', [], Response::HTTP_SEE_OTHER);
}
// Display registration form (first display or user errors in form)
return $this->render('user/signup.html.twig', ['signupForm' => $form->createView(),]);
}
}
<?php
/*
* This file is part of the Comptoir-du-Libre software.
* <https://gitlab.adullact.net/Comptoir/comptoir-du-libre>
*
* Copyright (c) ADULLACT <https://adullact.org>
* Association des Développeurs et Utilisateurs de Logiciels Libres
* pour les Administrations et les Collectivités Territoriales
*
* Comptoir-du-Libre is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* You should have received a copy of the GNU Affero General Public License
* along with this software. If not, see <https://www.gnu.org/licenses/agpl-3.0.en.html>.
*/
declare(strict_types=1);
namespace App\Tests\Functional\User;
use App\Repository\UserRepository;
use App\Tests\Functional\TestHelperBreadcrumbTrait;
use App\Tests\Functional\TestHelperFormTrait;
use App\Tests\Functional\TestHelperTrait;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\HttpFoundation\Response;
/**
* @group gogogo
*/
class FunctionalTestCreateAccountTest extends WebTestCase
{
use TestHelperTrait;
use TestHelperFormTrait;
use TestHelperBreadcrumbTrait;
private int $minPasswordLength;
private string $userEmailWhoNeverAddedToDatabase;
protected function setUp(): void
{
$this->userEmailWhoNeverAddedToDatabase = 'please-do-no-add-to-database@example.org';
$this->minPasswordLength = (int) $_ENV['WEBAPP_USER_CONFIG_MIN_PASSWORD_LENGTH'];
}
/**
* Check user account haas not been created
*/
private function checkUserAccountHasNotBeenCreated(
string $userEmail,
): void {
$userRepository = static::getContainer()->get(UserRepository::class);
$this->assertNull($userRepository->findOneByEmail($userEmail));
$this->assertNull($this->getMailerEvent(0)); // check no email was send to end user
}
/**
* Get valid data for SignUp form
*
* @return string[]
*/
private function getValidDataForSignupForm(
string $htmlFormName,
string $userEmail,
): array {
$email = $userEmail;
$password = '0123456789_ABCD';
return [
$htmlFormName . '[email]' => "$email",
$htmlFormName . '[plainPassword][first]' => "$password",
$htmlFormName . '[plainPassword][second]' => "$password",
];
}
/**
* Send SignUp form request, with wrong data
*/
private function sendRequestSignupFormWithWrongData(
array $formDataWithWrongData,
KernelBrowser $kernelBrowser
): Crawler {
$crawler = $kernelBrowser->submitForm(
button: 'public_signupForm_submit',
fieldValues: $formDataWithWrongData
);
$this->assertRouteSame("app_account_signup");
$this->assertResponseStatusCodeSame(Response::HTTP_OK); // HTTP status code = 200
$this->assertSelectorTextSame('h1', 'Create your account');
// $this->assertSelectorTextSame('div.alert-danger', 'The form contains some errors, as described below.');
return $crawler;
}
/**
* Load an empty SignUp form
* and if necessary test all form fields
*/
private function loadEmptySignUpForm(
bool $enableAssertions = false,
): KernelBrowser {
$locale = 'en';
$kernelBrowser = static::createClient();
$crawler = $kernelBrowser->request('GET', "/$locale/account/signup");
$session = $kernelBrowser->getRequest()->getSession();
$sessionSignupUrlToken = $session->get('signup_url_token');
$sessionSignupFormToken = $session->get('signup_form_token');
$realFormUrl = "/$locale/account/signup/t/$sessionSignupUrlToken";
$htmlFormName = "signup_$sessionSignupFormToken";
if ($enableAssertions === true) {
$this->assertRouteSame("app_account_signup_init");
$this->assertNotNull($sessionSignupUrlToken);
$this->assertNotNull($sessionSignupFormToken);
$this->assertResponseStatusCodeSame(
expectedCode: Response::HTTP_SEE_OTHER,
message: 'Bad HTTP code response'
);
$this->assertResponseHeaderSame(
headerName: "Location",
expectedValue: "$realFormUrl",
message: "Bad redirect location. Tested URL: /$locale/account/signup"
);
}
$crawler = $kernelBrowser->request('GET', "$realFormUrl");
if ($enableAssertions === true) {
$this->assertRouteSame("app_account_signup");
$this->assertResponseStatusCodeSame(Response::HTTP_OK); // HTTP status code = 200
$this->assertSelectorTextSame('h1', 'Create your account');
// Test all form fields
$this->checkAttribute(crawler: $crawler, cssFilter: "form[name=$htmlFormName]", attributesExpected: []);
$minPasswordLength = $this->minPasswordLength;
$this->checkFormField(
crawler: $crawler,
htmlFormName: $htmlFormName,
fieldIdsuffix: 'email', // see: HONEYPOT_FIELD_NAME
fieldAttributes: [
'type' => 'email',
'tabindex' => '-1',
'aria-hidden' => 'true',
'class' => 'visually-hidden form-control',
],
notAllowedFieldAttributes: ['required',],
labelText: 'Email address',
);
$this->checkFormField(
crawler: $crawler,
htmlFormName: $htmlFormName,
fieldIdsuffix: 'login', // see: EMAIL_FIELD_NAME
fieldAttributes: [
'type' => 'email',
'required' => 'required'
],
labelText: 'Email address',
helpText: 'Enter your email address, and we will send you a link to activate your account.'
);
$this->checkFormField(
crawler: $crawler,
htmlFormName: $htmlFormName,
fieldIdsuffix: 'plainPassword_first',
fieldAttributes: [
'type' => 'password',
'required' => 'required',
'autocomplete' => 'new-password',
'minlength' => "$minPasswordLength",
'maxlength' => "4096",
],
labelText: 'Your password',
helpText: "Please enter your password. It should be at least $minPasswordLength characters."
);
$this->checkFormField(
crawler: $crawler,
htmlFormName: $htmlFormName,
fieldIdsuffix: 'plainPassword_second',
fieldAttributes: [
'type' => 'password',
'required' => 'required',
'autocomplete' => 'new-password',
'minlength' => "$minPasswordLength",
'maxlength' => "4096",
],
labelText: 'Repeat password',
helpText: 'Please repeat your password.'
);
$this->checkAttribute(
crawler: $crawler,
cssFilter: "button#public_signupForm_submit",
attributesExpected: ['_text' => 'Create your account'],
);
// HTML content checks breadcrumb
$breadcrumb = [ '/en/account/signup' => 'Sign up',];
$this->checkHasValidBreadcrumb($crawler, $breadcrumb, "en");
}
return $kernelBrowser;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* @group Form
* @group CreateAccount
* @group CreateAccount_Form
*/
public function testAnonymousUserIsRedirectedToSignUpPageWithSessionTokenInUrl(): void
{
$locale = 'en';
$kernelBrowser = static::createClient();
$crawler = $kernelBrowser->request('GET', "/$locale/account/signup");
$session = $kernelBrowser->getRequest()->getSession();
$sessionSignupUrlToken = $session->get('signup_url_token');
$sessionSignupFormToken = $session->get('signup_form_token');
$this->assertNotNull($sessionSignupUrlToken);
$this->assertNotNull($sessionSignupFormToken);
$this->assertResponseStatusCodeSame(expectedCode: Response::HTTP_SEE_OTHER, message: 'Bad HTTP code response');
$this->assertResponseHeaderSame(
headerName: "Location",
expectedValue: "/$locale/account/signup/t/$sessionSignupUrlToken",
message: "Bad redirect location. Tested URL: /$locale/account/signup"
);
}
/**
* @group Form
* @group CreateAccount
* @group CreateAccount_Form
* @group NoConnectedUser
* @group NoConnectedUser_SignUp
*/
public function testAnonymousUserCanDisplaySignUpForm(): void
{
$this->loadEmptySignUpForm(enableAssertions: true);
}
/////////////////////////////////////////////////////////////////////////
/**
* @group Form
* @group CreateAccount
* @group CreateAccount_Form
* @group NoConnectedUser
* @group NoConnectedUser_SignUp
* @group NoConnectedUser_SignUp_With_Wrong_Data
*/
public function testAnonymousUserCanNotSendSignUpFormWithoutEmailField(): void
{
$userEmail = $this->userEmailWhoNeverAddedToDatabase;
$kernelBrowser = $this->loadEmptySignUpForm(enableAssertions: false);
$session = $kernelBrowser->getRequest()->getSession();
$htmlFormName = "signup_" . $session->get('signup_form_token');
$this->commonCheckerWhenFormIsSentWithMissingOrEmptyField(
kernelBrowser: $kernelBrowser,
validFormData: $this->getValidDataForSignupForm($htmlFormName, $userEmail),
formName: "$htmlFormName",
fieldName: 'login', // see: EMAIL_FIELD_NAME
invalidFeedback: 'This field is mandatory',
methodNameToSendFormWithWrongData: 'sendRequestSignupFormWithWrongData'
);
$this->checkUserAccountHasNotBeenCreated($userEmail);
}
/**
* @group Form
* @group CreateAccount
* @group CreateAccount_Form
* @group NoConnectedUser
* @group NoConnectedUser_SignUp
* @group NoConnectedUser_SignUp_With_Wrong_Data
*/
public function testAnonymousUserCanNotSendSignUpFormWithInvalidFormatEmail(): void
{
$userEmail = $this->userEmailWhoNeverAddedToDatabase;
$kernelBrowser = $this->loadEmptySignUpForm(enableAssertions: false);
$session = $kernelBrowser->getRequest()->getSession();
$htmlFormName = "signup_" . $session->get('signup_form_token');
$validFormData = $this->getValidDataForSignupForm($htmlFormName, $userEmail);
$fieldName = 'login'; // see: EMAIL_FIELD_NAME
foreach ($this->getListOfEmailsInInvalidFormat() as $notValidEmail) {
$formDataWithWrongData = $validFormData;
$formDataWithWrongData["${htmlFormName}[${fieldName}]"] = $notValidEmail;
$crawler = $this->sendRequestSignupFormWithWrongData(
formDataWithWrongData: $formDataWithWrongData,
kernelBrowser: $kernelBrowser
);
$this->commonCheckerIfFormFieldIsInvalid(
crawler: $crawler,
cssFilterOfInvadFormField: "#${htmlFormName}_${fieldName}",
cssFilterOfInvalidFeedback: '.invalid-feedback',
invalidFeedback: 'Please enter a valid email address.',
);
$this->checkUserAccountHasNotBeenCreated($userEmail);
}
}
/////////////////////////////////////////////////////////////////////////
/**
* @group Form
* @group CreateAccount
* @group CreateAccount_Form
* @group NoConnectedUser
* @group NoConnectedUser_SignUp
* @group NoConnectedUser_SignUp_With_Wrong_Data
*/
public function testAnonymousUserCanNotSendSignUpFormWithoutSimilarPasswordFields(): void
{
$userEmail = $this->userEmailWhoNeverAddedToDatabase;
$kernelBrowser = $this->loadEmptySignUpForm(enableAssertions: false);
$session = $kernelBrowser->getRequest()->getSession();
$htmlFormName = "signup_" . $session->get('signup_form_token');
$this->commonCheckWhenFormIsSentWithoutSimilarNewPasswordFields(
kernelBrowser: $kernelBrowser,
validFormData: $this->getValidDataForSignupForm($htmlFormName, $userEmail),
formName: "$htmlFormName",
fieldName: 'plainPassword',
minPasswordLength: $this->minPasswordLength,
methodNameToSendFormWithWrongData: 'sendRequestSignupFormWithWrongData',
invalidFeedback: 'The password fields must match.'
);
$this->checkUserAccountHasNotBeenCreated($userEmail);
}
/**
* @group Form
* @group CreateAccount
* @group CreateAccount_Form
* @group NoConnectedUser
* @group NoConnectedUser_SignUp
* @group NoConnectedUser_SignUp_With_Wrong_Data
*/
public function testAnonymousUserCanNotSendSignUpFormWithTooSmallPassword(): void
{
$userEmail = $this->userEmailWhoNeverAddedToDatabase;
$kernelBrowser = $this->loadEmptySignUpForm(enableAssertions: false);
$session = $kernelBrowser->getRequest()->getSession();
$htmlFormName = "signup_" . $session->get('signup_form_token');
$this->commonCheckWhenFormIsSentWithTooSmallRepeatPassword(
kernelBrowser: $kernelBrowser,
validFormData: $this->getValidDataForSignupForm($htmlFormName, $userEmail),
formName: "$htmlFormName",
fieldName: 'plainPassword',
minPasswordLength: $this->minPasswordLength,
methodNameToSendFormWithWrongData: 'sendRequestSignupFormWithWrongData',
);
$this->checkUserAccountHasNotBeenCreated($userEmail);
}
////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment