From fa29f1d821b3557199b40dd216c548b58f288f67 Mon Sep 17 00:00:00 2001 From: Volodymyr Komarov Date: Sun, 16 Apr 2023 14:34:58 +0300 Subject: [PATCH 1/9] Accept and store locale on register --- app/src/Controller/Auth/RegisterController.php | 3 +++ app/src/Request/RegisterRequest.php | 13 +++++++++++++ .../Auth/RegisterController/RegisterTest.php | 12 +++++++++++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/app/src/Controller/Auth/RegisterController.php b/app/src/Controller/Auth/RegisterController.php index 9aa13168..8f4179f0 100644 --- a/app/src/Controller/Auth/RegisterController.php +++ b/app/src/Controller/Auth/RegisterController.php @@ -11,6 +11,7 @@ use App\Service\Auth\AuthService; use App\Service\Auth\EmailConfirmationService; use App\Service\Auth\RefreshTokenService; +use App\Service\UserOptionsService; use App\Service\UserService; use App\View\UserView; use Psr\Http\Message\ResponseInterface; @@ -30,6 +31,7 @@ public function __construct( protected EmailConfirmationService $emailConfirmationService, protected RefreshTokenService $refreshTokenService, private CurrencyRepository $currencyRepository, + protected UserOptionsService $userOptionsService, ) { parent::__construct($userView, $response); } @@ -46,6 +48,7 @@ public function register(RegisterRequest $request): ResponseInterface } $this->authService->hashPassword($user, $request->password); + $this->userOptionsService->setLocale($user, $request->locale); try { $user = $this->userService->store($user); diff --git a/app/src/Request/RegisterRequest.php b/app/src/Request/RegisterRequest.php index 8c05b7aa..4c046572 100644 --- a/app/src/Request/RegisterRequest.php +++ b/app/src/Request/RegisterRequest.php @@ -10,6 +10,7 @@ use Spiral\Filters\Model\Filter; use Spiral\Filters\Model\FilterDefinitionInterface; use Spiral\Filters\Model\HasFilterDefinition; +use Spiral\Translator\Translator; use Spiral\Validator\FilterDefinition; class RegisterRequest extends Filter implements HasFilterDefinition @@ -32,6 +33,14 @@ class RegisterRequest extends Filter implements HasFilterDefinition #[Data] public string $passwordConfirmation = ''; + #[Data] + public string $locale = ''; + + public function __construct( + private readonly Translator $translator, + ) { + } + public function filterDefinition(): FilterDefinitionInterface { return new FilterDefinition(validationRules: [ @@ -62,6 +71,10 @@ public function filterDefinition(): FilterDefinitionInterface ['notEmpty', 'if' => ['withAll' => ['password']]], ['match', 'password', 'error' => 'error_password_confirmation_not_match'] ], + 'locale' => [ + 'is_string', + ['in_array', $this->translator->getCatalogueManager()->getLocales(), true], + ] ]); } diff --git a/tests/Feature/Controller/Auth/RegisterController/RegisterTest.php b/tests/Feature/Controller/Auth/RegisterController/RegisterTest.php index 01895c13..0aad001f 100644 --- a/tests/Feature/Controller/Auth/RegisterController/RegisterTest.php +++ b/tests/Feature/Controller/Auth/RegisterController/RegisterTest.php @@ -51,6 +51,7 @@ public function testUserCreated(): void 'email' => $user->email, 'password' => UserFactory::DEFAULT_PASSWORD, 'passwordConfirmation' => UserFactory::DEFAULT_PASSWORD, + 'locale' => UserFactory::locale(), ]); $response->assertOk(); @@ -86,6 +87,7 @@ public function testUserStoreFailed(): void 'email' => $user->email, 'password' => UserFactory::DEFAULT_PASSWORD, 'passwordConfirmation' => UserFactory::DEFAULT_PASSWORD, + 'locale' => UserFactory::locale(), ]); $response->assertStatus(500); @@ -120,6 +122,7 @@ public function testEmailConfirmationServiceFailStillStoreUser(): void 'email' => $user->email, 'password' => UserFactory::DEFAULT_PASSWORD, 'passwordConfirmation' => UserFactory::DEFAULT_PASSWORD, + 'locale' => UserFactory::locale(), ]); $response->assertOk(); @@ -141,7 +144,8 @@ public function testValidationFailsByEmptyForm(): void 'nickName' => '', 'email' => '', 'password' => '', - 'passwordConfirmation' => '' + 'passwordConfirmation' => '', + 'locale' => '', ]); $response->assertUnprocessable(); @@ -153,6 +157,7 @@ public function testValidationFailsByEmptyForm(): void $this->assertArrayHasKey('nickName', $body['errors']); $this->assertArrayHasKey('email', $body['errors']); $this->assertArrayHasKey('password', $body['errors']); + $this->assertArrayHasKey('locale', $body['errors']); } public function testValidationFailsByShortPassword(): void @@ -165,6 +170,7 @@ public function testValidationFailsByShortPassword(): void 'email' => $user->email, 'password' => Fixtures::string(5), 'passwordConfirmation' => Fixtures::string(5), + 'locale' => UserFactory::locale(), ]); $response->assertUnprocessable(); @@ -188,6 +194,7 @@ public function testValidationFailsByNickNameExists(): void 'email' => $newUser->email, 'password' => Fixtures::string(), 'passwordConfirmation' => Fixtures::string(), + 'locale' => UserFactory::locale(), ]); $response->assertUnprocessable(); @@ -210,6 +217,7 @@ public function testValidationFailsByNickNameInvalid(string $nickName): void 'email' => Fixtures::email(), 'password' => Fixtures::string(), 'passwordConfirmation' => Fixtures::string(), + 'locale' => UserFactory::locale(), ]); $response->assertUnprocessable(); @@ -243,6 +251,7 @@ public function testValidationFailsByEmailExists(): void 'email' => $existingUser->email, 'password' => Fixtures::string(), 'passwordConfirmation' => Fixtures::string(), + 'locale' => UserFactory::locale(), ]); $response->assertUnprocessable(); @@ -267,6 +276,7 @@ public function testValidationFailsByEmailInvalid(string $email): void 'email' => $email, 'password' => Fixtures::string(), 'passwordConfirmation' => Fixtures::string(), + 'locale' => UserFactory::locale(), ]); $response->assertUnprocessable(); From a24c2a842b7cd240edcc01f86d4046808fb8dcad Mon Sep 17 00:00:00 2001 From: Volodymyr Komarov Date: Wed, 19 Apr 2023 01:05:51 +0300 Subject: [PATCH 2/9] Implement data encryption in database at ORM level --- .env.actions | 1 + .env.sample | 1 + app/config/app.php | 2 + ...0417.234325_0_expand_encrypted_columns.php | 88 ++++++++++++++++++ .../20230417.235038_0_encrypt_columns.php | 47 ++++++++++ app/src/Bootloader/CheckerBootloader.php | 2 + .../Bootloader/EntityBehaviorBootloader.php | 3 + app/src/Command/EncryptKeyGenerate.php | 24 +++++ app/src/Config/AppConfig.php | 28 ++---- app/src/Database/Charge.php | 9 +- app/src/Database/Encrypter/Encrypter.php | 55 +++++++++++ .../Database/Encrypter/EncrypterInterface.php | 12 +++ .../Database/Typecast/EncryptedTypecast.php | 91 +++++++++++++++++++ app/src/Database/User.php | 10 +- app/src/Database/Wallet.php | 11 ++- app/src/Repository/UserRepository.php | 12 ++- app/src/Request/CheckNickNameRequest.php | 2 +- .../Request/ForgotPasswordCreateRequest.php | 2 +- .../Request/Profile/UpdateBasicRequest.php | 2 +- app/src/Request/RegisterRequest.php | 4 +- app/src/Request/Wallet/CreateRequest.php | 2 +- app/src/Security/EncryptedEntityChecker.php | 47 ++++++++++ app/src/Security/UniqueChecker.php | 12 ++- app/src/Service/Filter/Filter.php | 4 +- .../Command/EncryptKeyGenerateTest.php | 24 +++++ .../Auth/EmailConfirmationControllerTest.php | 9 +- .../Auth/RegisterController/RegisterTest.php | 4 +- .../Profile/ProfileControllerTest.php | 3 +- .../Wallets/IndexControllerTest.php | 45 +++++++++ .../Wallets/WalletsControllerTest.php | 9 +- .../Database/Encrypter/EncrypterTest.php | 26 ++++++ .../Typecast/EncryptedTypecastTest.php | 72 +++++++++++++++ tests/Feature/Security/UniqueCheckerTest.php | 6 +- tests/TestCase.php | 1 + tests/Traits/InteractsWithDatabase.php | 39 ++++++-- 35 files changed, 648 insertions(+), 61 deletions(-) create mode 100644 app/migrations/20230417.234325_0_expand_encrypted_columns.php create mode 100644 app/migrations/20230417.235038_0_encrypt_columns.php create mode 100644 app/src/Command/EncryptKeyGenerate.php create mode 100644 app/src/Database/Encrypter/Encrypter.php create mode 100644 app/src/Database/Encrypter/EncrypterInterface.php create mode 100644 app/src/Database/Typecast/EncryptedTypecast.php create mode 100644 app/src/Security/EncryptedEntityChecker.php create mode 100644 tests/Feature/Command/EncryptKeyGenerateTest.php create mode 100644 tests/Feature/Database/Encrypter/EncrypterTest.php create mode 100644 tests/Feature/Database/Typecast/EncryptedTypecastTest.php diff --git a/.env.actions b/.env.actions index 21058b79..76276014 100644 --- a/.env.actions +++ b/.env.actions @@ -1,5 +1,6 @@ DEBUG=false ENCRYPTER_KEY={encrypt-key} +DB_ENCRYPTER_KEY={encrypt-key} SAFE_MIGRATIONS=true VERBOSITY_LEVEL=basic MONOLOG_DEFAULT_CHANNEL=default diff --git a/.env.sample b/.env.sample index addee67b..fa51f195 100644 --- a/.env.sample +++ b/.env.sample @@ -3,6 +3,7 @@ DEBUG=true # Set to an application specific value, used to encrypt/decrypt cookies etc. ENCRYPTER_KEY={encrypt-key} +DB_ENCRYPTER_KEY={encrypt-key} # Set to TRUE to disable confirmation in `migrate` commands. SAFE_MIGRATIONS=true diff --git a/app/config/app.php b/app/config/app.php index dc61dbed..903ad12c 100644 --- a/app/config/app.php +++ b/app/config/app.php @@ -10,4 +10,6 @@ 'email_confirmation_link' => '/email/confirm/{token}', 'password_reset_link' => '/password/reset/{code}', 'wallet_link' => '/wallets/{wallet}', + + 'db_encrypter_key' => env('DB_ENCRYPTER_KEY'), ]; diff --git a/app/migrations/20230417.234325_0_expand_encrypted_columns.php b/app/migrations/20230417.234325_0_expand_encrypted_columns.php new file mode 100644 index 00000000..47666f5b --- /dev/null +++ b/app/migrations/20230417.234325_0_expand_encrypted_columns.php @@ -0,0 +1,88 @@ +table('users') + ->alterColumn('name', 'string', [ + 'nullable' => false, + 'default' => null, + 'size' => 1536, + ]) + ->alterColumn('last_name', 'string', [ + 'nullable' => true, + 'default' => null, + 'size' => 1536 + ]) + ->alterColumn('nick_name', 'string', [ + 'nullable' => false, + 'default' => null, + 'size' => 767 + ]) + ->alterColumn('email', 'string', [ + 'nullable' => false, + 'default' => null, + 'size' => 676 + ]) + ->update(); + + $this->table('wallets') + ->alterColumn('name', 'string', [ + 'nullable' => false, + 'default' => null, + 'size' => 1536 + ]) + ->alterColumn('slug', 'string', [ + 'nullable' => false, + 'default' => null, + 'size' => 1536 + ]) + ->update(); + } + + public function down(): void + { + $this->table('users') + ->alterColumn('name', 'string', [ + 'nullable' => false, + 'default' => null, + 'size' => 255, + ]) + ->alterColumn('last_name', 'string', [ + 'nullable' => true, + 'default' => null, + 'size' => 255 + ]) + ->alterColumn('nick_name', 'string', [ + 'nullable' => false, + 'default' => null, + 'size' => 255 + ]) + ->alterColumn('email', 'string', [ + 'nullable' => false, + 'default' => null, + 'size' => 255 + ]) + ->update(); + + $this->table('wallets') + ->alterColumn('name', 'string', [ + 'nullable' => false, + 'default' => null, + 'size' => 255 + ]) + ->alterColumn('slug', 'string', [ + 'nullable' => false, + 'default' => null, + 'size' => 255 + ]) + ->update(); + } +} diff --git a/app/migrations/20230417.235038_0_encrypt_columns.php b/app/migrations/20230417.235038_0_encrypt_columns.php new file mode 100644 index 00000000..ccb56f3e --- /dev/null +++ b/app/migrations/20230417.235038_0_encrypt_columns.php @@ -0,0 +1,47 @@ +database()->select(['id', 'name', 'last_name', 'nick_name', 'email'])->from('users')->fetchAll(); + + foreach ($items as $user) { + $this->database()->update('users', [ + 'name' => $this->encrypter->encrypt($user['name']), + 'last_name' => $this->encrypter->encrypt($user['last_name']), + 'nick_name' => $this->encrypter->encrypt($user['nick_name']), + 'email' => $this->encrypter->encrypt($user['email']), + ], [ + 'id' => $user['id'] + ])->run(); + } + + $items = $this->database()->select(['id', 'name', 'slug'])->from('wallets')->fetchAll(); + + foreach ($items as $wallet) { + $this->database()->update('wallets', [ + 'name' => $this->encrypter->encrypt($wallet['name']), + 'slug' => $this->encrypter->encrypt($wallet['slug']), + ], [ + 'id' => $wallet['id'] + ])->run(); + } + } + + public function down(): void + { + // + } +} diff --git a/app/src/Bootloader/CheckerBootloader.php b/app/src/Bootloader/CheckerBootloader.php index cdffa2d1..1ccee22a 100644 --- a/app/src/Bootloader/CheckerBootloader.php +++ b/app/src/Bootloader/CheckerBootloader.php @@ -4,6 +4,7 @@ namespace App\Bootloader; +use App\Security\EncryptedEntityChecker; use App\Security\PasswordChecker; use App\Security\UniqueChecker; use Spiral\Boot\Bootloader\Bootloader; @@ -18,5 +19,6 @@ public function boot(ValidatorBootloader $validation): void { $validation->addChecker('password', PasswordChecker::class); $validation->addChecker('unique', UniqueChecker::class); + $validation->addChecker('encrypted-entity', EncryptedEntityChecker::class); } } diff --git a/app/src/Bootloader/EntityBehaviorBootloader.php b/app/src/Bootloader/EntityBehaviorBootloader.php index a3fc9fdb..30b56bce 100644 --- a/app/src/Bootloader/EntityBehaviorBootloader.php +++ b/app/src/Bootloader/EntityBehaviorBootloader.php @@ -4,6 +4,8 @@ namespace App\Bootloader; +use App\Database\Encrypter\Encrypter; +use App\Database\Encrypter\EncrypterInterface; use Cycle\ORM\Transaction\CommandGeneratorInterface; use Cycle\ORM\Entity\Behavior\EventDrivenCommandGenerator; use Spiral\Boot\Bootloader\Bootloader; @@ -12,5 +14,6 @@ final class EntityBehaviorBootloader extends Bootloader { protected const BINDINGS = [ CommandGeneratorInterface::class => EventDrivenCommandGenerator::class, + EncrypterInterface::class => Encrypter::class, ]; } diff --git a/app/src/Command/EncryptKeyGenerate.php b/app/src/Command/EncryptKeyGenerate.php new file mode 100644 index 00000000..ae701067 --- /dev/null +++ b/app/src/Command/EncryptKeyGenerate.php @@ -0,0 +1,24 @@ +writeln($enc->generateKey()); + } +} diff --git a/app/src/Config/AppConfig.php b/app/src/Config/AppConfig.php index 3756a6d4..e843412b 100644 --- a/app/src/Config/AppConfig.php +++ b/app/src/Config/AppConfig.php @@ -21,56 +21,42 @@ class AppConfig extends InjectableConfig 'email_confirmation_link' => '', 'password_reset_link' => '', 'wallet_link' => '', + + 'db_encrypter_key' => '', ]; - /** - * @return string - */ public function getUrl(): string { return $this->config['url']; } - /** - * @return string - */ public function getWebSiteUrl(): string { return $this->config['website_url']; } - /** - * @return string - */ public function getWebAppUrl(): string { return $this->config['web_app_url']; } - /** - * @param string $token - * @return string - */ public function getEmailConfirmationLink(string $token): string { return str_replace('{token}', $token, $this->config['email_confirmation_link']); } - /** - * @param string $code - * @return string - */ public function getPasswordResetLink(string $code): string { return str_replace('{code}', $code, $this->config['password_reset_link']); } - /** - * @param int $walletId - * @return string - */ public function getWalletLink(int $walletId): string { return str_replace('{wallet}', (string) $walletId, $this->config['wallet_link']); } + + public function getDbEncrypterKey(): string + { + return (string) $this->config['db_encrypter_key']; + } } diff --git a/app/src/Database/Charge.php b/app/src/Database/Charge.php index bcd5c4ca..29602172 100644 --- a/app/src/Database/Charge.php +++ b/app/src/Database/Charge.php @@ -8,9 +8,12 @@ use Cycle\Annotated\Annotation as ORM; use Cycle\ORM\Collection\Pivoted\PivotedCollection; use Cycle\ORM\Entity\Behavior; +use Cycle\ORM\Parser\Typecast; use Ramsey\Uuid\UuidInterface; -#[ORM\Entity(repository: ChargeRepository::class)] +#[ORM\Entity(repository: ChargeRepository::class, typecast: [ + Typecast::class, +])] #[Behavior\Uuid\Uuid4(field: 'id', column: 'id')] #[Behavior\UpdatedAt(field: 'updatedAt', column: 'updated_at')] class Charge @@ -33,13 +36,13 @@ class Charge #[ORM\Column('decimal(13,2)')] public float $amount = 0.0; - #[ORM\Column('string')] + #[ORM\Column(type: 'string')] public string $title = ''; #[ORM\Column(type: 'integer', name: 'currency_exchange_id', nullable: true)] public int|null $currencyExchangeId = null; - #[ORM\Column('text')] + #[ORM\Column(type: 'text')] public string $description = ''; #[ORM\Column(type: 'datetime', name: 'created_at')] diff --git a/app/src/Database/Encrypter/Encrypter.php b/app/src/Database/Encrypter/Encrypter.php new file mode 100644 index 00000000..ec14906a --- /dev/null +++ b/app/src/Database/Encrypter/Encrypter.php @@ -0,0 +1,55 @@ +key = $this->config->getDbEncrypterKey(); + } + + public function encrypt(string $value): string + { + if (! $this->isEnabled()) { + return $value; + } + + $payload = openssl_encrypt($value, static::CIPHER, $this->key); + + if ($payload === false) { + throw new EncrypterException('Encryption unsuccessful: ' . openssl_error_string()); + } + + return $payload; + } + + public function decrypt(string $payload): string + { + if (! $this->isEnabled()) { + return $payload; + } + + $value = openssl_decrypt($payload, static::CIPHER, $this->key); + + if ($value === false) { + throw new EncrypterException('Decryption unsuccessful: ' . openssl_error_string()); + } + + return $value; + } + + private function isEnabled(): bool + { + return !empty($this->key); + } +} diff --git a/app/src/Database/Encrypter/EncrypterInterface.php b/app/src/Database/Encrypter/EncrypterInterface.php new file mode 100644 index 00000000..19714cb4 --- /dev/null +++ b/app/src/Database/Encrypter/EncrypterInterface.php @@ -0,0 +1,12 @@ + $rules + * @return array + */ + public function setRules(array $rules): array + { + foreach ($rules as $key => $rule) { + if ($rule !== self::RULE) { + continue; + } + + unset($rules[$key]); + + $this->rules[$key] = $rule; + } + + return $rules; + } + + public function cast(array $data): array + { + foreach ($this->rules as $column => $rule) { + if (! isset($data[$column]) || !is_string($data[$column])) { + continue; + } + + try { + $data[$column] = $this->encrypter->decrypt($data[$column]); + } catch (EncrypterException $exception) { + $original = $data[$column]; + + $data[$column] = ''; + + if (empty($original)) { + continue; + } + + $this->logger->warning('Unable to decrypt database column', [ + 'column' => $column, + 'message' => $exception->getMessage(), + 'value' => $original, + ]); + } + } + + return $data; + } + + public function uncast(array $data): array + { + foreach ($this->rules as $column => $rule) { + if (! isset($data[$column]) || !is_string($data[$column]) || empty($data[$column])) { + continue; + } + + try { + $data[$column] = $this->encrypter->encrypt($data[$column]); + } catch (EncrypterException $exception) { + $this->logger->warning('Unable to encrypt database column', [ + 'column' => $column, + 'message' => $exception->getMessage(), + ]); + } + } + + return $data; + } +} diff --git a/app/src/Database/User.php b/app/src/Database/User.php index 431b8f31..da1fd392 100644 --- a/app/src/Database/User.php +++ b/app/src/Database/User.php @@ -4,6 +4,7 @@ namespace App\Database; +use App\Database\Typecast\EncryptedTypecast; use App\Database\Typecast\JsonTypecast; use App\Repository\UserRepository; use App\Security\PasswordContainerInterface; @@ -14,6 +15,7 @@ #[ORM\Entity(repository: UserRepository::class, typecast: [ Typecast::class, JsonTypecast::class, + EncryptedTypecast::class, ])] #[ORM\Table(indexes: [ new ORM\Table\Index(columns: ['nick_name'], unique: true), @@ -26,16 +28,16 @@ class User implements PasswordContainerInterface #[ORM\Column('primary')] public int|null $id = null; - #[ORM\Column('string')] + #[ORM\Column(type: 'string', typecast: EncryptedTypecast::RULE)] public string $name = ''; - #[ORM\Column(type: 'string', name: 'last_name', nullable: true)] + #[ORM\Column(type: 'string', name: 'last_name', nullable: true, typecast: EncryptedTypecast::RULE)] public string|null $lastName = null; - #[ORM\Column(type: 'string', name: 'nick_name')] + #[ORM\Column(type: 'string', name: 'nick_name', typecast: EncryptedTypecast::RULE)] public string $nickName = ''; - #[ORM\Column('string')] + #[ORM\Column(type: 'string', typecast: EncryptedTypecast::RULE)] public string $email = ''; #[ORM\Column(type: 'boolean', name: 'is_email_confirmed', default: false)] diff --git a/app/src/Database/Wallet.php b/app/src/Database/Wallet.php index ba878ad8..6422120a 100644 --- a/app/src/Database/Wallet.php +++ b/app/src/Database/Wallet.php @@ -4,14 +4,19 @@ namespace App\Database; +use App\Database\Typecast\EncryptedTypecast; use App\Repository\WalletRepository; use App\Service\Sort\Sortable; use Cycle\Annotated\Annotation as ORM; use Cycle\ORM\Collection\Pivoted\PivotedCollection; +use Cycle\ORM\Parser\Typecast; use Doctrine\Common\Collections\ArrayCollection; use Cycle\ORM\Entity\Behavior; -#[ORM\Entity(repository: WalletRepository::class)] +#[ORM\Entity(repository: WalletRepository::class, typecast: [ + Typecast::class, + EncryptedTypecast::class, +])] #[Behavior\CreatedAt(field: 'createdAt', column: 'created_at')] #[Behavior\UpdatedAt(field: 'updatedAt', column: 'updated_at')] class Wallet implements Sortable @@ -19,10 +24,10 @@ class Wallet implements Sortable #[ORM\Column('primary')] public int|null $id = null; - #[ORM\Column('string')] + #[ORM\Column(type: 'string', typecast: EncryptedTypecast::RULE)] public string $name = ''; - #[ORM\Column('string')] + #[ORM\Column(type: 'string', typecast: EncryptedTypecast::RULE)] public string $slug = ''; #[ORM\Column(type: 'decimal(13,2)', name: 'total_amount', default: 0.0)] diff --git a/app/src/Repository/UserRepository.php b/app/src/Repository/UserRepository.php index 8bb28bd1..04b0e1a2 100644 --- a/app/src/Repository/UserRepository.php +++ b/app/src/Repository/UserRepository.php @@ -4,6 +4,7 @@ namespace App\Repository; +use App\Database\Encrypter\EncrypterInterface; use App\Database\User; use Cycle\ORM\Select; use Cycle\ORM\Select\Repository; @@ -13,6 +14,13 @@ class UserRepository extends Repository implements ActorProviderInterface { + public function __construct( + Select $select, + private readonly EncrypterInterface $encrypter, + ) { + parent::__construct($select); + } + /** * @param \Spiral\Auth\TokenInterface $token * @return object|null @@ -32,7 +40,9 @@ public function getActor(TokenInterface $token): ?object */ public function findByEmail(string $email): object|null { - return $this->findOne(['email' => $email]); + return $this->findOne([ + 'email' => $this->encrypter->encrypt($email), + ]); } /** diff --git a/app/src/Request/CheckNickNameRequest.php b/app/src/Request/CheckNickNameRequest.php index 508a3022..8c589297 100644 --- a/app/src/Request/CheckNickNameRequest.php +++ b/app/src/Request/CheckNickNameRequest.php @@ -34,7 +34,7 @@ public static function validationRules(): array 'type::notEmpty', ['string::longer', 3], ['string::regexp', '/^[a-zA-Z0-9_]*$/'], - ['unique::verify', User::class, 'nickName', [], ['id'], 'error' => 'error_nick_name_claimed'], + ['unique::verify', User::class, 'nickName', [], ['id'], true, 'error' => 'error_nick_name_claimed'], ], ]; } diff --git a/app/src/Request/ForgotPasswordCreateRequest.php b/app/src/Request/ForgotPasswordCreateRequest.php index 27f9591e..a55208e1 100644 --- a/app/src/Request/ForgotPasswordCreateRequest.php +++ b/app/src/Request/ForgotPasswordCreateRequest.php @@ -22,7 +22,7 @@ public function filterDefinition(): FilterDefinitionInterface 'email' => [ 'address::email', 'type::notEmpty', - ['entity::exists', User::class, 'email'], + ['encrypted-entity::exists', User::class, 'email'], ], ]); } diff --git a/app/src/Request/Profile/UpdateBasicRequest.php b/app/src/Request/Profile/UpdateBasicRequest.php index 524f926e..5e1ca520 100644 --- a/app/src/Request/Profile/UpdateBasicRequest.php +++ b/app/src/Request/Profile/UpdateBasicRequest.php @@ -55,7 +55,7 @@ public function filterDefinition(): FilterDefinitionInterface 'type::notEmpty', ['string::longer', 3], ['string::regexp', '/^[a-zA-Z0-9_]*$/'], - ['unique::verify', User::class, 'nickName', [], ['id']], + ['unique::verify', User::class, 'nickName', [], ['id'], true], ], 'defaultCurrencyCode' => [ 'is_string', diff --git a/app/src/Request/RegisterRequest.php b/app/src/Request/RegisterRequest.php index 4c046572..18f929ac 100644 --- a/app/src/Request/RegisterRequest.php +++ b/app/src/Request/RegisterRequest.php @@ -56,12 +56,12 @@ public function filterDefinition(): FilterDefinitionInterface 'type::notEmpty', ['string::longer', 3], ['string::regexp', '/^[a-zA-Z0-9_]*$/'], - ['entity::unique', User::class, 'nickName'], + ['encrypted-entity::unique', User::class, 'nickName'], ], 'email' => [ 'address::email', 'type::notEmpty', - ['entity::unique', User::class, 'email'], + ['encrypted-entity::unique', User::class, 'email'], ], 'password' => [ 'type::notEmpty', diff --git a/app/src/Request/Wallet/CreateRequest.php b/app/src/Request/Wallet/CreateRequest.php index 13f24c77..a9ea0a2c 100644 --- a/app/src/Request/Wallet/CreateRequest.php +++ b/app/src/Request/Wallet/CreateRequest.php @@ -36,7 +36,7 @@ public function filterDefinition(): FilterDefinitionInterface 'slug' => [ 'is_string', ['string::regexp', '/^[a-zA-Z0-9\-_]*$/'], - ['entity::unique', Wallet::class, 'slug'], + ['encrypted-entity::unique', Wallet::class, 'slug'], ], 'isPublic' => [ 'type::boolean', diff --git a/app/src/Security/EncryptedEntityChecker.php b/app/src/Security/EncryptedEntityChecker.php new file mode 100644 index 00000000..079e08c3 --- /dev/null +++ b/app/src/Security/EncryptedEntityChecker.php @@ -0,0 +1,47 @@ +orm); + } + + public function exists( + mixed $value, + string $role, + ?string $field = null, + bool $ignoreCase = false, + bool $multiple = false, + ): bool { + if (is_string($value)) { + $value = $this->encrypter->encrypt($value); + } + + return parent::exists($value, $role, $field, $ignoreCase, $multiple); + } + + public function unique( + mixed $value, + string $role, + string $field, + array $withFields = [], + bool $ignoreCase = false, + ): bool { + if (is_string($value)) { + $value = $this->encrypter->encrypt($value); + } + + return parent::unique($value, $role, $field, $withFields, $ignoreCase); + } +} diff --git a/app/src/Security/UniqueChecker.php b/app/src/Security/UniqueChecker.php index e2d9a4bf..012d9f45 100644 --- a/app/src/Security/UniqueChecker.php +++ b/app/src/Security/UniqueChecker.php @@ -4,6 +4,7 @@ namespace App\Security; +use App\Database\Encrypter\EncrypterInterface; use Cycle\ORM\ORMInterface; use Spiral\Validator\AbstractChecker; @@ -13,14 +14,15 @@ class UniqueChecker extends AbstractChecker 'verify' => 'error_value_is_not_unique' ]; - public function __construct(private ORMInterface $orm) - { - } + public function __construct( + private readonly ORMInterface $orm, + private readonly EncrypterInterface $encrypter + ) {} - public function verify(mixed $value, string $role, string $field, array $withFields = [], array $exceptFields = []): bool + public function verify(mixed $value, string $role, string $field, array $withFields = [], array $exceptFields = [], bool $encrypted = false): bool { $values = $this->withValues($withFields); - $values[$field] = $value; + $values[$field] = $encrypted ? $this->encrypter->encrypt($value) : $value; $exceptValues = $this->withValues($exceptFields); diff --git a/app/src/Service/Filter/Filter.php b/app/src/Service/Filter/Filter.php index 65285da4..76219ea5 100644 --- a/app/src/Service/Filter/Filter.php +++ b/app/src/Service/Filter/Filter.php @@ -16,12 +16,12 @@ trait Filter public function filter(array $query): static { + $this->filter = []; + if (count($query) === 0) { return $this; } - $this->filter = []; - foreach (FilterType::cases() as $type) { if (! array_key_exists($type->value, $query)) { continue; diff --git a/tests/Feature/Command/EncryptKeyGenerateTest.php b/tests/Feature/Command/EncryptKeyGenerateTest.php new file mode 100644 index 00000000..499b39f9 --- /dev/null +++ b/tests/Feature/Command/EncryptKeyGenerateTest.php @@ -0,0 +1,24 @@ +setContainer($this->getContainer()); + + $input = $this->getMockBuilder(InputInterface::class)->getMock(); + $output = $this->getMockBuilder(OutputInterface::class)->getMock(); + + $this->assertEquals(0, $command->run($input, $output)); + } +} diff --git a/tests/Feature/Controller/Auth/EmailConfirmationControllerTest.php b/tests/Feature/Controller/Auth/EmailConfirmationControllerTest.php index bff1ac35..f526b47c 100644 --- a/tests/Feature/Controller/Auth/EmailConfirmationControllerTest.php +++ b/tests/Feature/Controller/Auth/EmailConfirmationControllerTest.php @@ -94,8 +94,9 @@ public function testConfirm(): void ]); $this->assertDatabaseHas('users', [ - 'email' => $user->email, 'is_email_confirmed' => true, + ], [ + 'email' => $user->email, ]); } @@ -117,8 +118,9 @@ public function testConfirmWithExpiredToken(): void $this->assertArrayHasKey('error', $body); $this->assertDatabaseHas('users', [ - 'email' => $user->email, 'is_email_confirmed' => false, + ], [ + 'email' => $user->email, ]); } @@ -138,8 +140,9 @@ public function testConfirmWithMissingToken(): void $this->assertArrayHasKey('error', $body); $this->assertDatabaseHas('users', [ - 'email' => $user->email, 'is_email_confirmed' => false, + ], [ + 'email' => $user->email, ]); } diff --git a/tests/Feature/Controller/Auth/RegisterController/RegisterTest.php b/tests/Feature/Controller/Auth/RegisterController/RegisterTest.php index 0aad001f..0eec830b 100644 --- a/tests/Feature/Controller/Auth/RegisterController/RegisterTest.php +++ b/tests/Feature/Controller/Auth/RegisterController/RegisterTest.php @@ -63,7 +63,7 @@ public function testUserCreated(): void $this->assertArrayHasKey('accessToken', $body); $this->assertArrayHasKey('refreshToken', $body); - $this->assertDatabaseHas('users', ['email' => $user->email]); + $this->assertDatabaseHas('users', [], ['email' => $user->email]); } public function testUserStoreFailed(): void @@ -134,7 +134,7 @@ public function testEmailConfirmationServiceFailStillStoreUser(): void $this->assertArrayHasKey('accessToken', $body); $this->assertArrayHasKey('refreshToken', $body); - $this->assertDatabaseHas('users', ['email' => $user->email]); + $this->assertDatabaseHas('users', [], ['email' => $user->email]); } public function testValidationFailsByEmptyForm(): void diff --git a/tests/Feature/Controller/Profile/ProfileControllerTest.php b/tests/Feature/Controller/Profile/ProfileControllerTest.php index e3ec073e..380762ff 100644 --- a/tests/Feature/Controller/Profile/ProfileControllerTest.php +++ b/tests/Feature/Controller/Profile/ProfileControllerTest.php @@ -190,9 +190,10 @@ public function testUpdate(): void $this->assertDatabaseHas('users', [ 'id' => $user->id, + 'default_currency_code' => $user->defaultCurrencyCode, + ], [ 'name' => $user->name, 'last_name' => $user->lastName, - 'default_currency_code' => $user->defaultCurrencyCode, ]); } diff --git a/tests/Feature/Controller/Wallets/IndexControllerTest.php b/tests/Feature/Controller/Wallets/IndexControllerTest.php index d448642c..1de37aca 100644 --- a/tests/Feature/Controller/Wallets/IndexControllerTest.php +++ b/tests/Feature/Controller/Wallets/IndexControllerTest.php @@ -275,4 +275,49 @@ public function testTotalWithDateFiltersReturnsFilteredTotalByTag(array $total, $this->assertArrayContains($total['income'], $body, 'data.totalIncomeAmount'); $this->assertArrayContains($total['expense'], $body, 'data.totalExpenseAmount'); } + + public function testTotalWithDateFiltersDoesNotOverlapsBetweenRequests() + { + $auth = $this->makeAuth($user = $this->userFactory->create()); + $wallet = $this->walletFactory->forUser($user)->create(); + + for ($i = 1; $i <= 4; $i++) { + $charge = ChargeFactory::make(); + $charge->type = Charge::TYPE_INCOME; + $charge->amount = 100 + $i; + $charge->createdAt = new \DateTimeImmutable("0{$i}-06-2022"); + $this->chargeFactory->forUser($user)->forWallet($wallet)->create($charge); + } + + for ($i = 1; $i <= 4; $i++) { + $charge = ChargeFactory::make(); + $charge->type = Charge::TYPE_EXPENSE; + $charge->amount = 50 + $i; + $charge->createdAt = new \DateTimeImmutable("0{$i}-06-2022"); + $this->chargeFactory->forUser($user)->forWallet($wallet)->create($charge); + } + + $response = $this->withAuth($auth)->get("/wallets/{$wallet->id}/total", [ + 'date-from' => '02-06-2022', + 'date-to' => '03-06-2022', + ]); + + $response->assertOk(); + + $body = $this->getJsonResponseBody($response); + + $this->assertArrayContains(100, $body, 'data.totalAmount'); + $this->assertArrayContains(205, $body, 'data.totalIncomeAmount'); + $this->assertArrayContains(105, $body, 'data.totalExpenseAmount'); + + $response = $this->withAuth($auth)->get("/wallets/{$wallet->id}/total"); + + $response->assertOk(); + + $body = $this->getJsonResponseBody($response); + + $this->assertArrayContains(200, $body, 'data.totalAmount'); + $this->assertArrayContains(410, $body, 'data.totalIncomeAmount'); + $this->assertArrayContains(210, $body, 'data.totalExpenseAmount'); + } } diff --git a/tests/Feature/Controller/Wallets/WalletsControllerTest.php b/tests/Feature/Controller/Wallets/WalletsControllerTest.php index 683273ff..a09cbe91 100644 --- a/tests/Feature/Controller/Wallets/WalletsControllerTest.php +++ b/tests/Feature/Controller/Wallets/WalletsControllerTest.php @@ -119,9 +119,10 @@ public function testCreateStoreWallet(): void $this->assertArrayHasKey('id', $body['data']); $this->assertDatabaseHas('wallets', [ + 'default_currency_code' => $wallet->defaultCurrencyCode, + ], [ 'name' => $wallet->name, 'slug' => $wallet->slug, - 'default_currency_code' => $wallet->defaultCurrencyCode, ]); $this->assertDatabaseHas('user_wallets', [ @@ -271,10 +272,11 @@ public function testUpdateWalletUpdated(): void $this->assertArrayContains($otherWallet->defaultCurrencyCode, $body, 'data.defaultCurrencyCode'); $this->assertDatabaseHas('wallets', [ - 'name' => $otherWallet->name, - 'slug' => $wallet->slug, 'is_public' => $otherWallet->isPublic, 'default_currency_code' => $otherWallet->defaultCurrencyCode, + ], [ + 'name' => $otherWallet->name, + 'slug' => $wallet->slug, ]); } @@ -452,6 +454,7 @@ public function testDeleteThrownException(): void $this->assertDatabaseHas('wallets', [ 'id' => $wallet->id, + ], [ 'name' => $wallet->name, 'slug' => $wallet->slug, ]); diff --git a/tests/Feature/Database/Encrypter/EncrypterTest.php b/tests/Feature/Database/Encrypter/EncrypterTest.php new file mode 100644 index 00000000..1a5f72f8 --- /dev/null +++ b/tests/Feature/Database/Encrypter/EncrypterTest.php @@ -0,0 +1,26 @@ +getMockBuilder(AppConfig::class)->onlyMethods(['getDbEncrypterKey'])->getMock(); + $config->method('getDbEncrypterKey')->willReturn(''); + + $encrypter = new Encrypter($config); + + $string = Fixtures::string(); + + $this->assertEquals($string, $encrypter->encrypt($string)); + $this->assertEquals($string, $encrypter->decrypt($string)); + } +} diff --git a/tests/Feature/Database/Typecast/EncryptedTypecastTest.php b/tests/Feature/Database/Typecast/EncryptedTypecastTest.php new file mode 100644 index 00000000..aa918e8b --- /dev/null +++ b/tests/Feature/Database/Typecast/EncryptedTypecastTest.php @@ -0,0 +1,72 @@ +getMockBuilder(EncrypterInterface::class)->getMock(); + $encrypter->method('decrypt')->willThrowException(new EncrypterException()); + + $typecast = new EncryptedTypecast($this->getContainer()->get(LoggerInterface::class), $encrypter); + $typecast->setRules([ + 'column' => EncryptedTypecast::RULE, + 'other' => 'string', + ]); + + $data = $typecast->cast([ + 'column' => 1, + ]); + + $this->assertArrayHasKey('column', $data); + $this->assertEquals(1, $data['column']); + + $data = $typecast->cast([ + 'column' => 'value', + ]); + + $this->assertArrayHasKey('column', $data); + $this->assertEmpty($data['column']); + + $data = $typecast->cast([ + 'column' => '', + ]); + + $this->assertArrayHasKey('column', $data); + $this->assertEmpty($data['column']); + } + + public function testUncastException(): void + { + $encrypter = $this->getMockBuilder(EncrypterInterface::class)->getMock(); + $encrypter->method('encrypt')->willThrowException(new EncrypterException()); + + $typecast = new EncryptedTypecast($this->getContainer()->get(LoggerInterface::class), $encrypter); + $typecast->setRules([ + 'column' => EncryptedTypecast::RULE, + ]); + + $data = $typecast->uncast([ + 'column' => 'value', + ]); + + $this->assertArrayHasKey('column', $data); + $this->assertEquals('value', $data['column']); + + $data = $typecast->cast([ + 'column' => '', + ]); + + $this->assertArrayHasKey('column', $data); + $this->assertEmpty($data['column']); + } +} diff --git a/tests/Feature/Security/UniqueCheckerTest.php b/tests/Feature/Security/UniqueCheckerTest.php index add10ada..011e155a 100644 --- a/tests/Feature/Security/UniqueCheckerTest.php +++ b/tests/Feature/Security/UniqueCheckerTest.php @@ -4,6 +4,7 @@ namespace Tests\Feature\Security; +use App\Database\Encrypter\EncrypterInterface; use App\Security\UniqueChecker; use Cycle\ORM\ORMInterface; use Tests\Fixtures; @@ -13,7 +14,10 @@ class UniqueCheckerTest extends TestCase { public function testVerifyEmptyRole(): void { - $checker = new UniqueChecker($this->getContainer()->get(ORMInterface::class)); + $checker = new UniqueChecker( + $this->getContainer()->get(ORMInterface::class), + $this->getContainer()->get(EncrypterInterface::class) + ); $this->assertFalse($checker->verify(Fixtures::string(), '', Fixtures::string())); } diff --git a/tests/TestCase.php b/tests/TestCase.php index 6c1a87c7..953c7905 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -57,6 +57,7 @@ protected function scopedDatabase(): void /** @var \Cycle\Database\DatabaseInterface $db */ $db = $this->getContainer()->get(DatabaseInterface::class); + $db->begin(); $this->getContainer()->get(FinalizerInterface::class)->addFinalizer(static function () use ($db) { $db->rollback(); diff --git a/tests/Traits/InteractsWithDatabase.php b/tests/Traits/InteractsWithDatabase.php index b256aae3..0191088d 100644 --- a/tests/Traits/InteractsWithDatabase.php +++ b/tests/Traits/InteractsWithDatabase.php @@ -4,32 +4,59 @@ namespace Tests\Traits; +use App\Database\Encrypter\EncrypterInterface; use Cycle\Database\DatabaseInterface; trait InteractsWithDatabase { + protected function encryptWhere(array $where): array + { + $encrypter = $this->getContainer()->get(EncrypterInterface::class); + + foreach ($where as $key => $value) { + $where[$key] = $encrypter->encrypt($value); + } + + return $where; + } + public function getDatabase(): DatabaseInterface { return $this->getContainer()->get(DatabaseInterface::class); } - public function assertDatabaseHas(string $table, array $where) + public function assertDatabaseHas(string $table, array $where, array $whereEncrypted = []) { - $data = $this->getDatabase()->select()->from($table)->where($where)->fetchAll(); + $data = $this->getDatabase() + ->select() + ->from($table) + ->where($where) + ->where($this->encryptWhere($whereEncrypted)) + ->fetchAll(); $this->assertNotEmpty($data); } - public function assertDatabaseMissing(string $table, array $where) + public function assertDatabaseMissing(string $table, array $where, array $whereEncrypted = []) { - $data = $this->getDatabase()->select()->from($table)->where($where)->fetchAll(); + $data = $this->getDatabase() + ->select() + ->from($table) + ->where($where) + ->where($this->encryptWhere($whereEncrypted)) + ->fetchAll(); $this->assertEmpty($data); } - public function assertDatabaseCount(int $expected, string $table, array $where) + public function assertDatabaseCount(int $expected, string $table, array $where, array $whereEncrypted = []) { - $data = $this->getDatabase()->select()->from($table)->where($where)->fetchAll(); + $data = $this->getDatabase() + ->select() + ->from($table) + ->where($where) + ->where($this->encryptWhere($whereEncrypted)) + ->fetchAll(); $this->assertCount($expected, $data); } From ba2c3da940d8be7f61c052ba31dd16d27c8a9523 Mon Sep 17 00:00:00 2001 From: Volodymyr Komarov Date: Wed, 19 Apr 2023 01:36:06 +0300 Subject: [PATCH 3/9] Fix code style issues --- .github/workflows/quality.yml | 2 +- app/src/Database/Typecast/EncryptedTypecast.php | 3 ++- app/src/Repository/UserRepository.php | 2 +- app/src/Security/UniqueChecker.php | 3 ++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index d679250d..fbfa5102 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -132,7 +132,7 @@ jobs: php app.php migrate - name: Run PHPUnit - run: ./vendor/bin/phpunit --coverage-clover=coverage.xml + run: ./vendor/bin/phpunit --coverage-clover=coverage.xml --stop-on-failure - name: Upload Coverage To Codecov continue-on-error: true diff --git a/app/src/Database/Typecast/EncryptedTypecast.php b/app/src/Database/Typecast/EncryptedTypecast.php index a4b765d1..3f17149f 100644 --- a/app/src/Database/Typecast/EncryptedTypecast.php +++ b/app/src/Database/Typecast/EncryptedTypecast.php @@ -19,7 +19,8 @@ final class EncryptedTypecast implements CastableInterface, UncastableInterface public function __construct( private readonly LoggerInterface $logger, private readonly EncrypterInterface $encrypter, - ) {} + ) { + } /** * @param array $rules diff --git a/app/src/Repository/UserRepository.php b/app/src/Repository/UserRepository.php index 04b0e1a2..cfd78c0c 100644 --- a/app/src/Repository/UserRepository.php +++ b/app/src/Repository/UserRepository.php @@ -17,7 +17,7 @@ class UserRepository extends Repository implements ActorProviderInterface public function __construct( Select $select, private readonly EncrypterInterface $encrypter, - ) { + ) { parent::__construct($select); } diff --git a/app/src/Security/UniqueChecker.php b/app/src/Security/UniqueChecker.php index 012d9f45..19b2ac63 100644 --- a/app/src/Security/UniqueChecker.php +++ b/app/src/Security/UniqueChecker.php @@ -17,7 +17,8 @@ class UniqueChecker extends AbstractChecker public function __construct( private readonly ORMInterface $orm, private readonly EncrypterInterface $encrypter - ) {} + ) { + } public function verify(mixed $value, string $role, string $field, array $withFields = [], array $exceptFields = [], bool $encrypted = false): bool { From 6593ef3302c57d32d80c9831f48a051c8a15992b Mon Sep 17 00:00:00 2001 From: Volodymyr Komarov Date: Wed, 19 Apr 2023 01:51:27 +0300 Subject: [PATCH 4/9] Try without encryption --- phpunit.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/phpunit.xml b/phpunit.xml index 1d73a685..b9e6a6e5 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -36,5 +36,6 @@ + From 2f5b3ac8c3ba44abf0a99e0d3474b1a022b1fddc Mon Sep 17 00:00:00 2001 From: Volodymyr Komarov Date: Wed, 19 Apr 2023 01:59:17 +0300 Subject: [PATCH 5/9] Revert attempt to fix --- phpunit.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/phpunit.xml b/phpunit.xml index b9e6a6e5..1d73a685 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -36,6 +36,5 @@ - From 275f295c6f171a97e4d94331990b5ba594073fc1 Mon Sep 17 00:00:00 2001 From: Volodymyr Komarov Date: Wed, 19 Apr 2023 19:58:01 +0300 Subject: [PATCH 6/9] Add a fix for transactional database test --- tests/TestCase.php | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/tests/TestCase.php b/tests/TestCase.php index 953c7905..4c309e41 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -59,13 +59,31 @@ protected function scopedDatabase(): void $db = $this->getContainer()->get(DatabaseInterface::class); $db->begin(); - $this->getContainer()->get(FinalizerInterface::class)->addFinalizer(static function () use ($db) { - $db->rollback(); - }); +// $this->getContainer()->get(FinalizerInterface::class)->addFinalizer(static function () use ($db) { +// $db->rollback(); +// }); + } + + protected function scopedDatabaseFinalise(): void + { + if (! $this instanceof DatabaseTransaction) { + return; + } + + /** @var \Cycle\Database\DatabaseInterface $db */ + $db = $this->getContainer()->get(DatabaseInterface::class); + + while ($db->getDriver()->getTransactionLevel() !== 0) { + if (! $db->rollback()) { + return; + } + } } protected function tearDown(): void { + $this->scopedDatabaseFinalise(); + parent::tearDown(); $container = $this->getContainer(); From c4a80ab2de8b5ce64b8fce9669c9b850c8069fea Mon Sep 17 00:00:00 2001 From: Volodymyr Komarov Date: Wed, 19 Apr 2023 23:59:26 +0300 Subject: [PATCH 7/9] Add a fix for transactional database test --- tests/TestCase.php | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/TestCase.php b/tests/TestCase.php index 4c309e41..628f237f 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -59,9 +59,16 @@ protected function scopedDatabase(): void $db = $this->getContainer()->get(DatabaseInterface::class); $db->begin(); -// $this->getContainer()->get(FinalizerInterface::class)->addFinalizer(static function () use ($db) { -// $db->rollback(); -// }); + $this->getContainer()->get(FinalizerInterface::class)->addFinalizer(static function () use ($db) { + /** @var \Cycle\Database\DatabaseInterface $db */ + $db = $this->getContainer()->get(DatabaseInterface::class); + + while ($db->getDriver()->getTransactionLevel() !== 0) { + if (! $db->rollback()) { + return; + } + } + }); } protected function scopedDatabaseFinalise(): void @@ -82,7 +89,7 @@ protected function scopedDatabaseFinalise(): void protected function tearDown(): void { - $this->scopedDatabaseFinalise(); +// $this->scopedDatabaseFinalise(); parent::tearDown(); From 1f32531bf90a174eb38fae32a3032afa95837fc6 Mon Sep 17 00:00:00 2001 From: Volodymyr Komarov Date: Thu, 20 Apr 2023 00:00:18 +0300 Subject: [PATCH 8/9] Add a fix for transactional database test --- tests/TestCase.php | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/tests/TestCase.php b/tests/TestCase.php index 628f237f..f1fa67df 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -58,17 +58,6 @@ protected function scopedDatabase(): void /** @var \Cycle\Database\DatabaseInterface $db */ $db = $this->getContainer()->get(DatabaseInterface::class); $db->begin(); - - $this->getContainer()->get(FinalizerInterface::class)->addFinalizer(static function () use ($db) { - /** @var \Cycle\Database\DatabaseInterface $db */ - $db = $this->getContainer()->get(DatabaseInterface::class); - - while ($db->getDriver()->getTransactionLevel() !== 0) { - if (! $db->rollback()) { - return; - } - } - }); } protected function scopedDatabaseFinalise(): void @@ -89,7 +78,7 @@ protected function scopedDatabaseFinalise(): void protected function tearDown(): void { -// $this->scopedDatabaseFinalise(); + $this->scopedDatabaseFinalise(); parent::tearDown(); From 7eba8d67407d167eb05d46a705be187c42f5674d Mon Sep 17 00:00:00 2001 From: Volodymyr Komarov Date: Thu, 20 Apr 2023 00:06:31 +0300 Subject: [PATCH 9/9] Disable encryption for actions --- .env.actions | 2 +- .github/workflows/quality.yml | 2 +- phpunit.xml | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.env.actions b/.env.actions index 76276014..1959ef76 100644 --- a/.env.actions +++ b/.env.actions @@ -1,6 +1,6 @@ DEBUG=false ENCRYPTER_KEY={encrypt-key} -DB_ENCRYPTER_KEY={encrypt-key} +DB_ENCRYPTER_KEY= SAFE_MIGRATIONS=true VERBOSITY_LEVEL=basic MONOLOG_DEFAULT_CHANNEL=default diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index fbfa5102..d679250d 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -132,7 +132,7 @@ jobs: php app.php migrate - name: Run PHPUnit - run: ./vendor/bin/phpunit --coverage-clover=coverage.xml --stop-on-failure + run: ./vendor/bin/phpunit --coverage-clover=coverage.xml - name: Upload Coverage To Codecov continue-on-error: true diff --git a/phpunit.xml b/phpunit.xml index 1d73a685..b9e6a6e5 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -36,5 +36,6 @@ +