diff --git a/apps/mooc/backend/config/routes/users.yaml b/apps/mooc/backend/config/routes/users.yaml new file mode 100644 index 0000000..61c3741 --- /dev/null +++ b/apps/mooc/backend/config/routes/users.yaml @@ -0,0 +1,4 @@ +users_put: + path: /users/{id} + controller: CodelyTv\Apps\Mooc\Backend\Controller\Users\UsersPutController + methods: [PUT] \ No newline at end of file diff --git a/apps/mooc/backend/src/Controller/Users/UsersPutController.php b/apps/mooc/backend/src/Controller/Users/UsersPutController.php new file mode 100644 index 0000000..534db3b --- /dev/null +++ b/apps/mooc/backend/src/Controller/Users/UsersPutController.php @@ -0,0 +1,36 @@ +creator = $userCreator; + } + + public function __invoke(string $id, Request $request) + { + $request = new CreateUserRequest( + $id, + $request->request->get('name'), + $request->request->get('email'), + $request->request->get('password') + ); + + try { + $this->creator->__invoke($request); + } catch (InvalidArgumentException $exception) { + return new Response('', Response::HTTP_BAD_REQUEST); + } + return new Response('', Response::HTTP_CREATED); + } +} \ No newline at end of file diff --git a/src/Mooc/Users/Application/CreateUserRequest.php b/src/Mooc/Users/Application/CreateUserRequest.php new file mode 100644 index 0000000..f6eeb77 --- /dev/null +++ b/src/Mooc/Users/Application/CreateUserRequest.php @@ -0,0 +1,39 @@ +id = $id; + $this->name = $name; + $this->email = $email; + $this->password = $password; + } + + public function id(): string + { + return $this->id; + } + + public function name(): string + { + return $this->name; + } + + public function email(): string + { + return $this->email; + } + + public function password(): string + { + return $this->password; + } +} \ No newline at end of file diff --git a/src/Mooc/Users/Application/UserCreator.php b/src/Mooc/Users/Application/UserCreator.php new file mode 100644 index 0000000..8cd7102 --- /dev/null +++ b/src/Mooc/Users/Application/UserCreator.php @@ -0,0 +1,31 @@ +repository = $repository; + } + + public function __invoke(CreateUserRequest $request) + { + $user = new User( + new UserId($request->id()), + new UserName($request->name()), + new UserEmail($request->email()), + new UserPassword($request->password()) + ); + $this->repository->saveUser($user); + } +} \ No newline at end of file diff --git a/src/Mooc/Users/Domain/User.php b/src/Mooc/Users/Domain/User.php new file mode 100644 index 0000000..164719b --- /dev/null +++ b/src/Mooc/Users/Domain/User.php @@ -0,0 +1,49 @@ +id = $id; + $this->name = $name; + $this->email = $email; + $this->password = $password; + } + + public function id(): UserId + { + return $this->id; + } + + public function email(): UserEmail + { + return $this->email; + } + + public function name(): UserName + { + return $this->name; + } + + public function password(): UserPassword + { + return $this->password; + } + + public function toString(): string + { + return json_encode([ + 'id' => $this->id()->value(), + 'name' => $this->name()->value(), + 'email' => $this->email()->value(), + 'password' => $this->password()->hashValue() + ]); + } +} \ No newline at end of file diff --git a/src/Mooc/Users/Domain/UserEmail.php b/src/Mooc/Users/Domain/UserEmail.php new file mode 100644 index 0000000..4c49476 --- /dev/null +++ b/src/Mooc/Users/Domain/UserEmail.php @@ -0,0 +1,25 @@ +ensureIsValidEmail($value); + parent::__construct($value); + } + + /** @throws InvalidArgumentException */ + private function ensureIsValidEmail(string $email): void + { + $validate = filter_var($email, FILTER_VALIDATE_EMAIL); + if ($validate === false) { + throw new InvalidArgumentException(sprintf('<%s> does not allow the value <%s>.', static::class, $email)); + } + } +} \ No newline at end of file diff --git a/src/Mooc/Users/Domain/UserId.php b/src/Mooc/Users/Domain/UserId.php new file mode 100644 index 0000000..2df187d --- /dev/null +++ b/src/Mooc/Users/Domain/UserId.php @@ -0,0 +1,10 @@ +ensureIsValidName($value); + parent::__construct($value); + } + + /** @throws InvalidArgumentException */ + private function ensureIsValidName(string $name): void + { + $validate = strlen($name) > self::LIMIT_MIN; + if ($validate === false) { + throw new InvalidArgumentException(sprintf('<%s> does not allow the value <%s>.', static::class, $name)); + } + } +} \ No newline at end of file diff --git a/src/Mooc/Users/Domain/UserPassword.php b/src/Mooc/Users/Domain/UserPassword.php new file mode 100644 index 0000000..42e9dec --- /dev/null +++ b/src/Mooc/Users/Domain/UserPassword.php @@ -0,0 +1,43 @@ +ensureIsValidPassword($value); + parent::__construct($value); + } + + /** @throws InvalidArgumentException */ + private function ensureIsValidPassword(string $password): void + { + if (strlen($password) < self::MIN_LENGTH) { + throw new InvalidArgumentException('The password has an invalid length. It must be greater than 8 characters'); + } + if (false === (bool)preg_match('/[a-z]/', $password)) { + throw new InvalidArgumentException('The password is invalid. It must contain a lower case character'); + } + if (false === (bool)preg_match('/[A-Z]/', $password)) { + throw new InvalidArgumentException('The password is invalid. It must contain an upper case character'); + } + if (false === (bool)preg_match('/[0-9]/', $password)) { + throw new InvalidArgumentException('The password is invalid. It must contain a number'); + } + if (false === (bool)preg_match('/[^a-zA-Z0-9]/', $password)) { + throw new InvalidArgumentException('The password is invalid. It must contain a symbol (not a letter nor a number)'); + } + } + + public function hashValue(): string + { + return password_hash($this->value(), PASSWORD_DEFAULT); + } +} \ No newline at end of file diff --git a/src/Mooc/Users/Domain/UserRepository.php b/src/Mooc/Users/Domain/UserRepository.php new file mode 100644 index 0000000..dba52d5 --- /dev/null +++ b/src/Mooc/Users/Domain/UserRepository.php @@ -0,0 +1,8 @@ +directory = __DIR__ . '/users'; + } + + public function saveUser(User $user): void + { + file_put_contents($this->fileName($user->id()->value()), $user->toString()); + } + + private function fileName(string $id): string + { + return "{$this->directory}.{$id}"; + } +} \ No newline at end of file diff --git a/tests/apps/mooc/backend/features/users/users_put.feature b/tests/apps/mooc/backend/features/users/users_put.feature new file mode 100644 index 0000000..812c560 --- /dev/null +++ b/tests/apps/mooc/backend/features/users/users_put.feature @@ -0,0 +1,16 @@ +Feature: Create a new user + In order to have users on the platform + As a user with admin permissions + I want to create a new user + + Scenario: A valid non existing user + Given I send a PUT request to "/users/1aab45ba-3c7a-4344-8936-78466eca77fa" with body: + """ + { + "name": "John Smith", + "email": "john-smith@gamil.com", + "password": "**-Pa55w0rD-**" + } + """ + Then the response status code should be 201 + And the response should be empty diff --git a/tests/apps/mooc/backend/mooc_backend.yml b/tests/apps/mooc/backend/mooc_backend.yml index 44a5a45..e736865 100644 --- a/tests/apps/mooc/backend/mooc_backend.yml +++ b/tests/apps/mooc/backend/mooc_backend.yml @@ -22,3 +22,9 @@ mooc_backend: contexts: - CodelyTv\Tests\Shared\Infrastructure\Behat\ApiRequestContext - CodelyTv\Tests\Shared\Infrastructure\Behat\ApiResponseContext + + users: + paths: [ tests/apps/mooc/backend/features/users ] + contexts: + - CodelyTv\Tests\Shared\Infrastructure\Behat\ApiRequestContext + - CodelyTv\Tests\Shared\Infrastructure\Behat\ApiResponseContext \ No newline at end of file diff --git a/tests/src/Mooc/Courses/Application/Domain/CourseNameMother.php b/tests/src/Mooc/Courses/Application/Domain/CourseNameMother.php index e8a75b7..9e87749 100644 --- a/tests/src/Mooc/Courses/Application/Domain/CourseNameMother.php +++ b/tests/src/Mooc/Courses/Application/Domain/CourseNameMother.php @@ -16,6 +16,6 @@ public static function create(string $value): CourseName public static function random(): CourseName { - return self::create(WordMother::random()); + return self::create(WordMother::randomWord()); } } diff --git a/tests/src/Mooc/Users/Application/CreateUserRequestMother.php b/tests/src/Mooc/Users/Application/CreateUserRequestMother.php new file mode 100644 index 0000000..4d8e121 --- /dev/null +++ b/tests/src/Mooc/Users/Application/CreateUserRequestMother.php @@ -0,0 +1,23 @@ +value(), + UserNameMother::random()->value(), + UserEmailMother::random()->value(), + UserPasswordMother::random()->value() + ); + } +} \ No newline at end of file diff --git a/tests/src/Mooc/Users/Application/UserCreatorTest.php b/tests/src/Mooc/Users/Application/UserCreatorTest.php new file mode 100644 index 0000000..da2be4d --- /dev/null +++ b/tests/src/Mooc/Users/Application/UserCreatorTest.php @@ -0,0 +1,139 @@ +createMock(UserRepository::class); + $repository->method('saveUser')->with($user); + + // when + $creator = new UserCreator($repository); + $creator->__invoke($request); + } + + /** @test */ + public function it_is_not_possible_a_very_short_user_name(): void + { + // then + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage(' does not allow the value <>.'); + + // given + $request = CreateUserRequestMother::create( + UserIdMother::random()->value(), + '', + UserEmailMother::random()->value(), + UserPasswordMother::random()->value() + ); + $repository = $this->createMock(UserRepository::class); + $repository + ->expects($this->never()) + ->method('saveUser'); + + // when + $creator = new UserCreator($repository); + $creator->__invoke($request); + } + + public function invalidPasswordsProvider(): array + { + return [ + 'it is missed an upper case letter' => ['_abc_123_', 'The password is invalid. It must contain an upper case character'], + 'it is missed a lower case letter' => ['_ABC_123_', 'The password is invalid. It must contain a lower case character'], + 'it is missed a number' => ['_ABC_abc_', 'The password is invalid. It must contain a number'], + 'it is missed a symbol character' => ['ABC123abc', 'The password is invalid. It must contain a symbol (not a letter nor a number)'], + 'There are not an enough number of characters' => ['Aa1_Bb2', 'The password has an invalid length. It must be greater than 8 characters'] + ]; + } + + /** + * @test + * @dataProvider invalidPasswordsProvider + */ + public function it_is_not_possible_a_invalid_password(string $password, string $errorMessage): void + { + // then + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage($errorMessage); + + // given + $request = CreateUserRequestMother::create( + UserIdMother::random()->value(), + UserNameMother::random()->value(), + UserEmailMother::random()->value(), + $password + ); + $repository = $this->createMock(UserRepository::class); + $repository + ->expects($this->never()) + ->method('saveUser'); + + // when + $creator = new UserCreator($repository); + $creator->__invoke($request); + } + + /** + * @test + */ + public function it_is_not_possible_an_invalid_id(): void + { + // then + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage(' does not allow the value <123>.'); + + // given + $request = CreateUserRequestMother::create( + '123', + UserNameMother::random()->value(), + UserEmailMother::random()->value(), + UserPasswordMother::random()->value() + ); + $repository = $this->createMock(UserRepository::class); + $repository + ->expects($this->never()) + ->method('saveUser'); + + // when + $creator = new UserCreator($repository); + $creator->__invoke($request); + } + + /** + * @test + */ + public function it_is_not_possible_an_invalid_email(): void + { + // then + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage(' does not allow the value .'); + + // given + $request = CreateUserRequestMother::create( + UserIdMother::random()->value(), + UserNameMother::random()->value(), + 'abc', + UserPasswordMother::random()->value() + ); + $repository = $this->createMock(UserRepository::class); + $repository + ->expects($this->never()) + ->method('saveUser'); + + // when + $creator = new UserCreator($repository); + $creator->__invoke($request); + } +} diff --git a/tests/src/Mooc/Users/Application/UserEmailMother.php b/tests/src/Mooc/Users/Application/UserEmailMother.php new file mode 100644 index 0000000..f5146d4 --- /dev/null +++ b/tests/src/Mooc/Users/Application/UserEmailMother.php @@ -0,0 +1,19 @@ +id()), + UserNameMother::create($request->name()), + UserEmailMother::create($request->email()), + UserPasswordMother::create($request->password()) + ); + } + + public static function random(): User + { + return self::create( + UserIdMother::random(), + UserNameMother::random(), + UserEmailMother::random(), + UserPasswordMother::random(), + ); + } +} \ No newline at end of file diff --git a/tests/src/Mooc/Users/Application/UserNameMother.php b/tests/src/Mooc/Users/Application/UserNameMother.php new file mode 100644 index 0000000..7961b73 --- /dev/null +++ b/tests/src/Mooc/Users/Application/UserNameMother.php @@ -0,0 +1,19 @@ +saveUser($user); + $this->userId = $user->id()->value(); + } + + public function tearDown(): void + { + parent::tearDown(); // TODO: Change the autogenerated stub + $filename = "/app/src/Mooc/Users/Infraestructure/users.{$this->userId}"; + unlink($filename); + } +} diff --git a/tests/src/Shared/Domain/WordMother.php b/tests/src/Shared/Domain/WordMother.php index 6dfaf08..b3df93d 100644 --- a/tests/src/Shared/Domain/WordMother.php +++ b/tests/src/Shared/Domain/WordMother.php @@ -6,8 +6,29 @@ final class WordMother { - public static function random(): string + public static function randomWord(): string { return MotherCreator::random()->word; } + + public static function randomUserEmail(): string + { + return MotherCreator::random()->email; + } + + public static function randomUserPassword(): string + { + $minChunk = [ + substr(str_shuffle('ABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, 1), + substr(str_shuffle('abcdefghijklmnopqrstuvwxyz'), 0, 1), + substr(str_shuffle('0123456789'), 0, 1), + substr(str_shuffle('`-=~!@#$%^&*()_+,./<>?;:[]{}\|'), 0, 1), + ]; + return str_shuffle(MotherCreator::random()->password(4, 16) . implode('', $minChunk)); + } + + public static function randomUserName(): string + { + return MotherCreator::random()->name; + } }