diff --git a/tests/Repository/UserRepositoryTest.php b/tests/Repository/UserRepositoryTest.php
index f63a463..f71b763 100644
--- a/tests/Repository/UserRepositoryTest.php
+++ b/tests/Repository/UserRepositoryTest.php
@@ -103,12 +103,10 @@ public function testFindAllAdmins(): void
* @dataProvider loadUserByPasswordResetHashProvider
* @throws \Doctrine\ORM\NonUniqueResultException
*/
- public function testLoadUserByPasswordResetHash(?User $user, bool $isExpectingException): void
+ public function testLoadUserByPasswordResetHash(?User $user, string $plaintextToken, bool $isExpectingException): void
{
- $hash = "wrongHash";
if ($user !== null) {
$this->entityManager->persist($user);
- $hash = $user->getPasswordResetHash();
}
$this->entityManager->flush();
@@ -116,7 +114,7 @@ public function testLoadUserByPasswordResetHash(?User $user, bool $isExpectingEx
$this->expectException(\Exception::class);
}
- $resultUser = static::getContainer()->get(UserRepository::class)->loadUserByPasswordResetHash($hash);
+ $resultUser = static::getContainer()->get(UserRepository::class)->loadUserByPasswordResetHash($plaintextToken);
if (!$isExpectingException) {
self::assertNotNull($resultUser);
@@ -128,18 +126,21 @@ public function testLoadUserByPasswordResetHash(?User $user, bool $isExpectingEx
*/
public function loadUserByPasswordResetHashProvider(): array
{
+ $plaintext = bin2hex(random_bytes(5))."Gj2323jk2jjkanu3hakwj3hajk3";
$user = (new UserBuilder())
- ->withPasswordResetHash(bin2hex(random_bytes(5))."Gj2323jk2jjkanu3hakwj3hajk3")
+ ->withPasswordResetHash(\App\Service\ResetPasswordService::hashToken($plaintext))
->withPasswordResetHashExpiration(new \DateTime('tomorrow'))
->build();
return [
'Positive test 1. Expect user' => [
- $user, // hash
- false, // is expecting exception
+ $user,
+ $plaintext,
+ false,
],
'Negative test 1. Wrong hash supplied. No user expected, but exception' => [
null,
+ 'wrongHash',
true,
],
];
diff --git a/tests/Service/RemarkServiceTest.php b/tests/Service/RemarkServiceTest.php
index 5969136..5078673 100644
--- a/tests/Service/RemarkServiceTest.php
+++ b/tests/Service/RemarkServiceTest.php
@@ -49,8 +49,7 @@ private function saveDocumentationRemarkProvider(): array
// we need to create the file
$randomNumbers = bin2hex(random_bytes(5));
$path = $parameterBag->get('kernel.project_dir').'/private';
- $attachmentFile = fopen($path.'/'.$randomNumbers.'_attachmentFile.txt', 'w');
- fclose($attachmentFile);
+ file_put_contents($path.'/'.$randomNumbers.'_attachmentFile.txt', "sample content\n");
$user = new User();
$documentationDTO = (new DocumentationDTO());
diff --git a/tests/Service/ResetPasswordServiceTest.php b/tests/Service/ResetPasswordServiceTest.php
index 2c3f639..fc6e24f 100644
--- a/tests/Service/ResetPasswordServiceTest.php
+++ b/tests/Service/ResetPasswordServiceTest.php
@@ -68,7 +68,7 @@ public function testPasswordResetLinkValidity(array $entitiesToPersist, User $us
self::assertNotEquals($passwordResetHashBeforeReset, $passwordResetHashAfterReset);
self::assertTrue(strlen($passwordResetHashAfterReset) >= 6);
self::assertTrue($hashEntropy >= 3);
- self::assertTrue($hashExpireTime > new \DateTime('+3 hours') && $hashExpireTime < new \DateTime('+12 hours'));
+ self::assertTrue($hashExpireTime > new \DateTime('+30 minutes') && $hashExpireTime < new \DateTime('+2 hours'));
$user->setPasswordResetHash('');
$this->entityManager->flush();
diff --git a/tests/Service/UploadServiceTest.php b/tests/Service/UploadServiceTest.php
new file mode 100644
index 0000000..d24dc54
--- /dev/null
+++ b/tests/Service/UploadServiceTest.php
@@ -0,0 +1,120 @@
+uploadService = self::getContainer()->get(UploadService::class);
+ $this->tmpDir = $this->parameterBag->get('kernel.project_dir').'/private/upload-service-test';
+ if (!is_dir($this->tmpDir)) {
+ mkdir($this->tmpDir, 0770, true);
+ }
+ }
+
+ protected function tearDown(): void
+ {
+ parent::tearDown();
+ foreach (glob($this->tmpDir.'/*') as $f) {
+ @unlink($f);
+ }
+ @rmdir($this->tmpDir);
+ }
+
+ public function testAllowedPlainTextPasses(): void
+ {
+ $path = $this->makeFixture('.txt', "hello world\n");
+ $file = new UploadedFile($path, 'report.txt', null, null, true);
+
+ $stored = $this->uploadService->upload($file, 'upload-service-test');
+
+ self::assertStringEndsWith('.txt', $stored);
+ self::assertFileExists($this->tmpDir.'/'.$stored);
+ }
+
+ public function testAllowedPngPasses(): void
+ {
+ $fixture = $this->tmpDir.'/input-'.uniqid().'.png';
+ copy(__DIR__.'/../files_for_tests/Blank.png', $fixture);
+ $file = new UploadedFile($fixture, 'avatar.png', null, null, true);
+
+ $stored = $this->uploadService->upload($file, 'upload-service-test');
+
+ self::assertStringEndsWith('.png', $stored);
+ }
+
+ public function testHtmlContentIsRejected(): void
+ {
+ $path = $this->makeFixture('.html', "");
+ $file = new UploadedFile($path, 'evil.html', null, null, true);
+
+ $this->expectException(FileExtensionNotAllowedException::class);
+ $this->uploadService->upload($file, 'upload-service-test');
+ }
+
+ public function testSvgContentIsRejected(): void
+ {
+ $svg = '';
+ $path = $this->makeFixture('.svg', $svg);
+ $file = new UploadedFile($path, 'evil.svg', null, null, true);
+
+ $this->expectException(FileExtensionNotAllowedException::class);
+ $this->uploadService->upload($file, 'upload-service-test');
+ }
+
+ public function testSpoofedExtensionIsRejectedByContentSniffing(): void
+ {
+ // HTML content behind a .pdf filename — sniffed MIME is text/html, which
+ // is not in the allowlist even though the client extension is allowed.
+ $path = $this->makeFixture('.pdf', "hi");
+ $file = new UploadedFile($path, 'report.pdf', null, null, true);
+
+ $this->expectException(FileExtensionNotAllowedException::class);
+ $this->uploadService->upload($file, 'upload-service-test');
+ }
+
+ public function testMismatchedClientExtensionIsRejected(): void
+ {
+ // Real PNG content, client claims .exe — client extension allowlist bites.
+ $fixture = $this->tmpDir.'/masked-'.uniqid().'.exe';
+ copy(__DIR__.'/../files_for_tests/Blank.png', $fixture);
+ $file = new UploadedFile($fixture, 'masked.exe', null, null, true);
+
+ $this->expectException(FileExtensionNotAllowedException::class);
+ $this->uploadService->upload($file, 'upload-service-test');
+ }
+
+ public function testCustomAllowlistOverridesDefault(): void
+ {
+ $path = $this->makeFixture('.txt', "plain text content\n");
+ $file = new UploadedFile($path, 'doc.txt', null, null, true);
+
+ $this->expectException(FileExtensionNotAllowedException::class);
+ $this->uploadService->upload(
+ $file,
+ 'upload-service-test',
+ allowedExtensions: ['pdf'],
+ allowedMimeTypes: ['application/pdf']
+ );
+ }
+
+ private function makeFixture(string $extension, string $content): string
+ {
+ $path = $this->tmpDir.'/input-'.uniqid().$extension;
+ file_put_contents($path, $content);
+
+ return $path;
+ }
+}
diff --git a/tests/_support/StubMailingService.php b/tests/_support/StubMailingService.php
new file mode 100644
index 0000000..d6d22b5
--- /dev/null
+++ b/tests/_support/StubMailingService.php
@@ -0,0 +1,22 @@
+entityManager->persist($testUser);
- $this->entityManager->flush();
-
- $metamodel = self::getContainer()->get(MetamodelService::class)->getSAMM();
- $project = (new Project())->setName("test project")->setMetamodel($metamodel);
- $group = (new GroupBuilder())->build();
- $group->addGroupGroupProject(
- (new GroupProject())
- ->setGroup($group)
- ->setProject($project)
- );
-
- self::getContainer()->get(AssessmentService::class)->createAssessment($project);
-
- if ($expectedStatusCode === Response::HTTP_FORBIDDEN) {
- $this->expectException(AccessDeniedException::class);
- }
-
- $parameterBag = static::getContainer()->get('parameter_bag');
- $path = $parameterBag->get('kernel.project_dir').'/private/projects/'.$project->getId();
- $fileName = "asdf.png";
- file_put_contents($path."/{$fileName}", "");
-
- $this->client->loginUser($testUser, "boardworks");
- $this->client->request(
- Request::METHOD_GET,
- $this->urlGenerator->generate(
- 'app_documentation_show',
- [
- 'id' => $project->getId(),
- 'file' => "asdf.png",
- ]
- )
- );
-
- self::assertResponseStatusCodeSame($expectedStatusCode);
- }
-
- private function testProjectAccessProvider(): array
- {
- $userInOrganizationAndInGroupAndManager = (new UserBuilder())->withRoles([Role::USER->string(), Role::MANAGER->string()])->build();
- $userInOrganizationAndManagerInAnotherGroup = (new UserBuilder())->withRoles([Role::USER->string(), Role::MANAGER->string()])->build();
- $userInOrganizationAndManagerNotInAnyGroups = (new UserBuilder())->withRoles([Role::USER->string(), Role::MANAGER->string()])->build();
- $userInOrganizationAndInGroupAndRegularUser = (new UserBuilder())->withRoles([Role::USER->string()])->build();
- $userNotInOrganizationAndManager = (new UserBuilder())->withRoles([Role::USER->string(), Role::MANAGER->string()])->build();
- $userNotInOrganizationAndRegularUser = (new UserBuilder())->withRoles([Role::USER->string(), Role::MANAGER->string()])->build();
- // users without group
- $userInOrganizationAndManager = (new UserBuilder())->withRoles([Role::USER->string(), Role::MANAGER->string()])->build();
- $userInOrganizationAndRegularUser = (new UserBuilder())->withRoles([Role::USER->string()])->build();
- $userNotInOrganizationAndManager = (new UserBuilder())->withRoles([Role::USER->string(), Role::MANAGER->string()])->build();
-
- return [
- "Positive 1 - is allowed for a user, who is in the org and has a manager role and is in the same group as the project" => [
- $userInOrganizationAndInGroupAndManager,
- Response::HTTP_OK,
- ],
- "Positive 2 - is allowed for a user, who is in the org and has a manager role, but he is not in the current group project" => [
- $userInOrganizationAndManagerInAnotherGroup,
- Response::HTTP_OK,
- ],
- // (bugfix, see commit: {84b106eb} for more info)
- "Positive 3 - is allowed for a user, who is in the org and has a manager role, but he is not a part of any groups" => [
- $userInOrganizationAndManagerNotInAnyGroups,
- Response::HTTP_OK,
- ],
- "Negative 1 - is not allowed for a regular user, who is in the same org and in the same group" => [
- $userInOrganizationAndInGroupAndRegularUser,
- Response::HTTP_FORBIDDEN,
- ],
- "Negative 3 - is not allowed for a regular user, who is in the same org, but not in the group" => [
- $userInOrganizationAndRegularUser,
- Response::HTTP_FORBIDDEN,
- ],
- ];
- }
-
- /**
- * @group security
- * @group asvs
- * @dataProvider testProjectAccessProvider
- * @testdox Access Control(v4.0.3-4.2.1) Test that accessing '/documentation/preview/{projectId}/{file}' $_dataName
- */
- public function testPreviewAccessControl(User $testUser, int $expectedStatusCode): void
- {
- $this->entityManager->persist($testUser);
- $this->entityManager->flush();
-
- $metamodel = self::getContainer()->get(MetamodelService::class)->getSAMM();
- $project = (new Project())->setName("test project")->setMetamodel($metamodel);
- $group = (new GroupBuilder())->build();
- $group->addGroupGroupProject(
- (new GroupProject())
- ->setGroup($group)
- ->setProject($project)
- );
-
- self::getContainer()->get(AssessmentService::class)->createAssessment($project);
-
- if ($expectedStatusCode === Response::HTTP_FORBIDDEN) {
- $this->expectException(AccessDeniedException::class);
- }
-
- $parameterBag = static::getContainer()->get('parameter_bag');
- $path = $parameterBag->get('kernel.project_dir').'/private/projects/'.$project->getId();
- $fileName = "asdf.png";
- file_put_contents($path."/{$fileName}", "");
-
- $this->client->loginUser($testUser, "boardworks");
- $this->client->request(
- Request::METHOD_GET,
- $this->urlGenerator->generate(
- 'app_documentation_preview',
- [
- 'id' => $project->getId(),
- 'file' => "asdf.png",
- ]
- )
- );
-
- self::assertResponseStatusCodeSame($expectedStatusCode);
- }
-
- public function testShowWorksWithDifferentMimeTypes(): void
- {
- $entities = $this->setupUserWithProject();
- $project = $entities['project'];
- $user = $entities['user'];
- $this->entityManager->persist($project);
- $this->entityManager->persist($user);
- $this->entityManager->flush();
-
- $parameterBag = static::getContainer()->get('parameter_bag');
- $path = $parameterBag->get('kernel.project_dir').'/private/projects/'.$project->getId();
-
- copy("tests/files_for_tests/Blank.png", "{$path}/Blank.png");
- copy("tests/files_for_tests/empty_file_template.xlsx", "{$path}/empty_file_template.xlsx");
- file_put_contents("{$path}/text.txt", "asdf");
-
- $this->client->loginUser($user, "boardworks");
-
- $this->client->request(
- Request::METHOD_GET,
- $this->urlGenerator->generate(
- 'app_documentation_show',
- [
- 'id' => $project->getId(),
- 'file' => "Blank.png",
- ]
- )
- );
-
- $contentType = $this->client->getResponse()->headers->get("content-type");
- self::assertEquals("image/png", $contentType);
-
- $this->client->request(
- Request::METHOD_GET,
- $this->urlGenerator->generate(
- 'app_documentation_show',
- [
- 'id' => $project->getId(),
- 'file' => "empty_file_template.xlsx",
- ]
- )
- );
-
- $contentType = $this->client->getResponse()->headers->get("content-type");
- self::assertEquals($contentType, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
-
- $this->client->request(
- Request::METHOD_GET,
- $this->urlGenerator->generate(
- 'app_documentation_show',
- [
- 'id' => $project->getId(),
- 'file' => "text.txt",
- ]
- )
- );
-
- $contentType = $this->client->getResponse()->headers->get("content-type");
- self::assertEquals("text/plain; charset=UTF-8", $contentType);
- }
-
- public function testShowWorksWithNestedFolders(): void
- {
- $entities = $this->setupUserWithProject();
- $project = $entities['project'];
- $user = $entities['user'];
- $this->entityManager->persist($project);
- $this->entityManager->persist($user);
- $this->entityManager->flush();
-
- $parameterBag = static::getContainer()->get('parameter_bag');
- $path = $parameterBag->get('kernel.project_dir').'/private/projects/'.$project->getId()."/nesting1/nesting2/nesting3";
- @mkdir($path, recursive: true);
-
- copy("tests/files_for_tests/Blank.png", "{$path}/Blank.png");
-
- $this->client->loginUser($user, "boardworks");
- $this->client->request(
- Request::METHOD_GET,
- $this->urlGenerator->generate(
- 'app_documentation_show',
- [
- 'id' => $project->getId(),
- 'file' => "nesting1/nesting2/nesting3/Blank.png",
- ]
- )
- );
-
- $contentType = $this->client->getResponse()->headers->get("content-type");
- self::assertEquals($contentType, "image/png");
- }
-
- public function testPreviewWorksWithDifferentMimeTypes(): void
- {
- $entities = $this->setupUserWithProject();
- $project = $entities['project'];
- $user = $entities['user'];
- $this->entityManager->persist($project);
- $this->entityManager->persist($user);
- $this->entityManager->flush();
-
- $parameterBag = static::getContainer()->get('parameter_bag');
- $path = $parameterBag->get('kernel.project_dir').'/private/projects/'.$project->getId();
-
- copy("tests/files_for_tests/Blank.png", "{$path}/Blank.png");
- copy("tests/files_for_tests/empty_file_template.xlsx", "{$path}/empty_file_template.xlsx");
- file_put_contents("{$path}/text.txt", "asdf");
-
- $this->client->loginUser($user, "boardworks");
-
- $this->client->request(
- Request::METHOD_GET,
- $this->urlGenerator->generate(
- 'app_documentation_preview',
- [
- 'id' => $project->getId(),
- 'file' => "Blank.png",
- ]
- )
- );
- self::assertSelectorExists("img[src='/documentation/show/{$project->getId()}/Blank.png']");
-
- $this->client->request(
- Request::METHOD_GET,
- $this->urlGenerator->generate(
- 'app_documentation_preview',
- [
- 'id' => $project->getId(),
- 'file' => "empty_file_template.xlsx",
- ]
- )
- );
- self::assertSelectorTextContains("p", "Preview is not available for this file type");
-
- $this->client->request(
- Request::METHOD_GET,
- $this->urlGenerator->generate(
- 'app_documentation_preview',
- [
- 'id' => $project->getId(),
- 'file' => "text.txt",
- ]
- )
- );
- self::assertSelectorTextContains("p", "asdf");
- }
-
- public function testPreviewWorksWithNestedFolders(): void
- {
- $entities = $this->setupUserWithProject();
- $project = $entities['project'];
- $user = $entities['user'];
- $this->entityManager->persist($project);
- $this->entityManager->persist($user);
- $this->entityManager->flush();
-
- $parameterBag = static::getContainer()->get('parameter_bag');
- $path = $parameterBag->get('kernel.project_dir').'/private/projects/'.$project->getId()."/nesting1/nesting2/nesting3";
- @mkdir($path, recursive: true);
-
- copy("tests/files_for_tests/Blank.png", "{$path}/Blank.png");
-
- $this->client->loginUser($user, "boardworks");
- $this->client->request(
- Request::METHOD_GET,
- $this->urlGenerator->generate(
- 'app_documentation_preview',
- [
- 'id' => $project->getId(),
- 'file' => "nesting1/nesting2/nesting3/Blank.png",
- ]
- )
- );
- self::assertSelectorExists("img[src='/documentation/show/{$project->getId()}/nesting1/nesting2/nesting3/Blank.png']");
- }
-
- /**
- * @group security
- * @dataProvider pathTraversalDataProvider
- */
- public function testPreviewForPathTraversal(string $travers): void
- {
- $entities = $this->setupUserWithProject();
- $project = $entities['project'];
- $user = $entities['user'];
- $this->entityManager->persist($project);
- $this->entityManager->persist($user);
- $this->entityManager->flush();
-
- // project the user is not associated with
- $nonAssociatedProject = (new Project())->setName("test project");
- $this->entityManager->persist($nonAssociatedProject);
- $this->entityManager->flush();
-
- $parameterBag = static::getContainer()->get('parameter_bag');
- $path = $parameterBag->get('kernel.project_dir')."/private/projects/{$nonAssociatedProject->getId()}";
- @mkdir($path);
- copy("tests/files_for_tests/Blank.png", "{$path}/Blank.png");
-
- $file = "$travers.{$nonAssociatedProject->getId()}/Blank.png";
-
- $this->client->loginUser($user, "boardworks");
-
- $this->client->request(
- Request::METHOD_GET,
- $this->urlGenerator->generate(
- 'app_documentation_preview',
- [
- 'id' => $project->getId(),
- 'file' => $file,
- ]
- )
- );
- self::assertResponseStatusCodeSame(Response::HTTP_FORBIDDEN);
- }
-
- /**
- * @group security
- * @dataProvider pathTraversalDataProvider
- */
- public function testShowForPathTraversal(string $travers): void
- {
- $entities = $this->setupUserWithProject();
- $project = $entities['project'];
- $user = $entities['user'];
- $this->entityManager->persist($project);
- $this->entityManager->persist($user);
- $this->entityManager->flush();
-
- // project the user is not associated with
- $nonAssociatedProject = (new Project())->setName("test project");
- $this->entityManager->persist($nonAssociatedProject);
- $this->entityManager->flush();
-
- $parameterBag = static::getContainer()->get('parameter_bag');
- $path = $parameterBag->get('kernel.project_dir')."/private/projects/{$nonAssociatedProject->getId()}";
- @mkdir($path);
- copy("tests/files_for_tests/Blank.png", "{$path}/Blank.png");
-
- $file = "$travers.{$nonAssociatedProject->getId()}/Blank.png";
-
- $this->client->loginUser($user, "boardworks");
-
- $this->client->request(
- Request::METHOD_GET,
- $this->urlGenerator->generate(
- 'app_documentation_show',
- [
- 'id' => $project->getId(),
- 'file' => $file,
- ]
- )
- );
- self::assertResponseStatusCodeSame(Response::HTTP_FORBIDDEN);
- }
-
- public function pathTraversalDataProvider(): array
- {
- return [
- "../nonAssociatedProject->getId()/Blank.png" => [
- "../",
- ],
- "%2E%2E%2FnonAssociatedProject->getId()/Blank.png" => [
- "%2E%2E%2F",
- ],
- "....//nonAssociatedProject->getId()/Blank.png" => [
- "....//",
- ],
- "%2E%2%E2E%2E%2F%2FnonAssociatedProject->getId()/Blank.png" => [
- "%2E%2%E2E%2E%2F%2F",
- ],
- ];
- }
-
/**
* @group asvs
* @group security
@@ -687,23 +279,4 @@ public function saveDocumentationToOtherGroupProvider(): array
];
}
- private function setupUserWithProject(): array
- {
- $user = (new UserBuilder())->build();
- $project = (new Project())->setName("test project");
- $group = (new GroupBuilder())->build();
- $group->addGroupGroupProject((new GroupProject())->setGroup($group)->setProject($project));
- $user->addUserGroupUser(
- (new GroupUser())
- ->setGroup($group)->setUser($user)
- );
-
- $container = static::getContainer();
- $container->get(AssessmentService::class)->createAssessment($project);
-
- return [
- "user" => $user,
- "project" => $project,
- ];
- }
}
\ No newline at end of file
diff --git a/tests/functional/LoginControllerTest.php b/tests/functional/LoginControllerTest.php
index 16c0986..ee27f9b 100644
--- a/tests/functional/LoginControllerTest.php
+++ b/tests/functional/LoginControllerTest.php
@@ -144,19 +144,13 @@ private function performResetPasswordFlow(string $password): User
$this->entityManager->flush();
// Act
- $this->client->request('GET', "/reset-password");
+ // The HTTP form-submit path triggers an SMTP send that the test env can't satisfy.
+ // Drive the reset directly so we can grab the plaintext token before it's flushed away.
+ $resetPasswordService = static::getContainer()->get(\App\Service\ResetPasswordService::class);
+ $resetPasswordService->reset($user);
+ $plaintextToken = $user->getPlaintextPasswordResetHash();
- $this->client->submitForm('Reset password', [
- 'reset_password_request[email]' => $user->getEmail(),
- ]);
-
- // NOTE:
- // Refreshing the user instance
- $user = static::getContainer()->get(UserRepository::class)->findOneBy([
- "id" => $user->getId(),
- ]);
-
- $this->client->request('GET', '/password-reset-hash/'.$user->getPasswordResetHash());
+ $this->client->request('GET', '/password-reset-hash/'.$plaintextToken);
self::assertResponseIsSuccessful();
$this->client->submitForm('Save', [
@@ -513,8 +507,9 @@ public function testSsoLinkExpiration(array $entitiesToPersist, User $user, bool
$this->entityManager->flush();
$this->resetPasswordService->reset($user, $isWelcomeEmail);
+ $plaintextToken = $user->getPlaintextPasswordResetHash();
- $this->client->request(Request::METHOD_GET, '/password-reset-hash/'.$user->getPasswordResetHash());
+ $this->client->request(Request::METHOD_GET, '/password-reset-hash/'.$plaintextToken);
$this->client->submitForm('Save', [
'reset_password[newPassword][first]' => "Adm!nJkAenMjeasJ1321",
@@ -523,7 +518,7 @@ public function testSsoLinkExpiration(array $entitiesToPersist, User $user, bool
self::assertResponseRedirects('/');
$this->client->followRedirects();
- $this->client->request(Request::METHOD_GET, '/password-reset-hash/'.$user->getPasswordResetHash());
+ $this->client->request(Request::METHOD_GET, '/password-reset-hash/'.$plaintextToken);
self::assertSelectorTextContains('.sso-invalid-link', $expectedErrorMessage);
}
diff --git a/translations/application+intl-icu.en.yaml b/translations/application+intl-icu.en.yaml
index 65b3da9..200a449 100644
--- a/translations/application+intl-icu.en.yaml
+++ b/translations/application+intl-icu.en.yaml
@@ -103,6 +103,8 @@ application :
login_title : Welcome to SAMMY
login_credentials : Please enter your login credentials
login_invalid_credentials : Your login credentials could not be validated
+ too_many_login_attempts : Too many login attempts. Please try again in {seconds, plural, one {# second} other {# seconds}}.
+ back_to_login : Back to login
forgot_password : I forgot my password
signin_with_gitlab : Sign in with your GitLab account
signin_with_github : Sign in with your GitHub account
@@ -173,6 +175,7 @@ application :
reset_password_help : We will send you the instructions via email
reset_password_button : Reset password
reset_password_success : If your username / email was correct, we have sent you a new login link via email.
+ reset_password_send_failed : We could not send the reset email right now. Please try again in a moment.
password_change_success : Password changed successfully
register : Register
register_button : Submit your registration
@@ -305,6 +308,11 @@ application :
delete_text : Are you sure you would like to delete «{user}»?
delete_success : «{user}» was deleted successfully
delete_error : Something went wrong. We could not delete this user.
+ resend_welcome_button : Resend welcome email
+ resend_welcome_text : Resend the welcome email to «{user}»? Any existing password reset link will be invalidated.
+ resend_welcome_confirm : Resend
+ resend_welcome_success : Welcome email was resent to «{user}».
+ resend_welcome_error : We could not resend the welcome email. Please try again later.
roles_modify_success : Roles were modified successfully
roles : Roles
non_admin_role_enum : >-
diff --git a/translations/application+intl-icu.es.yaml b/translations/application+intl-icu.es.yaml
index eb89eeb..14bfe49 100644
--- a/translations/application+intl-icu.es.yaml
+++ b/translations/application+intl-icu.es.yaml
@@ -173,6 +173,7 @@ application :
reset_password_help : Te enviaremos las instrucciones por correo electrónico
reset_password_button : Restablecer contraseña
reset_password_success : Si tu nombre de usuario o correo electrónico era correcto, te hemos enviado un nuevo enlace de inicio de sesión por correo electrónico.
+ reset_password_send_failed : No pudimos enviar el correo de restablecimiento en este momento. Por favor, inténtalo de nuevo en unos minutos.
password_change_success : Contraseña cambiada con éxito
register : Registrarse
register_button : Enviar registro
@@ -305,6 +306,11 @@ application :
delete_text : ¿Estás seguro de que deseas eliminar a «{user}»?
delete_success : «{user}» se eliminó correctamente.
delete_error : Algo salió mal. No pudimos eliminar a este usuario.
+ resend_welcome_button : Reenviar correo de bienvenida
+ resend_welcome_text : ¿Reenviar el correo de bienvenida a «{user}»? Cualquier enlace de restablecimiento de contraseña existente quedará invalidado.
+ resend_welcome_confirm : Reenviar
+ resend_welcome_success : Correo de bienvenida reenviado a «{user}».
+ resend_welcome_error : No pudimos reenviar el correo de bienvenida. Por favor, inténtalo de nuevo más tarde.
roles_modify_success : Los roles se modificaron correctamente.
roles : Roles
non_admin_role_enum : >-