diff --git a/src/app/Packages/Domains/Interface/UserRepositroyInterface.php b/src/app/Packages/Domains/Interface/UserRepositroyInterface.php index a71cbd8e..ab2f3b00 100644 --- a/src/app/Packages/Domains/Interface/UserRepositroyInterface.php +++ b/src/app/Packages/Domains/Interface/UserRepositroyInterface.php @@ -2,10 +2,12 @@ namespace App\Packages\Domains\Interface; -use App\Packages\Features\CommandUseCases\UseCommand\User\CreateUserCommand; +use App\Packages\Domains\User\UserEntity; use App\Packages\Features\QueryUseCases\Dto\User\UserDto; interface UserRepositroyInterface { - public function createUser(CreateUserCommand $command): UserDto; + public function createUser(UserEntity $entity): UserDto; + + public function updateUser(UserEntity $entity): UserDto; } \ No newline at end of file diff --git a/src/app/Packages/Domains/Tests/User/Factory/UserEntityFactoryTest.php b/src/app/Packages/Domains/Tests/User/Factory/UserEntityFactoryTest.php index 4bede3f9..3b8119e2 100644 --- a/src/app/Packages/Domains/Tests/User/Factory/UserEntityFactoryTest.php +++ b/src/app/Packages/Domains/Tests/User/Factory/UserEntityFactoryTest.php @@ -33,6 +33,26 @@ public function test_build_returns_user_entity_with_correct_values(): void $this->assertSame($data['email'], $entity->getEmail()); $this->assertSame(AgeRange::Teens, $entity->getAgeRange()); $this->assertTrue($entity->getSubscription()->isFree()); + $this->assertNull($entity->getPasswordHash()); + } + + public function test_build_sets_password_hash_when_provided(): void + { + $faker = FakerFactory::create(); + + $entity = UserEntityFactory::build([ + 'id' => null, + 'first_name' => $faker->firstName(), + 'last_name' => $faker->lastName(), + 'email' => $faker->unique()->safeEmail(), + 'age_range' => '20s', + 'subscription_tier' => 'free', + 'subscription_expires_at' => null, + 'password_hash' => 'hashed_value', + ]); + + $this->assertNull($entity->getId()); + $this->assertSame('hashed_value', $entity->getPasswordHash()); } public function test_build_passes_expires_at_to_subscription(): void diff --git a/src/app/Packages/Domains/Tests/User/UserEntityTest.php b/src/app/Packages/Domains/Tests/User/UserEntityTest.php index 98065e55..94c86356 100644 --- a/src/app/Packages/Domains/Tests/User/UserEntityTest.php +++ b/src/app/Packages/Domains/Tests/User/UserEntityTest.php @@ -17,12 +17,13 @@ private function buildEntity(array $overrides = []): UserEntity $subscription = new Subscription(SubscriptionTier::Free, null); return new UserEntity( - $overrides['id'] ?? $faker->unique()->randomNumber(5), - $overrides['first_name'] ?? $faker->firstName(), - $overrides['last_name'] ?? $faker->lastName(), - $overrides['email'] ?? $faker->unique()->safeEmail(), - $overrides['age_range'] ?? AgeRange::Twenties, - $overrides['subscription'] ?? $subscription, + id: array_key_exists('id', $overrides) ? $overrides['id'] : $faker->unique()->randomNumber(5), + firstName: $overrides['first_name'] ?? $faker->firstName(), + lastName: $overrides['last_name'] ?? $faker->lastName(), + email: $overrides['email'] ?? $faker->unique()->safeEmail(), + ageRange: $overrides['age_range'] ?? AgeRange::Twenties, + subscription: $overrides['subscription'] ?? $subscription, + passwordHash: $overrides['password_hash'] ?? null, ); } @@ -32,6 +33,12 @@ public function test_getId_returns_correct_value(): void $this->assertSame(42, $entity->getId()); } + public function test_getId_returns_null_when_not_yet_persisted(): void + { + $entity = $this->buildEntity(['id' => null]); + $this->assertNull($entity->getId()); + } + public function test_getFirstName_returns_correct_value(): void { $entity = $this->buildEntity(['first_name' => 'John']); @@ -68,4 +75,16 @@ public function test_getSubscription_returns_subscription_instance(): void $entity = $this->buildEntity(['subscription' => $subscription]); $this->assertSame($subscription, $entity->getSubscription()); } + + public function test_getPasswordHash_returns_hashed_value(): void + { + $entity = $this->buildEntity(['password_hash' => 'hashed_secret']); + $this->assertSame('hashed_secret', $entity->getPasswordHash()); + } + + public function test_getPasswordHash_returns_null_when_not_set(): void + { + $entity = $this->buildEntity(['password_hash' => null]); + $this->assertNull($entity->getPasswordHash()); + } } \ No newline at end of file diff --git a/src/app/Packages/Domains/Tests/UserRepositoryTest.php b/src/app/Packages/Domains/Tests/UserRepositoryTest.php index fb93428e..1801cf18 100644 --- a/src/app/Packages/Domains/Tests/UserRepositoryTest.php +++ b/src/app/Packages/Domains/Tests/UserRepositoryTest.php @@ -4,13 +4,14 @@ use App\Models\User; use App\Packages\Domains\UserRepository; -use App\Packages\Features\CommandUseCases\UseCommand\User\CreateUserCommand; +use App\Packages\Domains\User\Factory\UserEntityFactory; +use App\Packages\Domains\User\UserEntity; use App\Packages\Features\QueryUseCases\Dto\User\UserDto; use Exception; use Faker\Factory as FakerFactory; use Mockery; use Mockery\MockInterface; -use Tests\TestCase; +use PHPUnit\Framework\TestCase; class UserRepositoryTest extends TestCase { @@ -20,26 +21,27 @@ protected function tearDown(): void parent::tearDown(); } - private function buildCommand(): CreateUserCommand + private function buildEntity(): UserEntity { $faker = FakerFactory::create(); - return CreateUserCommand::fromArray([ + return UserEntityFactory::build([ + 'id' => null, 'first_name' => $faker->firstName(), 'last_name' => $faker->lastName(), 'email' => $faker->unique()->safeEmail(), - 'password' => $faker->password(8), 'age_range' => $faker->randomElement(['teens', '20s', '30s', '40s', '50s', '60plus']), 'subscription_tier' => $faker->randomElement(['free', 'premium']), 'subscription_expires_at' => null, + 'password_hash' => 'hashed_password', ]); } - private function buildUserModelMock(bool $wasRecentlyCreated): User + private function buildAttributes(): array { $faker = FakerFactory::create(); - $attributes = [ + return [ 'id' => $faker->unique()->randomNumber(5), 'first_name' => $faker->firstName(), 'last_name' => $faker->lastName(), @@ -48,6 +50,11 @@ private function buildUserModelMock(bool $wasRecentlyCreated): User 'subscription_tier' => 'free', 'subscription_expires_at' => null, ]; + } + + private function buildCreateModelMock(bool $wasRecentlyCreated): User + { + $attributes = $this->buildAttributes(); /** @var User|MockInterface $createdUser */ $createdUser = Mockery::mock(User::class); @@ -61,21 +68,82 @@ private function buildUserModelMock(bool $wasRecentlyCreated): User return $userModel; } + private function buildUpdateModelMock(?int $foundId): User + { + $attributes = $this->buildAttributes(); + + /** @var User|MockInterface $userModel */ + $userModel = Mockery::mock(User::class); + + if ($foundId === null) { + $userModel->shouldReceive('find')->andReturn(null); + } else { + /** @var User|MockInterface $foundUser */ + $foundUser = Mockery::mock(User::class)->makePartial(); + $foundUser->shouldReceive('update')->andReturn(true); + $foundUser->shouldReceive('refresh')->andReturn(null); + $foundUser->shouldReceive('toArray')->andReturn($attributes); + + $userModel->shouldReceive('find')->with($foundId)->andReturn($foundUser); + } + + return $userModel; + } + public function test_createUser_returns_user_dto_on_success(): void { - $repository = new UserRepository($this->buildUserModelMock(wasRecentlyCreated: true)); - $result = $repository->createUser($this->buildCommand()); + $repository = new UserRepository($this->buildCreateModelMock(wasRecentlyCreated: true)); + $result = $repository->createUser($this->buildEntity()); $this->assertInstanceOf(UserDto::class, $result); } public function test_createUser_throws_exception_when_insert_fails(): void { - $repository = new UserRepository($this->buildUserModelMock(wasRecentlyCreated: false)); + $repository = new UserRepository($this->buildCreateModelMock(wasRecentlyCreated: false)); $this->expectException(Exception::class); $this->expectExceptionMessage('Failed to create user.'); - $repository->createUser($this->buildCommand()); + $repository->createUser($this->buildEntity()); + } + + public function test_updateUser_returns_user_dto_on_success(): void + { + $existingId = 1; + $entity = UserEntityFactory::build([ + 'id' => $existingId, + 'first_name' => 'Updated', + 'last_name' => 'Name', + 'email' => 'updated@example.com', + 'age_range' => 'teens', + 'subscription_tier' => 'free', + 'subscription_expires_at' => null, + ]); + + $repository = new UserRepository($this->buildUpdateModelMock(foundId: $existingId)); + $result = $repository->updateUser($entity); + + $this->assertInstanceOf(UserDto::class, $result); + } + + public function test_updateUser_throws_exception_when_user_not_found(): void + { + $entity = UserEntityFactory::build([ + 'id' => 999, + 'first_name' => 'Ghost', + 'last_name' => 'User', + 'email' => 'ghost@example.com', + 'age_range' => 'teens', + 'subscription_tier' => 'free', + 'subscription_expires_at' => null, + ]); + + $repository = new UserRepository($this->buildUpdateModelMock(foundId: null)); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('User not found.'); + + $repository->updateUser($entity); } } \ No newline at end of file diff --git a/src/app/Packages/Domains/User/Factory/UserEntityFactory.php b/src/app/Packages/Domains/User/Factory/UserEntityFactory.php index a2deba31..5f7825e3 100644 --- a/src/app/Packages/Domains/User/Factory/UserEntityFactory.php +++ b/src/app/Packages/Domains/User/Factory/UserEntityFactory.php @@ -17,12 +17,13 @@ public static function build(array $data): UserEntity ]); return new UserEntity( - $data['id'], - $data['first_name'], - $data['last_name'], - $data['email'], - AgeRange::from($data['age_range']), - $subscription, + id: isset($data['id']) ? (int) $data['id'] : null, + firstName: $data['first_name'], + lastName: $data['last_name'], + email: $data['email'], + ageRange: AgeRange::from($data['age_range']), + subscription: $subscription, + passwordHash: $data['password_hash'] ?? null, ); } } \ No newline at end of file diff --git a/src/app/Packages/Domains/User/UserEntity.php b/src/app/Packages/Domains/User/UserEntity.php index 961595e4..a8f517ce 100644 --- a/src/app/Packages/Domains/User/UserEntity.php +++ b/src/app/Packages/Domains/User/UserEntity.php @@ -7,19 +7,25 @@ final readonly class UserEntity { public function __construct( - private readonly int $id, + private readonly ?int $id, private readonly string $firstName, private readonly string $lastName, private readonly string $email, private readonly AgeRange $ageRange, private readonly Subscription $subscription, + private readonly ?string $passwordHash = null, ) {} - public function getId(): int + public function getId(): ?int { return $this->id; } + public function getPasswordHash(): ?string + { + return $this->passwordHash; + } + public function getFirstName(): string { return $this->firstName; diff --git a/src/app/Packages/Domains/UserRepository.php b/src/app/Packages/Domains/UserRepository.php index 10478b44..3b011df6 100644 --- a/src/app/Packages/Domains/UserRepository.php +++ b/src/app/Packages/Domains/UserRepository.php @@ -4,7 +4,7 @@ use App\Models\User; use App\Packages\Domains\Interface\UserRepositroyInterface; -use App\Packages\Features\CommandUseCases\UseCommand\User\CreateUserCommand; +use App\Packages\Domains\User\UserEntity; use App\Packages\Features\QueryUseCases\Dto\User\UserDto; use App\Packages\Features\QueryUseCases\Factory\Dto\UserDtoFactory; use Exception; @@ -15,16 +15,16 @@ public function __construct( private User $userModel ) {} - public function createUser(CreateUserCommand $command): UserDto + public function createUser(UserEntity $entity): UserDto { $insertUser = $this->userModel->create([ - 'email' => $command->email, - 'first_name' => $command->firstName, - 'last_name' => $command->lastName, - 'password' => bcrypt($command->password), - 'age_range' => $command->ageRange, - 'subscription_tier' => $command->subscriptionTier, - 'subscription_expires_at' => $command->subscriptionExpiresAt, + 'email' => $entity->getEmail(), + 'first_name' => $entity->getFirstName(), + 'last_name' => $entity->getLastName(), + 'password' => $entity->getPasswordHash(), + 'age_range' => $entity->getAgeRange()->value, + 'subscription_tier' => $entity->getSubscription()->getTier()->value, + 'subscription_expires_at' => $entity->getSubscription()->getExpiresAt()?->format('Y-m-d H:i:s'), ]); if (!$insertUser->wasRecentlyCreated) { @@ -33,4 +33,26 @@ public function createUser(CreateUserCommand $command): UserDto return UserDtoFactory::build($insertUser->toArray()); } + + public function updateUser(UserEntity $entity): UserDto + { + $user = $this->userModel->find($entity->getId()); + + if ($user === null) { + throw new Exception('User not found.'); + } + + $user->update([ + 'first_name' => $entity->getFirstName(), + 'last_name' => $entity->getLastName(), + 'email' => $entity->getEmail(), + 'age_range' => $entity->getAgeRange()->value, + 'subscription_tier' => $entity->getSubscription()->getTier()->value, + 'subscription_expires_at' => $entity->getSubscription()->getExpiresAt()?->format('Y-m-d H:i:s'), + ]); + + $user->refresh(); + + return UserDtoFactory::build($user->toArray()); + } } \ No newline at end of file diff --git a/src/app/Packages/Features/CommandUseCases/UseCase/User/CreateUserUseCase.php b/src/app/Packages/Features/CommandUseCases/UseCase/User/CreateUserUseCase.php index 29c42891..1abeb92b 100644 --- a/src/app/Packages/Features/CommandUseCases/UseCase/User/CreateUserUseCase.php +++ b/src/app/Packages/Features/CommandUseCases/UseCase/User/CreateUserUseCase.php @@ -3,6 +3,7 @@ namespace App\Packages\Features\CommandUseCases\UseCase\User; use App\Packages\Domains\Interface\UserRepositroyInterface; +use App\Packages\Domains\User\Factory\UserEntityFactory; use App\Packages\Features\CommandUseCases\UseCommand\User\CreateUserCommand; use App\Packages\Features\QueryUseCases\Dto\User\UserDto; @@ -14,6 +15,17 @@ public function __construct( public function handle(CreateUserCommand $command): UserDto { - return $this->userRepository->createUser($command); + $entity = UserEntityFactory::build([ + 'id' => null, + 'first_name' => $command->firstName, + 'last_name' => $command->lastName, + 'email' => $command->email, + 'age_range' => $command->ageRange, + 'subscription_tier' => $command->subscriptionTier, + 'subscription_expires_at' => $command->subscriptionExpiresAt, + 'password_hash' => bcrypt($command->password), + ]); + + return $this->userRepository->createUser($entity); } } \ No newline at end of file diff --git a/src/app/Packages/Features/Tests/CommandUseCases/CreateUserUseCaseTest.php b/src/app/Packages/Features/Tests/CommandUseCases/CreateUserUseCaseTest.php index 683fe4fa..233553de 100644 --- a/src/app/Packages/Features/Tests/CommandUseCases/CreateUserUseCaseTest.php +++ b/src/app/Packages/Features/Tests/CommandUseCases/CreateUserUseCaseTest.php @@ -3,19 +3,21 @@ namespace App\Packages\Features\Tests\CommandUseCases; use App\Packages\Domains\Interface\UserRepositroyInterface; +use App\Packages\Domains\User\UserEntity; use App\Packages\Features\CommandUseCases\UseCase\User\CreateUserUseCase; use App\Packages\Features\CommandUseCases\UseCommand\User\CreateUserCommand; use App\Packages\Features\QueryUseCases\Dto\User\UserDto; use Faker\Factory as FakerFactory; use Mockery; use Mockery\MockInterface; -use PHPUnit\Framework\TestCase; +use Tests\TestCase; class CreateUserUseCaseTest extends TestCase { protected function tearDown(): void { Mockery::close(); + parent::tearDown(); } private function buildCommand(): CreateUserCommand @@ -47,14 +49,17 @@ private function buildDto(): UserDto ); } - public function test_handle_returns_user_dto_from_repository(): void + public function test_handle_passes_entity_to_repository_and_returns_dto(): void { $command = $this->buildCommand(); $dto = $this->buildDto(); /** @var UserRepositroyInterface|MockInterface $repository */ $repository = Mockery::mock(UserRepositroyInterface::class); - $repository->shouldReceive('createUser')->with($command)->andReturn($dto); + $repository->shouldReceive('createUser') + ->once() + ->with(Mockery::type(UserEntity::class)) + ->andReturn($dto); $useCase = new CreateUserUseCase($repository); $result = $useCase->handle($command);