From 8e62dd9551a797629332d4dd6bb9125d1945fa64 Mon Sep 17 00:00:00 2001 From: finja Date: Tue, 11 Aug 2026 23:58:16 +0200 Subject: [PATCH 1/5] =?UTF-8?q?=E2=9C=85=20update=20phpunit=209.1=20-->=20?= =?UTF-8?q?13.0,=20update=20given=20unit=20test=20syntax?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +++ README.md | 17 ++++++++-- Tests/Unit/Domain/PasswordDtoTest.php | 46 +++++++-------------------- composer.json | 9 ++++++ 4 files changed, 39 insertions(+), 37 deletions(-) diff --git a/.gitignore b/.gitignore index e6abe13..fe5d8fe 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,9 @@ vendor/ # composer composer.lock +# phpunit +.phpunit.result.cache +phpunit.xml.dist + # IDEs .idea/ diff --git a/README.md b/README.md index ebf1d82..a2ccfbc 100644 --- a/README.md +++ b/README.md @@ -287,6 +287,16 @@ Sandstorm\UserManagement\Domain\Service\UserCreationServiceInterface: className: 'Your\Package\Domain\Service\YourCustomUserCreationService' ``` +## Customizing how the reset-password e-mail address is resolved +By default, the "forgot password" flow sends the reset link to the account identifier the user entered (i.e. username +and e-mail address are assumed to be identical). If your application decouples usernames from e-mail addresses, you +can override how the recipient address is resolved by implementing `FindEmailAddressForUserServiceInterface` and +wiring it up via `Objects.yaml`: +```YAML +Sandstorm\UserManagement\Domain\Service\FindEmailAddressForUserServiceInterface: + className: 'Your\Package\Domain\Service\YourCustomFindEmailAddressForUserService' +``` + ## Hooking into the login/logout process The UserManagement package emits three signals during the login and logout process, into which you can hook using Flows [Signals and Slots](http://flowframework.readthedocs.io/en/stable/TheDefinitiveGuide/PartIII/SignalsAndSlots.html) @@ -370,8 +380,11 @@ class RegistrationFlowValidationService implements RegistrationFlowValidationSer ``` # 4. Running Tests -Run all unit tests with: -`./bin/phpunit -c ./Build/BuildEssentials/PhpUnit/UnitTests.xml Packages/Application/Sandstorm.UserManagement/Tests/Unit` +Unit tests run standalone in this package, no Flow distribution required: +``` +composer install +vendor/bin/phpunit +``` To run E2E tests, see [E2E Test Readme](Tests/E2E/README.md) diff --git a/Tests/Unit/Domain/PasswordDtoTest.php b/Tests/Unit/Domain/PasswordDtoTest.php index e782a3e..59483b3 100644 --- a/Tests/Unit/Domain/PasswordDtoTest.php +++ b/Tests/Unit/Domain/PasswordDtoTest.php @@ -5,19 +5,16 @@ use Sandstorm\UserManagement\Domain\Model\PasswordDto; /** - * Testcase for the package class + * Testcase for PasswordDto * */ -class PackageTest extends UnitTestCase +class PasswordDtoTest extends UnitTestCase { - public function setUp() + public function setUp(): void { } - /** - * @test - */ - public function equalPasswordsAreEqual() + public function testEqualPasswordsAreEqual() { $passwordDto = new PasswordDto(); $passwordDto->setPassword('foobar'); @@ -26,10 +23,7 @@ public function equalPasswordsAreEqual() $this->assertTrue($passwordDto->arePasswordsEqual()); } - /** - * @test - */ - public function inequalPasswordsAreNotEqual() + public function testInequalPasswordsAreNotEqual() { $passwordDto = new PasswordDto(); $passwordDto->setPassword('FOOBAR'); @@ -38,10 +32,7 @@ public function inequalPasswordsAreNotEqual() $this->assertFalse($passwordDto->arePasswordsEqual()); } - /** - * @test - */ - public function passwordMinLength() + public function testPasswordMinLength() { $passwordDto = new PasswordDto(); $passwordDto->setPassword('6chars'); @@ -51,10 +42,7 @@ public function passwordMinLength() $this->assertFalse($passwordDto->isPasswordMinLength(7)); } - /** - * @test - */ - public function passwordMaxLength() + public function testPasswordMaxLength() { $passwordDto = new PasswordDto(); $passwordDto->setPassword('6chars'); @@ -64,10 +52,7 @@ public function passwordMaxLength() $this->assertFalse($passwordDto->isPasswordMaxLength(5)); } - /** - * @test - */ - public function passwordContainsLowercaseLetters() + public function testPasswordContainsLowercaseLetters() { $passwordDto = new PasswordDto(); $passwordDto->setPassword('4loweRCASELETTERS'); @@ -78,10 +63,7 @@ public function passwordContainsLowercaseLetters() $this->assertFalse($passwordDto->doesPasswordContainLowercaseLetters(5)); } - /** - * @test - */ - public function passwordContainsUppercaseLetters() + public function testPasswordContainsUppercaseLetters() { $passwordDto = new PasswordDto(); $passwordDto->setPassword('4UPPErcaseletters'); @@ -92,10 +74,7 @@ public function passwordContainsUppercaseLetters() $this->assertFalse($passwordDto->doesPasswordContainUppercaseLetters(5)); } - /** - * @test - */ - public function passwordContainsNumbers() + public function testPasswordContainsNumbers() { $passwordDto = new PasswordDto(); $passwordDto->setPassword('fournumbers1234'); @@ -106,10 +85,7 @@ public function passwordContainsNumbers() $this->assertFalse($passwordDto->doesPasswordContainNumbers(5)); } - /** - * @test - */ - public function passwordContainsSpecialCharacters() + public function testPasswordContainsSpecialCharacters() { $passwordDto = new PasswordDto(); $passwordDto->setPassword('4specialCHARS!"%$'); diff --git a/composer.json b/composer.json index 0f80e8f..4d890e7 100644 --- a/composer.json +++ b/composer.json @@ -26,9 +26,18 @@ "neos/flow": "^8.3 || ^9.0", "sandstorm/templatemailer": "^3.0.1 || dev-master" }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, "autoload": { "psr-4": { "Sandstorm\\UserManagement\\": "Classes" } + }, + "autoload-dev": { + "psr-4": { + "Sandstorm\\UserManagement\\Tests\\": "Tests", + "Neos\\Flow\\Tests\\": "vendor/neos/flow/Tests" + } } } From ffa2646528a5d33a7895be6acd3092ac5c6842d4 Mon Sep 17 00:00:00 2001 From: finja Date: Wed, 12 Aug 2026 00:03:02 +0200 Subject: [PATCH 2/5] =?UTF-8?q?=E2=9C=A8=20feature:=20add=20pluggable=20Fi?= =?UTF-8?q?ndEmailAddressForUserServiceInterface=20for=20password=20reset?= =?UTF-8?q?=20+=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controller/ResetPasswordController.php | 55 +++++++++++-------- Classes/Domain/Model/ResetPasswordFlow.php | 1 - ...dressForUserByAccountIdentifierService.php | 23 ++++++++ ...indEmailAddressForUserServiceInterface.php | 20 +++++++ Configuration/Objects.yaml | 2 + ...sForUserByAccountIdentifierServiceTest.php | 33 +++++++++++ 6 files changed, 111 insertions(+), 23 deletions(-) create mode 100644 Classes/Domain/Service/FindEmailAddressForUserByAccountIdentifierService.php create mode 100644 Classes/Domain/Service/FindEmailAddressForUserServiceInterface.php create mode 100644 Tests/Unit/Domain/Service/FindEmailAddressForUserByAccountIdentifierServiceTest.php diff --git a/Classes/Controller/ResetPasswordController.php b/Classes/Controller/ResetPasswordController.php index 8d3ae16..5e71c0b 100644 --- a/Classes/Controller/ResetPasswordController.php +++ b/Classes/Controller/ResetPasswordController.php @@ -5,6 +5,7 @@ use Neos\Flow\Property\TypeConverter\PersistentObjectConverter; use Sandstorm\UserManagement\Domain\Model\ResetPasswordFlow; use Sandstorm\UserManagement\Domain\Repository\ResetPasswordFlowRepository; +use Sandstorm\UserManagement\Domain\Service\FindEmailAddressForUserServiceInterface; use Sandstorm\UserManagement\Domain\Service\UserCreationServiceInterface; use Neos\Flow\Annotations as Flow; use Neos\Flow\Mvc\Controller\ActionController; @@ -40,6 +41,12 @@ class ResetPasswordController extends ActionController */ protected $emailService; + /** + * @Flow\Inject + * @var FindEmailAddressForUserServiceInterface + */ + protected $findEmailAddressForUserService; + /** * @Flow\Inject * @var Translator @@ -104,28 +111,32 @@ public function requestTokenAction(ResetPasswordFlow $resetPasswordFlow) } } - // Send out a confirmation mail - $resetPasswordLink = $this->uriBuilder->reset()->setCreateAbsoluteUri(true)->uriFor( - 'insertNewPassword', - ['token' => $resetPasswordFlow->getResetPasswordToken()], - 'ResetPassword'); - - $this->emailService->sendTemplateEmail( - 'ResetPasswordToken', - $this->getSubjectResetPassword(), - [$resetPasswordFlow->getEmail()], - [ - 'resetPasswordLink' => $resetPasswordLink, - 'resetPasswordFlow' => $resetPasswordFlow - ], - 'sandstorm_usermanagement_sender_email', - [], // cc - [], // bcc - [], // attachments - 'sandstorm_usermanagement_replyTo_email' - ); - - $this->resetPasswordFlowRepository->add($resetPasswordFlow); + $receiverMail = $this->findEmailAddressForUserService->getEmailAddressByAccount($account); + + if ($receiverMail !== null) { + // Send out a confirmation mail + $resetPasswordLink = $this->uriBuilder->reset()->setCreateAbsoluteUri(true)->uriFor( + 'insertNewPassword', + ['token' => $resetPasswordFlow->getResetPasswordToken()], + 'ResetPassword'); + + $this->emailService->sendTemplateEmail( + 'ResetPasswordToken', + $this->getSubjectResetPassword(), + [$receiverMail], + [ + 'resetPasswordLink' => $resetPasswordLink, + 'resetPasswordFlow' => $resetPasswordFlow + ], + 'sandstorm_usermanagement_sender_email', + [], // cc + [], // bcc + [], // attachments + 'sandstorm_usermanagement_replyTo_email' + ); + + $this->resetPasswordFlowRepository->add($resetPasswordFlow); + } } diff --git a/Classes/Domain/Model/ResetPasswordFlow.php b/Classes/Domain/Model/ResetPasswordFlow.php index cb9123c..101a9a4 100644 --- a/Classes/Domain/Model/ResetPasswordFlow.php +++ b/Classes/Domain/Model/ResetPasswordFlow.php @@ -16,7 +16,6 @@ class ResetPasswordFlow /** * @var string * @Flow\Validate(type="NotEmpty") - * @Flow\Validate(type="EmailAddress") */ protected $email; diff --git a/Classes/Domain/Service/FindEmailAddressForUserByAccountIdentifierService.php b/Classes/Domain/Service/FindEmailAddressForUserByAccountIdentifierService.php new file mode 100644 index 0000000..467a04c --- /dev/null +++ b/Classes/Domain/Service/FindEmailAddressForUserByAccountIdentifierService.php @@ -0,0 +1,23 @@ +getAccountIdentifier(); + } +} diff --git a/Classes/Domain/Service/FindEmailAddressForUserServiceInterface.php b/Classes/Domain/Service/FindEmailAddressForUserServiceInterface.php new file mode 100644 index 0000000..3e9f852 --- /dev/null +++ b/Classes/Domain/Service/FindEmailAddressForUserServiceInterface.php @@ -0,0 +1,20 @@ +setAccountIdentifier('user@example.com'); + + $service = new FindEmailAddressForUserByAccountIdentifierService(); + + $this->assertSame('user@example.com', $service->getEmailAddressByAccount($account)); + } + + public function testReturnsTheAccountIdentifierUnchangedWhenItIsAPlainUsername() + { + $account = new Account(); + $account->setAccountIdentifier('someuser'); + + $service = new FindEmailAddressForUserByAccountIdentifierService(); + + $this->assertSame('someuser', $service->getEmailAddressByAccount($account)); + } +} From 5a732555858a750a25c8ca6efb71c8993e01213e Mon Sep 17 00:00:00 2001 From: finja Date: Wed, 12 Aug 2026 00:11:28 +0200 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=92=9A=20add=20unit=20tests=20as=20gi?= =?UTF-8?q?thub=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/unit-tests.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/unit-tests.yml diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000..b56a4d0 --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,31 @@ +name: Unit Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + unit-tests: + name: Unit Tests (PHP ${{ matrix.php }}) + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + php: ['8.2', '8.3', '8.4'] + + steps: + - uses: actions/checkout@v6 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - name: Install dependencies + run: composer install --no-plugins --prefer-dist + + - name: Run unit tests + run: vendor/bin/phpunit From 0370e75cc966ff533f315fd5d0152570177e032c Mon Sep 17 00:00:00 2001 From: finja Date: Wed, 12 Aug 2026 00:20:27 +0200 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=92=9A=20FIX:=20pass=20Tests/Unit=20e?= =?UTF-8?q?xplicitly=20to=20phpunit=20command=20in=20ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/unit-tests.yml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index b56a4d0..1b8cc98 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -28,4 +28,4 @@ jobs: run: composer install --no-plugins --prefer-dist - name: Run unit tests - run: vendor/bin/phpunit + run: vendor/bin/phpunit Tests/Unit diff --git a/README.md b/README.md index a2ccfbc..a9ee754 100644 --- a/README.md +++ b/README.md @@ -383,7 +383,7 @@ class RegistrationFlowValidationService implements RegistrationFlowValidationSer Unit tests run standalone in this package, no Flow distribution required: ``` composer install -vendor/bin/phpunit +vendor/bin/phpunit Tests/Unit ``` To run E2E tests, see [E2E Test Readme](Tests/E2E/README.md) From 55abff42636311c06fda261909245c710af3f097 Mon Sep 17 00:00:00 2001 From: finja Date: Wed, 12 Aug 2026 00:24:00 +0200 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=92=9A=20only=20test=20php=208.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/unit-tests.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 1b8cc98..8134005 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -8,20 +8,18 @@ on: jobs: unit-tests: - name: Unit Tests (PHP ${{ matrix.php }}) + name: Unit Tests runs-on: ubuntu-latest timeout-minutes: 10 - strategy: - fail-fast: false - matrix: - php: ['8.2', '8.3', '8.4'] steps: - uses: actions/checkout@v6 - uses: shivammathur/setup-php@v2 with: - php-version: ${{ matrix.php }} + # no matrix across 8.2-8.4 here: these are plain PHP unit tests with no + # version-sensitive logic, so one version is enough signal for the added CI cost + php-version: '8.4' coverage: none - name: Install dependencies