diff --git a/LICENCE.txt b/LICENCE.txt index 6fc5283e..34f77526 100644 --- a/LICENCE.txt +++ b/LICENCE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2023 Samuel Gfeller +Copyright (c) 2024 Samuel Gfeller Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation diff --git a/src/Application/Action/Authentication/Ajax/NewPasswordResetSubmitAction.php b/src/Application/Action/Authentication/Ajax/NewPasswordResetSubmitAction.php index 5c7030b9..c8f12337 100644 --- a/src/Application/Action/Authentication/Ajax/NewPasswordResetSubmitAction.php +++ b/src/Application/Action/Authentication/Ajax/NewPasswordResetSubmitAction.php @@ -50,7 +50,7 @@ public function __invoke(ServerRequest $request, Response $response): Response } catch (InvalidTokenException $ite) { $this->templateRenderer->addPhpViewAttribute( 'formErrorMessage', - __( + __( // Message below asserted in PasswordResetSubmitActionTest 'Invalid, used or expired link.
Please request a new link below and make sure to click on the most recent email we send you.
' ) diff --git a/src/Application/Action/User/Ajax/UserCreateAction.php b/src/Application/Action/User/Ajax/UserCreateAction.php index 5cc31f9f..38334156 100644 --- a/src/Application/Action/User/Ajax/UserCreateAction.php +++ b/src/Application/Action/User/Ajax/UserCreateAction.php @@ -4,7 +4,6 @@ use App\Application\Responder\JsonResponder; use App\Domain\User\Service\UserCreator; -use Odan\Session\SessionInterface; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as ServerRequest; use Psr\Log\LoggerInterface; @@ -16,7 +15,6 @@ public function __construct( private LoggerInterface $logger, private JsonResponder $jsonResponder, private UserCreator $userCreator, - private SessionInterface $session, ) { } diff --git a/src/Domain/Authentication/Exception/InvalidCredentialsException.php b/src/Domain/Authentication/Exception/InvalidCredentialsException.php index e9c35e8d..eb0dbe8b 100644 --- a/src/Domain/Authentication/Exception/InvalidCredentialsException.php +++ b/src/Domain/Authentication/Exception/InvalidCredentialsException.php @@ -7,6 +7,7 @@ class InvalidCredentialsException extends AuthenticationException // Voluntarily not more information private string $userEmail; + // Invalid credentials asserted in LoginSubmitActionTest public function __construct(string $email, string $message = 'Invalid credentials') { parent::__construct($message); diff --git a/src/Infrastructure/Console/SqlSchemaGenerator.php b/src/Infrastructure/Console/SqlSchemaGenerator.php index b165b36d..5b88edd1 100644 --- a/src/Infrastructure/Console/SqlSchemaGenerator.php +++ b/src/Infrastructure/Console/SqlSchemaGenerator.php @@ -69,7 +69,7 @@ private function query(string $sql): PDOStatement $statement = $this->pdo->query($sql); if (!$statement) { - throw new UnexpectedValueException('Query failed: ' . $sql . ' Error: ' . $this->pdo->errorInfo()[2] ?? ''); + throw new UnexpectedValueException('Query failed: ' . $sql . ' Error: ' . $this->pdo->errorInfo()[2]); } return $statement; diff --git a/tests/Fixture/FixtureInterface.php b/tests/Fixture/FixtureInterface.php index 0dfe7899..555c1902 100644 --- a/tests/Fixture/FixtureInterface.php +++ b/tests/Fixture/FixtureInterface.php @@ -2,6 +2,11 @@ namespace App\Test\Fixture; +/** + * Fixture classes contain the properties $table and $records. + * @property string $table + * @property array $records + */ interface FixtureInterface { // Attributes are public but php doesn't support class properties in interfaces so getters are needed diff --git a/tests/Fixture/NoteFixture.php b/tests/Fixture/NoteFixture.php index 7cdc8c5a..6ae7dc6c 100644 --- a/tests/Fixture/NoteFixture.php +++ b/tests/Fixture/NoteFixture.php @@ -3,8 +3,7 @@ namespace App\Test\Fixture; /** - * Post values that can be inserted into the database - * UserFixture HAS to be inserted first. + * Note values that can be inserted into the database */ class NoteFixture implements FixtureInterface { diff --git a/tests/Integration/Authentication/LoginSubmitActionTest.php b/tests/Integration/Authentication/LoginSubmitActionTest.php index c638f99c..be3a9c6f 100644 --- a/tests/Integration/Authentication/LoginSubmitActionTest.php +++ b/tests/Integration/Authentication/LoginSubmitActionTest.php @@ -96,6 +96,14 @@ public function testLoginSubmitActionWrongPassword(): void // Assert that session user_id is not set self::assertNull($this->container->get(SessionInterface::class)->get('user_id')); + + // Get response body as string from stream + $stream = $response->getBody(); + $stream->rewind(); + $body = $stream->getContents(); + + // Assert that response body contains validation error + self::assertStringContainsString('Invalid credentials', $body); } /** @@ -104,8 +112,9 @@ public function testLoginSubmitActionWrongPassword(): void * @dataProvider \App\Test\Provider\Authentication\AuthenticationProvider::invalidLoginCredentialsProvider() * * @param array $invalidLoginValues valid credentials + * @param string $errorMessage validation message that should be in response body */ - public function testLoginSubmitActionInvalidValues(array $invalidLoginValues): void + public function testLoginSubmitActionInvalidValues(array $invalidLoginValues, string $errorMessage): void { $this->insertFixturesWithAttributes([], new UserFixture()); @@ -119,6 +128,14 @@ public function testLoginSubmitActionInvalidValues(array $invalidLoginValues): v $session = $this->container->get(SessionInterface::class); // Assert that session user_id is not set self::assertNull($session->get('user_id')); + + // Get response body as string from stream + $stream = $response->getBody(); + $stream->rewind(); + $body = $stream->getContents(); + + // Assert that response body contains validation error + self::assertStringContainsString($errorMessage, $body); } /** diff --git a/tests/Integration/Authentication/PasswordForgottenEmailSubmitActionTest.php b/tests/Integration/Authentication/PasswordForgottenEmailSubmitActionTest.php index 7e6b2b1d..0e576cb9 100644 --- a/tests/Integration/Authentication/PasswordForgottenEmailSubmitActionTest.php +++ b/tests/Integration/Authentication/PasswordForgottenEmailSubmitActionTest.php @@ -44,10 +44,13 @@ public function testPasswordForgottenEmailSubmit(): void // Simulate logged-in user with id 2 $this->container->get(SessionInterface::class)->set('user_id', $userId); - $request = $this->createFormRequest('POST', // Request to change password - $this->urlFor('password-forgotten-email-submit'), [ + $request = $this->createFormRequest( + 'POST', // Request to change password + $this->urlFor('password-forgotten-email-submit'), + [ 'email' => $email, - ]); + ] + ); $response = $this->app->handle($request); @@ -108,10 +111,13 @@ public function testPasswordForgottenEmailSubmitUserNotExisting(): void { // Not inserting user as it shouldn't exist - $request = $this->createFormRequest('POST', // Request to change password - $this->urlFor('password-forgotten-email-submit'), [ + $request = $this->createFormRequest( + 'POST', // Request to change password + $this->urlFor('password-forgotten-email-submit'), + [ 'email' => 'user@example.com', - ]); + ] + ); $response = $this->app->handle($request); @@ -135,17 +141,25 @@ public function testPasswordForgottenEmailSubmitInvalidData(): void // Simulate logged-in user with id 2 $this->container->get(SessionInterface::class)->set('user_id', $userRow['id']); - $request = $this->createFormRequest('POST', // Request to change password - $this->urlFor('password-forgotten-email-submit'), [ + $request = $this->createFormRequest( + 'POST', // Request to change password + $this->urlFor('password-forgotten-email-submit'), + [ 'email' => 'inval$d@ema$l.com', - ]); + ] + ); $response = $this->app->handle($request); // Assert that response has error status 422 self::assertSame(StatusCodeInterface::STATUS_UNPROCESSABLE_ENTITY, $response->getStatusCode()); - // As form is directly rendered with validation errors it's not possible to test them as response is a stream - // There is a visual test in insomnia for this, but I couldn't manage to keep the login session + // Get response body as string from stream + $stream = $response->getBody(); + $stream->rewind(); + $body = $stream->getContents(); + + // Assert that response body contains validation error + self::assertStringContainsString('Invalid email', $body); } } diff --git a/tests/Integration/Authentication/PasswordResetSubmitActionTest.php b/tests/Integration/Authentication/PasswordResetSubmitActionTest.php index 2314a556..14106415 100644 --- a/tests/Integration/Authentication/PasswordResetSubmitActionTest.php +++ b/tests/Integration/Authentication/PasswordResetSubmitActionTest.php @@ -107,17 +107,22 @@ public function testResetPasswordSubmitInvalidToken( self::assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode()); // Assert that token had NOT been used (except if already used) - self::assertSame( - $verification->usedAt, - $this->getTableRowById('user_verification', (int)$verification->id, ['used_at'])['used_at'] - ); + $this->assertTableRowValue($verification->usedAt, 'user_verification', $verification->id, 'used_at'); - // Assert that password was not changed to the new one + // Assert that the password was not changed to the new one $this->assertTableRowValue(UserStatus::Unverified->value, 'user', $userRow['id'], 'status'); // Assert that password was NOT changed $dbPasswordHash = $this->getTableRowById('user', $userRow['id'])['password_hash']; self::assertFalse(password_verify($newPassword, $dbPasswordHash)); + + // Get response body as string from stream + $stream = $response->getBody(); + $stream->rewind(); + $body = $stream->getContents(); + + // Assert that response body contains validation error + self::assertStringContainsString('Invalid, used or expired link', $body); } /** @@ -154,7 +159,14 @@ public function testResetPasswordSubmitInvalidData( // Assert that response has error status 422 self::assertSame(StatusCodeInterface::STATUS_UNPROCESSABLE_ENTITY, $response->getStatusCode()); - // As form is directly rendered with validation errors it's not possible to test them as response is a stream + + // Get response body as string from stream + $stream = $response->getBody(); + $stream->rewind(); + $body = $stream->getContents(); + + // Assert that response body contains validation error + self::assertStringContainsString('Minimum length is 3', $body); } /** diff --git a/tests/Integration/Client/ClientReadPageActionTest.php b/tests/Integration/Client/ClientReadPageActionTest.php index bd06fbe3..11491d5d 100644 --- a/tests/Integration/Client/ClientReadPageActionTest.php +++ b/tests/Integration/Client/ClientReadPageActionTest.php @@ -10,8 +10,6 @@ use Fig\Http\Message\StatusCodeInterface; use Odan\Session\SessionInterface; use PHPUnit\Framework\TestCase; -use Psr\Container\ContainerExceptionInterface; -use Psr\Container\NotFoundExceptionInterface; use Selective\TestTrait\Traits\DatabaseTestTrait; use Selective\TestTrait\Traits\HttpTestTrait; use Selective\TestTrait\Traits\RouteTestTrait; @@ -32,12 +30,9 @@ class ClientReadPageActionTest extends TestCase /** * Normal page action while being authenticated. * - * @throws NotFoundExceptionInterface - * @throws ContainerExceptionInterface - * * @return void */ - public function testClientReadPageActionAuthorization(): void + public function testClientReadPageActionAuthenticated(): void { // Insert linked and authenticated user $userId = $this->insertFixturesWithAttributes([], new UserFixture())['id']; diff --git a/tests/Provider/Authentication/AuthenticationProvider.php b/tests/Provider/Authentication/AuthenticationProvider.php index 629c64df..e2ada4dc 100644 --- a/tests/Provider/Authentication/AuthenticationProvider.php +++ b/tests/Provider/Authentication/AuthenticationProvider.php @@ -81,26 +81,27 @@ public static function authenticationSecurityCases(): array public static function invalidLoginCredentialsProvider(): array { return [ - [ + 'Invalid email' => [ [ - // Invalid email 'email' => 'admin@exam$ple.com', 'password' => '12345678', ], + 'validationErrorMessage' => 'Invalid email', ], - [ + 'Missing email' => [ [ // Missing email 'email' => '', 'password' => '12345678', ], + 'validationErrorMessage' => 'Invalid email', ], - [ + 'Missing password' => [ [ - // Missing password 'email' => 'admin@example.com', 'password' => '', ], + 'validationErrorMessage' => 'Invalid password', ], ]; } diff --git a/tests/Provider/Note/NoteProvider.php b/tests/Provider/Note/NoteProvider.php index 9b5295fd..e3f53295 100644 --- a/tests/Provider/Note/NoteProvider.php +++ b/tests/Provider/Note/NoteProvider.php @@ -75,7 +75,7 @@ public static function noteListUserAttributesAndExpectedResultProvider(): array */ public static function noteListWithFilterProvider(): array { - // Users linked to notes to insert (authenticated user not relevant for this test, he is inserted in test case) + // Users linked to notes to insert (authenticated user not relevant for this test, inserted in test case) $usersToInsert = [ ['id' => 10], ['id' => 11], diff --git a/tests/Provider/Security/LoginRequestProvider.php b/tests/Provider/Security/LoginRequestProvider.php index 0bf373cc..664c146f 100644 --- a/tests/Provider/Security/LoginRequestProvider.php +++ b/tests/Provider/Security/LoginRequestProvider.php @@ -4,9 +4,6 @@ class LoginRequestProvider { - // ! This must be the same as the mocked config values in the unit test case - private const userLoginThrottleThresholds = [4 => 10, 9 => 120, 12 => 'captcha']; - private const securitySettings = [ 'throttle_login' => true, 'login_throttle_rule' => [4 => 10, 9 => 120, 12 => 'captcha'], diff --git a/tests/Traits/DatabaseExtensionTestTrait.php b/tests/Traits/DatabaseExtensionTestTrait.php index f3ea5a82..5e28b402 100644 --- a/tests/Traits/DatabaseExtensionTestTrait.php +++ b/tests/Traits/DatabaseExtensionTestTrait.php @@ -6,7 +6,7 @@ use Selective\TestTrait\Traits\DatabaseTestTrait; /** - * Slim App Route Test Trait. + * Extension of DatabaseTestTrait to add more functionality. */ trait DatabaseExtensionTestTrait { diff --git a/tests/Traits/FixtureTestTrait.php b/tests/Traits/FixtureTestTrait.php index 05055fcd..e110538d 100644 --- a/tests/Traits/FixtureTestTrait.php +++ b/tests/Traits/FixtureTestTrait.php @@ -12,48 +12,6 @@ trait FixtureTestTrait { use DatabaseTestTrait; - /** - * Takes default record and modifies it to suit given attributes. - * This makes a lot more sense than storing all different kinds - * of records in each fixture and then searching them as the test - * function can control fixtures and there is no dependencies. - * - * @param array $attributes - * @param FixtureInterface $fixture - * @param int $amount amount rows that will be returned - * - * @return array either one row of requested fixture or array of rows if $amount is higher than 1 - */ - protected function getFixtureRecordsWithAttributes(array $attributes, FixtureInterface $fixture, int $amount = 1): array - { - $rows = $fixture->getRecords(); - $returnArray = []; - $rowKey = 0; - // To have a pool of different data; instead of taking one basic record when multiple records are asked, - // this iterates over the existing records - for ($i = 0; $i <= $amount; $i++) { - // If there are no more rows for the row key, reset it to 0 - if (!isset($rows[$rowKey])) { - $rowKey = 0; - } - // Remove id from row key before setting new values as id might be a given attribute - unset($rows[$rowKey]['id']); - // Add given attributes to row - foreach ($attributes as $colum => $value) { - // Set value to given attribute value - $rows[$rowKey][$colum] = $value; - } - $returnArray[] = $rows[$rowKey]; - $rowKey++; - } - - if ($amount === 1) { - return $returnArray[0]; - } - - return $returnArray; - } - /** * Inserts fixtures with given attributes and returns rows with id * This has to advantage to remove the dependency of each fixtures' @@ -109,81 +67,47 @@ protected function insertFixturesWithAttributes( } /** - * Insert multiple given fixture rows. - * - * @param string $table - * @param array $rows - * - * @return array rows with id - */ - protected function insertFixtureRows(string $table, array $rows): array - { - foreach ($rows as $key => $row) { - $rows[$key]['id'] = (int)$this->insertFixture($table, $row); - } - - return $rows; - } - - /** - * Returns fixture rows where given condition matches - * Note: this relies on the function of selective/test-traits DatabaseTestTrait.php. + * Takes the default record and modifies it to suit given attributes. + * This makes a lot more sense than storing all different kinds + * of records in each fixture and then searching them as the test + * function can control fixtures, and there are no dependencies. * - * @param array $conditions array of db field name and the expected value. Example: - * ['field_name' => 'expected_value', 'other_field_name' => 'other expected value',] + * @param array $attributes * @param FixtureInterface $fixture - * @param array $oppositeConditions optional NOT conditions. If ['id' => 1] is provided -> user 1 will NOT be returned + * @param int $amount amount rows that will be returned * - * @return array[] records matching the conditions + * @return array either one row of requested fixture or array of rows if $amount is higher than 1 */ - protected function findRecordsFromFixtureWhere( - array $conditions, + private function getFixtureRecordsWithAttributes( + array $attributes, FixtureInterface $fixture, - array $oppositeConditions = [] + int $amount = 1 ): array { $rows = $fixture->getRecords(); - $matchingRecords = []; - // Loop over all records (rows) - foreach ($rows as $row) { - // Check if condition matches on row columns - foreach ($conditions as $columnToCheck => $expectedColumnValue) { - // If the current condition (in loop) is about the field, check the value - if ($row[$columnToCheck] !== $expectedColumnValue) { - // If one value of the row does not match the condition, the rest of the current rows iteration is skipped - continue 2; - } + $returnArray = []; + $rowKey = 0; + // To have a pool of different data; instead of taking one basic record when multiple records are asked, + // this iterates over the existing records + for ($i = 0; $i <= $amount; $i++) { + // If there are no more rows for the row key, reset it to 0 + if (!isset($rows[$rowKey])) { + $rowKey = 0; } - // Check if opposite condition matches rows that should NOT be returned - foreach ($oppositeConditions as $columnToCheck => $expectedColumnValue) { - // If the current opposite condition (in loop) concerns the $columnToCheck, check if the value matches - if ($row[$columnToCheck] === $expectedColumnValue) { - // If one value of the row matches the condition, the rest of the current rows iteration is skipped - continue 2; - } + // Remove id from row key before setting new values as id might be a given attribute + unset($rows[$rowKey]['id']); + // Add given attributes to row + foreach ($attributes as $colum => $value) { + // Set value to given attribute value + $rows[$rowKey][$colum] = $value; } - - // If all conditions matched, this part is not skipped (with continue) and row is added to matching records - $matchingRecords[] = $row; + $returnArray[] = $rows[$rowKey]; + $rowKey++; } - return $matchingRecords; - } - - /** - * If only specific fixtures should be inserted for instance - * linked to a specific resource. - * - * @param array $conditions array of db column name and the expected value. - * Shape: ['field_name' => 'expected_value', 'other_field_name' => 'other expected value',] - * @param FixtureInterface $fixture - * - * @return void - */ - protected function insertFixtureWhere(array $conditions, FixtureInterface $fixture): void - { - $filteredRecords = $this->findRecordsFromFixtureWhere($conditions, $fixture); - foreach ($filteredRecords as $row) { - $this->insertFixture($fixture->getTable(), $row); + if ($amount === 1) { + return $returnArray[0]; } + + return $returnArray; } }