From eb2e4a17d6a54dbe7eeba0292042c688c3515354 Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Mon, 13 Jul 2026 23:12:11 -0300 Subject: [PATCH 01/50] feat: adiciona entidades User, RoleEnum e UserTest utilizando pydantic --- requirements-dev.txt | 33 +++++++-- src/shared/domain/entities/user.py | 79 +++++++------------- src/shared/domain/enums/role_enum.py | 5 ++ tests/shared/domain/entities/test_user.py | 87 ++++++++++++++++------- 4 files changed, 121 insertions(+), 83 deletions(-) create mode 100644 src/shared/domain/enums/role_enum.py diff --git a/requirements-dev.txt b/requirements-dev.txt index ab21f95..098c153 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,31 @@ -pytest==6.2.5 -pytest-cov==4.0.0 +annotated-types==0.7.0 +atomicwrites==1.4.1 +attrs==26.1.0 +aws-lambda-powertools==2.9.0 +aws-xray-sdk==2.11.0 boto3==1.24.88 +botocore==1.27.96 +colorama==0.4.6 +coverage==7.15.1 +dnspython==2.8.0 +email-validator==2.3.0 +idna==3.18 +iniconfig==2.3.0 +jmespath==1.1.0 +packaging==26.2 +pluggy==1.6.0 +py==1.11.0 +pydantic==2.13.4 +pydantic_core==2.46.4 +Pygments==2.20.0 +pytest==9.1.1 +pytest-cov==4.0.0 +python-dateutil==2.9.0.post0 python-dotenv==0.21.0 -aws-lambda-powertools==2.9.0 -aws_xray_sdk==2.11.0 \ No newline at end of file +s3transfer==0.6.2 +six==1.17.0 +toml==0.10.2 +typing-inspection==0.4.2 +typing_extensions==4.16.0 +urllib3==1.26.20 +wrapt==2.2.2 diff --git a/src/shared/domain/entities/user.py b/src/shared/domain/entities/user.py index 0e4f333..40a9578 100644 --- a/src/shared/domain/entities/user.py +++ b/src/shared/domain/entities/user.py @@ -1,60 +1,35 @@ -import abc -import re -from src.shared.domain.enums.state_enum import STATE -from src.shared.helpers.errors.domain_errors import EntityError +import uuid +from pydantic import BaseModel, Field, EmailStr, ConfigDict +from src.shared.domain.enums.role_enum import RoleEnum -class User(abc.ABC): - name: str - email: str - state: STATE - MIN_NAME_LENGTH = 2 - user_id: int - def __init__(self, name: str, email: str, state: STATE, user_id: int = None): - if not User.validate_name(name): - raise EntityError("name") - self.name = name +class User(BaseModel): + id: uuid.UUID = Field( + default_factory=uuid.uuid4, + description="Identificador único de usuário" + ) - if not User.validate_email(email): - raise EntityError("email") - self.email = email + email: EmailStr = Field( + ..., + description="Email do usuário", + min_length=5, + examples=["usuario@example.com"] + ) - if type(user_id) == int: - if user_id < 0: - raise EntityError("user_id") + role: RoleEnum = Field( + default=RoleEnum.USER, + description="Função do usuário", + ) - if type(user_id) != int and user_id is not None: - raise EntityError("user_id") + password_hash: str = Field( + ..., + description="Hash da senha do usuário", + ) - self.user_id = user_id - - if type(state) != STATE: - raise EntityError("state") - self.state = state - - @staticmethod - def validate_name(name: str) -> bool: - if name is None: - return False - elif type(name) != str: - return False - elif len(name) < User.MIN_NAME_LENGTH: - return False - - return True - - @staticmethod - def validate_email(email: str) -> bool: - if email is None: - return False - - regex = re.compile(r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)") - - return bool(re.fullmatch(regex, email)) - - - - def __repr__(self): - return f"User(name={self.name}, email={self.email}, user_id={self.user_id}, state={self.state})" + model_config = ConfigDict( + use_enum_values=True, + extra="forbid", + populate_by_name=True, + ) diff --git a/src/shared/domain/enums/role_enum.py b/src/shared/domain/enums/role_enum.py new file mode 100644 index 0000000..4f46e6b --- /dev/null +++ b/src/shared/domain/enums/role_enum.py @@ -0,0 +1,5 @@ +from enum import Enum + +class RoleEnum(str, Enum): + ADMIN = "admin" + USER = "user" \ No newline at end of file diff --git a/tests/shared/domain/entities/test_user.py b/tests/shared/domain/entities/test_user.py index b5f2a85..95665bc 100644 --- a/tests/shared/domain/entities/test_user.py +++ b/tests/shared/domain/entities/test_user.py @@ -1,41 +1,74 @@ -from src.shared.domain.entities.user import User -from src.shared.domain.enums.state_enum import STATE -from src.shared.helpers.errors.domain_errors import EntityError +import uuid import pytest +from pydantic import ValidationError + +from src.shared.domain.entities.user import User +from src.shared.domain.enums.role_enum import RoleEnum class Test_User: def test_user(self): - User(name="VITOR", email="21.01444-2@maua.br", user_id=1, state=STATE.APPROVED) + user = User(email="usuario@example.com", password_hash="hash_da_senha") + id_user = user.id + assert isinstance(id_user,uuid.UUID) + assert user.email == "usuario@example.com" + assert user.password_hash == "hash_da_senha" + assert user.role == "user" + + def test_user_not_has_email(self): + with pytest.raises(ValidationError): + User(password_hash="hash_da_senha") + + def test_user_not_has_password_hash(self): + with pytest.raises(ValidationError): + User(email="usuario@example.com") - def test_user_name_is_none(self): - with pytest.raises(EntityError): - User(name=None, email="21.01444-2@maua.br", user_id=1, state=STATE.APPROVED) + def test_user_with_custom_id(self): + user_id = uuid.uuid4() + user = User(id=user_id, email="usuario@example.com", password_hash="hash_da_senha") + assert user.id == user_id - def test_user_name_is_not_str(self): - with pytest.raises(EntityError): - User(name=1, email="21.01444-2@maua.br", user_id=1, state=STATE.APPROVED) + def test_user_role_is_admin(self): + user = User(email="admin@example.com", password_hash="hash_da_senha", role="admin") + assert user.role == "admin" - def test_user_name_is_shorter_than_min_length(self): - with pytest.raises(EntityError): - User(name="V", email="21.01444-2@maua.br", user_id=1, state=STATE.APPROVED) + def test_user_role_is_invalid(self): + with pytest.raises(ValidationError): + User(email="usuario@example.com", password_hash="hash_da_senha", role="invalid_role") def test_user_email_is_none(self): - with pytest.raises(EntityError): - User(name="VITOR", email=None, user_id=1, state=STATE.APPROVED) + with pytest.raises(ValidationError): + User(email=None, password_hash="hash_da_senha") + + def test_user_email_not_has_at_symbol(self): + with pytest.raises(ValidationError): + User(email="usuarioexample.com", password_hash="hash_da_senha") + + def test_user_email_not_has_domain(self): + with pytest.raises(ValidationError): + User(email="usuario@", password_hash="hash_da_senha") + + + # def test_user_name_is_shorter_than_min_length(self): + # with pytest.raises(ValidationError): + # User(name="V", email="21.01444-2@maua.br", user_id=1, state=STATE.APPROVED) + + # def test_user_email_is_none(self): + # with pytest.raises(ValidationError): + # User(name="VITOR", email=None, user_id=1, state=STATE.APPROVED) - def test_user_email_is_not_valid(self): - with pytest.raises(EntityError): - User(name="VITOR", email="21.01444-2maua.br", user_id=1, state=STATE.APPROVED) + # def test_user_email_is_not_valid(self): + # with pytest.raises(ValidationError): + # User(name="VITOR", email="21.01444-2maua.br", user_id=1, state=STATE.APPROVED) - def test_user_user_id_is_not_int(self): - with pytest.raises(EntityError): - User(name="VITOR", email="21.01444-2@maua.br", user_id="1", state=STATE.APPROVED) + # def test_user_user_id_is_not_int(self): + # with pytest.raises(ValidationError): + # User(name="VITOR", email="21.01444-2@maua.br", user_id="1", state=STATE.APPROVED) - def test_user_user_id_is_negative(self): - with pytest.raises(EntityError): - User(name="VITOR", email="21.01444-2@maua.br", user_id=-1, state=STATE.APPROVED) + # def test_user_user_id_is_negative(self): + # with pytest.raises(ValidationError): + # User(name="VITOR", email="21.01444-2@maua.br", user_id=-1, state=STATE.APPROVED) - def test_user_state_is_not_sate_enum(self): - with pytest.raises(EntityError): - User(name="VITOR", email="21.01444-2@maua.br", user_id=1, state="APPROVED") + # def test_user_state_is_not_sate_enum(self): + # with pytest.raises(ValidationError): + # User(name="VITOR", email="21.01444-2@maua.br", user_id=1, state="APPROVED") From 9aa1d05efd40ed91cf55898620e6e801e9c740b9 Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Tue, 14 Jul 2026 08:26:33 -0300 Subject: [PATCH 02/50] =?UTF-8?q?fix:=20corrige=20vers=C3=A3o=20do=20pacot?= =?UTF-8?q?e=20coverage=20para=207.10.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 098c153..bd1b6f0 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,7 +6,7 @@ aws-xray-sdk==2.11.0 boto3==1.24.88 botocore==1.27.96 colorama==0.4.6 -coverage==7.15.1 +coverage==7.10.7 dnspython==2.8.0 email-validator==2.3.0 idna==3.18 From 4f70096fbee4c285aa2206d1037b8da7f4c003d6 Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Thu, 16 Jul 2026 16:37:40 -0300 Subject: [PATCH 03/50] =?UTF-8?q?feat:=20atualiza=20depend=C3=AAncias=20no?= =?UTF-8?q?=20requirements-dev.txt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 +++++ requirements-dev.txt | 6 ++--- tests/shared/domain/entities/test_user.py | 27 +---------------------- 3 files changed, 10 insertions(+), 29 deletions(-) diff --git a/.gitignore b/.gitignore index 111f3c8..5963e86 100644 --- a/.gitignore +++ b/.gitignore @@ -134,3 +134,9 @@ dmypy.json /.vscode/ /.idea/ iac/local/docker/dynamodb/shared-local-instance.db + +# Docker +.dockerignore +compose.yaml +Dockerfile +README.Docker.md diff --git a/requirements-dev.txt b/requirements-dev.txt index bd1b6f0..2902213 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,10 +7,10 @@ boto3==1.24.88 botocore==1.27.96 colorama==0.4.6 coverage==7.10.7 -dnspython==2.8.0 +dnspython==2.7.0 email-validator==2.3.0 idna==3.18 -iniconfig==2.3.0 +iniconfig==2.1.0 jmespath==1.1.0 packaging==26.2 pluggy==1.6.0 @@ -18,7 +18,7 @@ py==1.11.0 pydantic==2.13.4 pydantic_core==2.46.4 Pygments==2.20.0 -pytest==9.1.1 +pytest==8.4.2 pytest-cov==4.0.0 python-dateutil==2.9.0.post0 python-dotenv==0.21.0 diff --git a/tests/shared/domain/entities/test_user.py b/tests/shared/domain/entities/test_user.py index 95665bc..02723ae 100644 --- a/tests/shared/domain/entities/test_user.py +++ b/tests/shared/domain/entities/test_user.py @@ -46,29 +46,4 @@ def test_user_email_not_has_at_symbol(self): def test_user_email_not_has_domain(self): with pytest.raises(ValidationError): - User(email="usuario@", password_hash="hash_da_senha") - - - # def test_user_name_is_shorter_than_min_length(self): - # with pytest.raises(ValidationError): - # User(name="V", email="21.01444-2@maua.br", user_id=1, state=STATE.APPROVED) - - # def test_user_email_is_none(self): - # with pytest.raises(ValidationError): - # User(name="VITOR", email=None, user_id=1, state=STATE.APPROVED) - - # def test_user_email_is_not_valid(self): - # with pytest.raises(ValidationError): - # User(name="VITOR", email="21.01444-2maua.br", user_id=1, state=STATE.APPROVED) - - # def test_user_user_id_is_not_int(self): - # with pytest.raises(ValidationError): - # User(name="VITOR", email="21.01444-2@maua.br", user_id="1", state=STATE.APPROVED) - - # def test_user_user_id_is_negative(self): - # with pytest.raises(ValidationError): - # User(name="VITOR", email="21.01444-2@maua.br", user_id=-1, state=STATE.APPROVED) - - # def test_user_state_is_not_sate_enum(self): - # with pytest.raises(ValidationError): - # User(name="VITOR", email="21.01444-2@maua.br", user_id=1, state="APPROVED") + User(email="usuario@", password_hash="hash_da_senha") \ No newline at end of file From ca6204b920f7ca807cf555e1de9d48edea58e052 Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Thu, 16 Jul 2026 18:33:47 -0300 Subject: [PATCH 04/50] =?UTF-8?q?feat:=20implementa=20cria=C3=A7=C3=A3o=20?= =?UTF-8?q?de=20usu=C3=A1rio=20com=20valida=C3=A7=C3=A3o=20de=20dados=20e?= =?UTF-8?q?=20gerenciamento=20de=20pap=C3=A9is?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../create_user/app/create_user_controller.py | 25 +++++++-- .../create_user/app/create_user_usecase.py | 15 ++---- .../create_user/app/create_user_viewmodel.py | 14 +++-- .../app/get_all_users_viewmodel.py | 10 ++-- src/shared/domain/entities/user.py | 2 +- .../repositories/user_repository_mock.py | 28 +++++----- .../app/test_create_user_controller.py | 36 +++++-------- .../app/test_get_all_users_viewmodel.py | 52 ++++++++----------- tests/shared/domain/entities/test_user.py | 4 +- .../repositories/test_user_repository_mock.py | 47 ++++++++--------- 10 files changed, 109 insertions(+), 124 deletions(-) diff --git a/src/modules/create_user/app/create_user_controller.py b/src/modules/create_user/app/create_user_controller.py index 29e3a59..73329c3 100644 --- a/src/modules/create_user/app/create_user_controller.py +++ b/src/modules/create_user/app/create_user_controller.py @@ -1,11 +1,17 @@ +from pydantic import ValidationError + from src.shared.helpers.external_interfaces.external_interface import IResponse, IRequest from .create_user_usecase import CreateUserUsecase from .create_user_viewmodel import CreateUserViewmodel + + from src.shared.helpers.errors.controller_errors import MissingParameters, WrongTypeParameter from src.shared.helpers.errors.domain_errors import EntityError from src.shared.helpers.errors.usecase_errors import NoItemsFound from src.shared.helpers.external_interfaces.http_codes import OK, NotFound, BadRequest, InternalServerError, Created +from src.shared.domain.enums.role_enum import RoleEnum + class CreateUserController: @@ -14,14 +20,15 @@ def __init__(self, usecase: CreateUserUsecase): def __call__(self, request: IRequest) -> IResponse: try: - if request.data.get('name') is None: - raise MissingParameters('name') if request.data.get('email') is None: raise MissingParameters('email') + if request.data.get('password_hash') is None: + raise MissingParameters('password_hash') user = self.CreateUserUsecase( - name=request.data.get('name'), - email=request.data.get('email') + email=request.data.get('email'), + password_hash=request.data.get('password_hash'), + role=request.data.get('role') or RoleEnum.USER ) viewmodel = CreateUserViewmodel(user) @@ -39,6 +46,16 @@ def __call__(self, request: IRequest) -> IResponse: except WrongTypeParameter as err: return BadRequest(body=err.message) + + except ValidationError as err: + error = err.errors()[0] + + if error['loc'][0] == "email": + return BadRequest(body="Field email is not valid") + if error['loc'][0] == "password_hash": + return BadRequest(body="Field password_hash is not valid") + + return BadRequest(body="Invalid data") except EntityError as err: diff --git a/src/modules/create_user/app/create_user_usecase.py b/src/modules/create_user/app/create_user_usecase.py index e1e9e80..8d89f48 100644 --- a/src/modules/create_user/app/create_user_usecase.py +++ b/src/modules/create_user/app/create_user_usecase.py @@ -1,7 +1,7 @@ from src.shared.domain.entities.user import User -from src.shared.domain.enums.state_enum import STATE +from src.shared.domain.enums.role_enum import RoleEnum from src.shared.domain.repositories.user_repository_interface import IUserRepository from src.shared.helpers.errors.domain_errors import EntityError @@ -10,18 +10,11 @@ class CreateUserUsecase: def __init__(self, repo: IUserRepository): self.repo = repo - def __call__(self, name: str, email: str) -> User: - - if not User.validate_name(name): - raise EntityError("name") - - if not User.validate_email(email): - raise EntityError("email") - + def __call__(self, email: str, password_hash: str, role: RoleEnum = RoleEnum.USER) -> User: user = User( - name=name, email=email, - state=STATE.PENDING + password_hash=password_hash, + role=role ) return self.repo.create_user(user) diff --git a/src/modules/create_user/app/create_user_viewmodel.py b/src/modules/create_user/app/create_user_viewmodel.py index fd71ebc..e38d419 100644 --- a/src/modules/create_user/app/create_user_viewmodel.py +++ b/src/modules/create_user/app/create_user_viewmodel.py @@ -1,24 +1,22 @@ from src.shared.domain.entities.user import User -from src.shared.domain.enums.state_enum import STATE - +from src.shared.domain.enums.role_enum import RoleEnum +from uuid import UUID class CreateUserViewmodel: - user_id: int + user_id: UUID name: str email: str - state: STATE + role: RoleEnum def __init__(self, user: User): self.user_id = user.user_id - self.name = user.name self.email = user.email - self.state = user.state + self.role = user.role def to_dict(self): return { 'user_id': self.user_id, - 'name': self.name, 'email': self.email, - 'state': self.state.value, + 'role': self.role, 'message': "the user was created successfully" } diff --git a/src/modules/get_all_users/app/get_all_users_viewmodel.py b/src/modules/get_all_users/app/get_all_users_viewmodel.py index 32ff93c..b32173f 100644 --- a/src/modules/get_all_users/app/get_all_users_viewmodel.py +++ b/src/modules/get_all_users/app/get_all_users_viewmodel.py @@ -4,18 +4,18 @@ class UserViewmodel: + def __init__(self, user: User): - self.state = user.state - self.email = user.email - self.name = user.name self.user_id = user.user_id + self.email = user.email + self.role = user.role + def to_dict(self): return { 'user_id': self.user_id, - 'name': self.name, 'email': self.email, - 'state': self.state.value + 'role': self.role, } diff --git a/src/shared/domain/entities/user.py b/src/shared/domain/entities/user.py index 40a9578..9eeda8c 100644 --- a/src/shared/domain/entities/user.py +++ b/src/shared/domain/entities/user.py @@ -6,7 +6,7 @@ class User(BaseModel): - id: uuid.UUID = Field( + user_id: uuid.UUID = Field( default_factory=uuid.uuid4, description="Identificador único de usuário" ) diff --git a/src/shared/infra/repositories/user_repository_mock.py b/src/shared/infra/repositories/user_repository_mock.py index d576853..abc498a 100644 --- a/src/shared/infra/repositories/user_repository_mock.py +++ b/src/shared/infra/repositories/user_repository_mock.py @@ -12,17 +12,16 @@ class UserRepositoryMock(IUserRepository): def __init__(self): self.users = [ - User(name="Bruno Soller", email="soller@soller.com", user_id=1, state=STATE.APPROVED), - User(name="Vitor Brancas", email="brancas@brancas.com", user_id=2, state=STATE.REJECTED), - User(name="João Vilas", email="bruno@bruno.com", user_id=3, state=STATE.PENDING) + User(email="admin@example.com", role="admin", password_hash="hash_da_senha"), + User(email="user@example.com", role="user", password_hash="hash_da_senha"), ] - self.user_counter = 3 + self.user_counter = 2 - def get_user(self, user_id: int) -> User: + def get_user(self, user_email: str) -> User: for user in self.users: - if user.user_id == user_id: + if user.email == user_email: return user - raise NoItemsFound("user_id") + raise NoItemsFound("user_email") def get_all_user(self) -> List[User]: return self.users @@ -32,20 +31,21 @@ def create_user(self, new_user: User) -> User: self.user_counter += 1 return new_user - def delete_user(self, user_id: int) -> User: + def delete_user(self, user_email: str) -> User: for idx, user in enumerate(self.users): - if user.user_id == user_id: + if user.email == user_email: + self.user_counter -= 1 return self.users.pop(idx) - raise NoItemsFound("user_id") + raise NoItemsFound("user_email") - def update_user(self, user_id: int, new_name: str) -> User: + def update_user(self, user_email: str, new_password_hash: str) -> User: for user in self.users: - if user.user_id == user_id: - user.name = new_name + if user.email == user_email: + user.password_hash = new_password_hash return user - raise NoItemsFound("user_id") + raise NoItemsFound("user_email") def get_user_counter(self) -> int: return self.user_counter diff --git a/tests/modules/create_user/app/test_create_user_controller.py b/tests/modules/create_user/app/test_create_user_controller.py index d34ff44..380ff7d 100644 --- a/tests/modules/create_user/app/test_create_user_controller.py +++ b/tests/modules/create_user/app/test_create_user_controller.py @@ -11,31 +11,31 @@ def test_create_user_controller(self): controller = CreateUserController(usecase=usecase) request = HttpRequest(body={ - 'name': 'Branco do Branco Branco da Silva', - 'email': 'branco@branco.com' + 'email': 'branco@branco.com', + 'password_hash': 'hash_da_senha' }) response = controller(request=request) assert response.status_code == 201 assert response.body['user_id'] == repo.users[-1].user_id - assert response.body['name'] == repo.users[-1].name assert response.body['email'] == repo.users[-1].email - assert response.body['state'] == repo.users[-1].state.value + assert response.body['role'] == repo.users[-1].role assert response.body['message'] == "the user was created successfully" - def test_create_user_controller_missing_name(self): + def test_create_user_controller_missing_password_hash(self): repo = UserRepositoryMock() usecase = CreateUserUsecase(repo=repo) controller = CreateUserController(usecase=usecase) request = HttpRequest(body={ - 'email': '21.01444-2@maua.br'}) + 'email': '21.01444-2@maua.br'} + ) response = controller(request=request) assert response.status_code == 400 - assert response.body == "Field name is missing" + assert response.body == "Field password_hash is missing" def test_create_user_controller_missing_email(self): @@ -44,7 +44,8 @@ def test_create_user_controller_missing_email(self): controller = CreateUserController(usecase=usecase) request = HttpRequest(body={ - 'name': 'Branco do Branco Branco da Silva'}) + 'password_hash': 'hash_da_senha' + }) response = controller(request=request) @@ -57,28 +58,15 @@ def test_create_user_controller_invalid_email(self): controller = CreateUserController(usecase=usecase) request = HttpRequest(body={ - 'name': 'Branco do Branco Branco da Silva', - 'email': 'branco@branco'}) + 'email': 'branco@branco', + 'password_hash': 'hash_da_senha' + }) response = controller(request=request) assert response.status_code == 400 assert response.body == "Field email is not valid" - def test_create_user_controller_invalid_name(self): - repo = UserRepositoryMock() - usecase = CreateUserUsecase(repo=repo) - controller = CreateUserController(usecase=usecase) - - request = HttpRequest(body={ - 'name': 'B', - 'email': 'branco@branco.com'}) - - response = controller(request=request) - - assert response.status_code == 400 - assert response.body == "Field name is not valid" - diff --git a/tests/modules/get_all_users/app/test_get_all_users_viewmodel.py b/tests/modules/get_all_users/app/test_get_all_users_viewmodel.py index f9b26ba..0626028 100644 --- a/tests/modules/get_all_users/app/test_get_all_users_viewmodel.py +++ b/tests/modules/get_all_users/app/test_get_all_users_viewmodel.py @@ -1,20 +1,18 @@ +import uuid + from src.modules.get_all_users.app.get_all_users_viewmodel import GetAllUsersViewmodel, UserViewmodel from src.shared.domain.entities.user import User from src.shared.domain.enums.state_enum import STATE class Test_GetAllUsersViewmodel: - all_users_list = [ - User(user_id=1, - name="Lucas Duez", - email="deuzexmachina@gmail.com", - state=STATE.APPROVED), - User(user_id=2, - name="Laura Blablachan", - email="laurinha@gmail.com", - state=STATE.APPROVED), - ] + uuid1 = uuid.uuid4() + uuid2 = uuid.uuid4() + all_users_list = [ + User(user_id=uuid1, email="admin@example.com", role="admin", password_hash="hash_da_senha"), + User(user_id=uuid2, email="user@example.com", role="user", password_hash="hash_da_senha"), + ] def test_get_all_users_viewmodel(self): viewmodel = GetAllUsersViewmodel(self.all_users_list) @@ -22,16 +20,14 @@ def test_get_all_users_viewmodel(self): expected = { "all_users": [ { - 'user_id': 1, - 'name': "Lucas Duez", - 'email': "deuzexmachina@gmail.com", - 'state': 'APPROVED', + 'user_id': self.uuid1, + 'email': "admin@example.com", + 'role': 'admin', }, { - 'user_id': 2, - 'name': "Laura Blablachan", - 'email': "laurinha@gmail.com", - 'state': 'APPROVED', + 'user_id': self.uuid2, + 'email': "user@example.com", + 'role': 'user', } ], "message": "all users has been retrieved" @@ -43,22 +39,20 @@ def test_get_all_users_viewmodel(self): def test_user_viewmodel(self): viewmodel = UserViewmodel( - User(user_id=2, - name="Laura Blablachan", - email="laurinha@gmail.com", - state=STATE.APPROVED), -) + User( + user_id=self.uuid1, + email="admin@example.com", + role="admin", + password_hash="hash_da_senha"), + ) response = viewmodel.to_dict() expected = { - 'user_id': 2, - 'name': "Laura Blablachan", - 'email': "laurinha@gmail.com", - 'state': 'APPROVED', + 'user_id': self.uuid1, + 'email': "admin@example.com", + 'role': "admin", } assert response == expected - - diff --git a/tests/shared/domain/entities/test_user.py b/tests/shared/domain/entities/test_user.py index 02723ae..c32ff66 100644 --- a/tests/shared/domain/entities/test_user.py +++ b/tests/shared/domain/entities/test_user.py @@ -9,7 +9,7 @@ class Test_User: def test_user(self): user = User(email="usuario@example.com", password_hash="hash_da_senha") - id_user = user.id + id_user = user.user_id assert isinstance(id_user,uuid.UUID) assert user.email == "usuario@example.com" assert user.password_hash == "hash_da_senha" @@ -25,7 +25,7 @@ def test_user_not_has_password_hash(self): def test_user_with_custom_id(self): user_id = uuid.uuid4() - user = User(id=user_id, email="usuario@example.com", password_hash="hash_da_senha") + user = User(user_id=user_id, email="usuario@example.com", password_hash="hash_da_senha") assert user.id == user_id def test_user_role_is_admin(self): diff --git a/tests/shared/infra/repositories/test_user_repository_mock.py b/tests/shared/infra/repositories/test_user_repository_mock.py index 4dcda8b..b13b80c 100644 --- a/tests/shared/infra/repositories/test_user_repository_mock.py +++ b/tests/shared/infra/repositories/test_user_repository_mock.py @@ -8,68 +8,63 @@ class Test_UserRepositoryMock: def test_get_user(self): repo = UserRepositoryMock() - user = repo.get_user(1) + user = repo.get_user("admin@example.com") - assert user.name == "Bruno Soller" - assert user.email == "soller@soller.com" - assert user.user_id == 1 - assert user.state == STATE.APPROVED + assert user.email == "admin@example.com" + assert user.password_hash == "hash_da_senha" + assert user.role == "admin" def test_get_user_not_found(self): repo = UserRepositoryMock() with pytest.raises(NoItemsFound): - user = repo.get_user(69) + user = repo.get_user("nonexistent@example.com") def test_get_all_user(self): repo = UserRepositoryMock() users = repo.get_all_user() - assert len(users) == 3 + assert len(users) == 2 def test_create_user(self): repo = UserRepositoryMock() user = User( - name="Vitor Soller", email="dohype@vitin.com", - user_id=4, - state=STATE.PENDING + password_hash="hash_da_senha", ) repo.create_user(user) - assert repo.users[3].name == "Vitor Soller" - assert repo.users[3].email == "dohype@vitin.com" - assert repo.users[3].user_id == 4 - assert repo.users[3].state == STATE.PENDING + assert repo.users[-1].email == "dohype@vitin.com" + assert repo.users[-1].password_hash == "hash_da_senha" + assert repo.users[-1].role == "user" - assert repo.user_counter == 4 + assert repo.user_counter == 3 def test_delete_user(self): repo = UserRepositoryMock() - user = repo.delete_user(1) - assert user.name == "Bruno Soller" - assert user.email == "soller@soller.com" - assert user.user_id == 1 - assert user.state == STATE.APPROVED + user = repo.delete_user("user@example.com") + assert user.email == "user@example.com" + assert user.password_hash == "hash_da_senha" + assert user.role == "user" def test_delete_user_not_found(self): repo = UserRepositoryMock() with pytest.raises(NoItemsFound): - user = repo.delete_user(69) + user = repo.delete_user("nonexistent@example.com") def test_update_user(self): repo = UserRepositoryMock() - user = repo.update_user(1, "Bruno Guirão") + user = repo.update_user("admin@example.com", "new_hash_da_senha") - assert user.name == "Bruno Guirão" - assert repo.users[0].name == "Bruno Guirão" + assert user.password_hash == "new_hash_da_senha" + assert repo.users[0].password_hash == "new_hash_da_senha" def test_update_user_not_found(self): repo = UserRepositoryMock() with pytest.raises(NoItemsFound): - user = repo.update_user(69, "Bruno Guirão") + user = repo.update_user("nonexistent@example.com", "new_hash_da_senha") def test_get_users_counter(self): repo = UserRepositoryMock() - assert repo.get_user_counter() == 3 + assert repo.get_user_counter() == 2 From ec1d0a4f66ee58e77f30d1bbb5eeb38538e80957 Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Fri, 17 Jul 2026 14:02:41 -0300 Subject: [PATCH 05/50] =?UTF-8?q?refactor:=20renomeia=20RoleEnum=20para=20?= =?UTF-8?q?ROLE=20e=20atualiza=20refer=C3=AAncias=20em=20todo=20o=20c?= =?UTF-8?q?=C3=B3digo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../create_user/app/create_user_controller.py | 4 +- .../create_user/app/create_user_usecase.py | 10 +++-- .../create_user/app/create_user_viewmodel.py | 4 +- .../delete_user/app/delete_user_viewmodel.py | 14 +++--- .../get_user/app/get_user_viewmodel.py | 16 +++---- .../update_user/app/update_user_viewmodel.py | 16 +++---- src/shared/domain/entities/user.py | 7 +-- src/shared/domain/enums/role_enum.py | 2 +- src/shared/infra/dto/user_dynamo_dto.py | 6 +-- .../repositories/user_repository_mock.py | 2 +- .../app/test_create_user_presenter.py | 2 +- .../app/test_create_user_usecase.py | 15 ++++--- .../app/test_create_user_viewmodel.py | 43 ++++++++++++++----- .../app/test_delete_user_viewmodel.py | 2 +- .../app/test_get_all_users_viewmodel.py | 2 +- .../get_user/app/test_get_user_viewmodel.py | 2 +- .../app/test_update_user_viewmodel.py | 2 +- tests/shared/domain/entities/test_user.py | 2 +- .../shared/infra/dto/test_user_dynamo_dto.py | 2 +- .../repositories/test_user_repository_mock.py | 2 +- 20 files changed, 89 insertions(+), 66 deletions(-) diff --git a/src/modules/create_user/app/create_user_controller.py b/src/modules/create_user/app/create_user_controller.py index 73329c3..db54fe5 100644 --- a/src/modules/create_user/app/create_user_controller.py +++ b/src/modules/create_user/app/create_user_controller.py @@ -10,7 +10,7 @@ from src.shared.helpers.errors.usecase_errors import NoItemsFound from src.shared.helpers.external_interfaces.http_codes import OK, NotFound, BadRequest, InternalServerError, Created -from src.shared.domain.enums.role_enum import RoleEnum +from src.shared.domain.enums.role_enum import ROLE class CreateUserController: @@ -28,7 +28,7 @@ def __call__(self, request: IRequest) -> IResponse: user = self.CreateUserUsecase( email=request.data.get('email'), password_hash=request.data.get('password_hash'), - role=request.data.get('role') or RoleEnum.USER + role=request.data.get('role') or ROLE.USER ) viewmodel = CreateUserViewmodel(user) diff --git a/src/modules/create_user/app/create_user_usecase.py b/src/modules/create_user/app/create_user_usecase.py index 8d89f48..160e57f 100644 --- a/src/modules/create_user/app/create_user_usecase.py +++ b/src/modules/create_user/app/create_user_usecase.py @@ -1,8 +1,10 @@ +from pydantic import ValidationError from src.shared.domain.entities.user import User -from src.shared.domain.enums.role_enum import RoleEnum +from src.shared.domain.enums.role_enum import ROLE from src.shared.domain.repositories.user_repository_interface import IUserRepository + from src.shared.helpers.errors.domain_errors import EntityError @@ -10,11 +12,11 @@ class CreateUserUsecase: def __init__(self, repo: IUserRepository): self.repo = repo - def __call__(self, email: str, password_hash: str, role: RoleEnum = RoleEnum.USER) -> User: + def __call__(self, email: str, password_hash: str, role: ROLE = ROLE.USER) -> User: user = User( email=email, password_hash=password_hash, role=role - ) + ) - return self.repo.create_user(user) + return self.repo.create_user(user) \ No newline at end of file diff --git a/src/modules/create_user/app/create_user_viewmodel.py b/src/modules/create_user/app/create_user_viewmodel.py index e38d419..d60edd4 100644 --- a/src/modules/create_user/app/create_user_viewmodel.py +++ b/src/modules/create_user/app/create_user_viewmodel.py @@ -1,12 +1,12 @@ from src.shared.domain.entities.user import User -from src.shared.domain.enums.role_enum import RoleEnum +from src.shared.domain.enums.role_enum import ROLE from uuid import UUID class CreateUserViewmodel: user_id: UUID name: str email: str - role: RoleEnum + role: ROLE def __init__(self, user: User): self.user_id = user.user_id diff --git a/src/modules/delete_user/app/delete_user_viewmodel.py b/src/modules/delete_user/app/delete_user_viewmodel.py index 83b6a6e..2df8599 100644 --- a/src/modules/delete_user/app/delete_user_viewmodel.py +++ b/src/modules/delete_user/app/delete_user_viewmodel.py @@ -1,24 +1,22 @@ +import uuid from src.shared.domain.entities.user import User -from src.shared.domain.enums.state_enum import STATE +from src.shared.domain.enums.role_enum import ROLE class DeleteUserViewmodel: - user_id: int - name: str + user_id: uuid.UUID email: str - state: STATE + role: ROLE def __init__(self, user: User): self.user_id = user.user_id - self.name = user.name self.email = user.email - self.state = user.state + self.role = user.role def to_dict(self): return { 'user_id': self.user_id, - 'name': self.name, 'email': self.email, - 'state': self.state.value, + 'role': self.role, 'message': "the user was deleted successfully" } diff --git a/src/modules/get_user/app/get_user_viewmodel.py b/src/modules/get_user/app/get_user_viewmodel.py index bb4be7f..c829f49 100644 --- a/src/modules/get_user/app/get_user_viewmodel.py +++ b/src/modules/get_user/app/get_user_viewmodel.py @@ -1,24 +1,24 @@ +import uuid + from src.shared.domain.entities.user import User -from src.shared.domain.enums.state_enum import STATE +from src.shared.domain.enums.role_enum import ROLE class GetUserViewmodel: - user_id: int - name: str + user_id: uuid.UUID email: str - state: STATE + role: ROLE def __init__(self, user: User): self.user_id = user.user_id - self.name = user.name self.email = user.email - self.state = user.state + self.role = user.role def to_dict(self): return { 'user_id': self.user_id, - 'name': self.name, + 'email': self.email, - 'state': self.state.value, + 'role': self.role, 'message': "the user was retrieved successfully" } diff --git a/src/modules/update_user/app/update_user_viewmodel.py b/src/modules/update_user/app/update_user_viewmodel.py index 85bcc49..6fb4202 100644 --- a/src/modules/update_user/app/update_user_viewmodel.py +++ b/src/modules/update_user/app/update_user_viewmodel.py @@ -1,25 +1,25 @@ +from uuid import UUID + from src.shared.domain.entities.user import User -from src.shared.domain.enums.state_enum import STATE +from src.shared.domain.enums.role_enum import ROLE class UpdateUserViewmodel: - user_id: int + user_id: UUID name: str email: str - state: STATE + role: ROLE def __init__(self, user: User): self.user_id = user.user_id - self.name = user.name self.email = user.email - self.state = user.state + self.role = user.role def to_dict(self): return { - 'user_id': self.user_id, - 'name': self.name, + 'user_id': self.user_id, 'email': self.email, - 'state': self.state.value, + 'role': self.role, 'message': "the user was updated successfully" } diff --git a/src/shared/domain/entities/user.py b/src/shared/domain/entities/user.py index 9eeda8c..bc6eb23 100644 --- a/src/shared/domain/entities/user.py +++ b/src/shared/domain/entities/user.py @@ -2,7 +2,7 @@ import uuid from pydantic import BaseModel, Field, EmailStr, ConfigDict -from src.shared.domain.enums.role_enum import RoleEnum +from src.shared.domain.enums.role_enum import ROLE class User(BaseModel): @@ -18,14 +18,15 @@ class User(BaseModel): examples=["usuario@example.com"] ) - role: RoleEnum = Field( - default=RoleEnum.USER, + role: ROLE = Field( + default=ROLE.USER, description="Função do usuário", ) password_hash: str = Field( ..., description="Hash da senha do usuário", + min_length=4 ) model_config = ConfigDict( diff --git a/src/shared/domain/enums/role_enum.py b/src/shared/domain/enums/role_enum.py index 4f46e6b..eb84cf1 100644 --- a/src/shared/domain/enums/role_enum.py +++ b/src/shared/domain/enums/role_enum.py @@ -1,5 +1,5 @@ from enum import Enum -class RoleEnum(str, Enum): +class ROLE(str, Enum): ADMIN = "admin" USER = "user" \ No newline at end of file diff --git a/src/shared/infra/dto/user_dynamo_dto.py b/src/shared/infra/dto/user_dynamo_dto.py index fee2c45..06ed8ca 100644 --- a/src/shared/infra/dto/user_dynamo_dto.py +++ b/src/shared/infra/dto/user_dynamo_dto.py @@ -1,16 +1,16 @@ from decimal import Decimal from src.shared.domain.entities.user import User -from src.shared.domain.enums.state_enum import STATE +from src.shared.domain.enums.role_enum import ROLE class UserDynamoDTO: name: str email: str - state: STATE + role: ROLE user_id: int - def __init__(self, name: str, email: str, state: STATE, user_id: int): + def __init__(self, name: str, email: str, role: ROLE, user_id: int): self.name = name self.email = email self.user_id = user_id diff --git a/src/shared/infra/repositories/user_repository_mock.py b/src/shared/infra/repositories/user_repository_mock.py index abc498a..f86c9a7 100644 --- a/src/shared/infra/repositories/user_repository_mock.py +++ b/src/shared/infra/repositories/user_repository_mock.py @@ -1,7 +1,7 @@ from typing import List from src.shared.domain.entities.user import User -from src.shared.domain.enums.state_enum import STATE +from src.shared.domain.enums.role_enum import ROLE from src.shared.domain.repositories.user_repository_interface import IUserRepository from src.shared.helpers.errors.usecase_errors import NoItemsFound diff --git a/tests/modules/create_user/app/test_create_user_presenter.py b/tests/modules/create_user/app/test_create_user_presenter.py index 4b8eaea..cfed519 100644 --- a/tests/modules/create_user/app/test_create_user_presenter.py +++ b/tests/modules/create_user/app/test_create_user_presenter.py @@ -52,7 +52,7 @@ def test_create_user(self): "time": "12/Mar/2020:19:03:58 +0000", "timeEpoch": 1583348638390 }, - "body": '{"name":"EhOLudjas", "email":"eho@ludjas.com"}', + "body": '{"email":"user@example.com", "password_hash": "hash_da_senha", "role": "admin"}', "pathParameters": None, "isBase64Encoded": None, "stageVariables": None diff --git a/tests/modules/create_user/app/test_create_user_usecase.py b/tests/modules/create_user/app/test_create_user_usecase.py index 8ca54b9..79093c9 100644 --- a/tests/modules/create_user/app/test_create_user_usecase.py +++ b/tests/modules/create_user/app/test_create_user_usecase.py @@ -11,22 +11,23 @@ def test_create_user(self): repo = UserRepositoryMock() usecase = CreateUserUsecase(repo) - user = usecase(name="Vitor Choueri", email="branco@branco.branco") + user = usecase(email="user@example.com", password_hash="hash_senha") assert repo.users[-1] == user - def test_create_user_invalid_name(self): + def test_create_user_invalid_email(self): repo = UserRepositoryMock() usecase = CreateUserUsecase(repo) with pytest.raises(EntityError): - user = usecase(name="V", email="branco@branco.branco") - - def test_create_user_invalid_email(self): + user = usecase(email="branco@brancobranco", password_hash="hash_senha") + + def test_create_user_invalid_password(self): repo = UserRepositoryMock() - usecase = CreateUserUsecase(repo) + usecase = CreateUserUsecase(repo=repo) with pytest.raises(EntityError): - user = usecase(name="Vitor Choueri", email="branco@brancobranco") + user = usecase(email="user@example.com", password_hash="h") + diff --git a/tests/modules/create_user/app/test_create_user_viewmodel.py b/tests/modules/create_user/app/test_create_user_viewmodel.py index 2437cc2..09bc8a2 100644 --- a/tests/modules/create_user/app/test_create_user_viewmodel.py +++ b/tests/modules/create_user/app/test_create_user_viewmodel.py @@ -1,22 +1,43 @@ +import uuid + from src.modules.create_user.app.create_user_viewmodel import CreateUserViewmodel from src.shared.domain.entities.user import User -from src.shared.domain.enums.state_enum import STATE +from src.shared.domain.enums.role_enum import ROLE class Test_CreateUserViewModel: - def test_create_user_viewmodel(self): + def test_create_user_viewmodel_admin(self): + user_uuid = uuid.uuid4() user = User( - user_id=1, - name="Vitor Soller", - email="vitinho@hype.com", - state=STATE.APPROVED + user_id=user_uuid, + email="user@example.com", + password_hash="hash_senha", + role=ROLE.ADMIN ) userViewmodel = CreateUserViewmodel(user=user).to_dict() - expected = {'user_id': 1, - 'name': 'Vitor Soller', - 'email': 'vitinho@hype.com', - 'state': 'APPROVED', - 'message': 'the user was created successfully'} + expected = {'user_id': user_uuid, + 'email': 'user@example.com', + 'role': 'admin', + 'message': 'the user was created successfully' + } assert expected == userViewmodel + + def test_create_user_viewmodel_user(self): + user_uuid = uuid.uuid4() + user = User( + user_id=user_uuid, + email="user@example.com", + password_hash="hash_senha", + role=ROLE.USER + ) + userViewmodel = CreateUserViewmodel(user=user).to_dict() + + expected = {'user_id': user_uuid, + 'email': 'user@example.com', + 'role': 'user', + 'message': 'the user was created successfully' + } + + assert expected == userViewmodel \ No newline at end of file diff --git a/tests/modules/delete_user/app/test_delete_user_viewmodel.py b/tests/modules/delete_user/app/test_delete_user_viewmodel.py index 5a3d38c..e3c05ee 100644 --- a/tests/modules/delete_user/app/test_delete_user_viewmodel.py +++ b/tests/modules/delete_user/app/test_delete_user_viewmodel.py @@ -1,6 +1,6 @@ from src.modules.delete_user.app.delete_user_viewmodel import DeleteUserViewmodel from src.shared.domain.entities.user import User -from src.shared.domain.enums.state_enum import STATE +from src.shared.domain.enums.role_enum import ROLE class Test_DeleteUserViewmodel: diff --git a/tests/modules/get_all_users/app/test_get_all_users_viewmodel.py b/tests/modules/get_all_users/app/test_get_all_users_viewmodel.py index 0626028..9bc5ef0 100644 --- a/tests/modules/get_all_users/app/test_get_all_users_viewmodel.py +++ b/tests/modules/get_all_users/app/test_get_all_users_viewmodel.py @@ -2,7 +2,7 @@ from src.modules.get_all_users.app.get_all_users_viewmodel import GetAllUsersViewmodel, UserViewmodel from src.shared.domain.entities.user import User -from src.shared.domain.enums.state_enum import STATE +from src.shared.domain.enums.role_enum import ROLE class Test_GetAllUsersViewmodel: diff --git a/tests/modules/get_user/app/test_get_user_viewmodel.py b/tests/modules/get_user/app/test_get_user_viewmodel.py index dae0fc6..ef61e53 100644 --- a/tests/modules/get_user/app/test_get_user_viewmodel.py +++ b/tests/modules/get_user/app/test_get_user_viewmodel.py @@ -1,6 +1,6 @@ from src.modules.get_user.app.get_user_viewmodel import GetUserViewmodel from src.shared.domain.entities.user import User -from src.shared.domain.enums.state_enum import STATE +from src.shared.domain.enums.role_enum import ROLE class Test_GetUserViewModel: diff --git a/tests/modules/update_user/app/test_update_user_viewmodel.py b/tests/modules/update_user/app/test_update_user_viewmodel.py index 877c052..4400ab5 100644 --- a/tests/modules/update_user/app/test_update_user_viewmodel.py +++ b/tests/modules/update_user/app/test_update_user_viewmodel.py @@ -1,6 +1,6 @@ from src.modules.update_user.app.update_user_viewmodel import UpdateUserViewmodel from src.shared.domain.entities.user import User -from src.shared.domain.enums.state_enum import STATE +from src.shared.domain.enums.role_enum import ROLE class Test_UpadateUserViewmodel: diff --git a/tests/shared/domain/entities/test_user.py b/tests/shared/domain/entities/test_user.py index c32ff66..b61bc7c 100644 --- a/tests/shared/domain/entities/test_user.py +++ b/tests/shared/domain/entities/test_user.py @@ -3,7 +3,7 @@ from pydantic import ValidationError from src.shared.domain.entities.user import User -from src.shared.domain.enums.role_enum import RoleEnum +from src.shared.domain.enums.role_enum import ROLE class Test_User: diff --git a/tests/shared/infra/dto/test_user_dynamo_dto.py b/tests/shared/infra/dto/test_user_dynamo_dto.py index a3a71ad..9400dca 100644 --- a/tests/shared/infra/dto/test_user_dynamo_dto.py +++ b/tests/shared/infra/dto/test_user_dynamo_dto.py @@ -1,7 +1,7 @@ from decimal import Decimal from src.shared.domain.entities.user import User -from src.shared.domain.enums.state_enum import STATE +from src.shared.domain.enums.role_enum import ROLE from src.shared.infra.dto.user_dynamo_dto import UserDynamoDTO from src.shared.infra.repositories.user_repository_mock import UserRepositoryMock diff --git a/tests/shared/infra/repositories/test_user_repository_mock.py b/tests/shared/infra/repositories/test_user_repository_mock.py index b13b80c..2b95126 100644 --- a/tests/shared/infra/repositories/test_user_repository_mock.py +++ b/tests/shared/infra/repositories/test_user_repository_mock.py @@ -1,5 +1,5 @@ from src.shared.domain.entities.user import User -from src.shared.domain.enums.state_enum import STATE +from src.shared.domain.enums.role_enum import ROLE from src.shared.helpers.errors.usecase_errors import NoItemsFound from src.shared.infra.repositories.user_repository_mock import UserRepositoryMock import pytest From 144657a9e1ca5a2e27e7ad663123fab138e6350a Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Mon, 20 Jul 2026 12:15:19 -0300 Subject: [PATCH 06/50] refactor: comenta testes do template --- tests/modules/create_user/app/test_create_user_controller.py | 3 ++- tests/modules/create_user/app/test_create_user_presenter.py | 3 ++- tests/modules/create_user/app/test_create_user_usecase.py | 2 ++ tests/modules/create_user/app/test_create_user_viewmodel.py | 4 ++-- tests/modules/delete_user/app/test_delete_user_controller.py | 3 ++- tests/modules/delete_user/app/test_delete_user_presenter.py | 3 ++- tests/modules/delete_user/app/test_delete_user_usecase.py | 3 ++- tests/modules/delete_user/app/test_delete_user_viewmodel.py | 3 ++- .../get_all_users/app/test_get_all_users_controller.py | 3 ++- .../modules/get_all_users/app/test_get_all_users_presenter.py | 3 ++- tests/modules/get_all_users/app/test_get_all_users_usecase.py | 3 ++- .../modules/get_all_users/app/test_get_all_users_viewmodel.py | 3 ++- tests/modules/get_user/app/test_get_user_controller.py | 3 ++- tests/modules/get_user/app/test_get_user_presenter.py | 3 ++- tests/modules/get_user/app/test_get_user_usecase.py | 3 ++- tests/modules/get_user/app/test_get_user_viewmodel.py | 3 ++- tests/modules/update_user/app/test_update_user_controller.py | 3 ++- tests/modules/update_user/app/test_update_user_presenter.py | 3 ++- tests/modules/update_user/app/test_update_user_usecase.py | 3 ++- tests/modules/update_user/app/test_update_user_viewmodel.py | 3 ++- tests/shared/domain/entities/test_user.py | 2 +- tests/shared/infra/dto/test_user_dynamo_dto.py | 3 ++- 22 files changed, 43 insertions(+), 22 deletions(-) diff --git a/tests/modules/create_user/app/test_create_user_controller.py b/tests/modules/create_user/app/test_create_user_controller.py index 380ff7d..6463334 100644 --- a/tests/modules/create_user/app/test_create_user_controller.py +++ b/tests/modules/create_user/app/test_create_user_controller.py @@ -1,4 +1,4 @@ -from src.modules.create_user.app.create_user_controller import CreateUserController +""" from src.modules.create_user.app.create_user_controller import CreateUserController from src.modules.create_user.app.create_user_usecase import CreateUserUsecase from src.shared.helpers.external_interfaces.http_models import HttpRequest from src.shared.infra.repositories.user_repository_mock import UserRepositoryMock @@ -71,3 +71,4 @@ def test_create_user_controller_invalid_email(self): + """ \ No newline at end of file diff --git a/tests/modules/create_user/app/test_create_user_presenter.py b/tests/modules/create_user/app/test_create_user_presenter.py index cfed519..4485e95 100644 --- a/tests/modules/create_user/app/test_create_user_presenter.py +++ b/tests/modules/create_user/app/test_create_user_presenter.py @@ -1,4 +1,4 @@ -import json +""" import json from src.modules.create_user.app.create_user_presenter import lambda_handler @@ -119,3 +119,4 @@ def test_create_user(self): assert response["statusCode"] == 201 assert json.loads(response["body"])["message"] == "the user was created successfully" + """ \ No newline at end of file diff --git a/tests/modules/create_user/app/test_create_user_usecase.py b/tests/modules/create_user/app/test_create_user_usecase.py index 79093c9..b78083a 100644 --- a/tests/modules/create_user/app/test_create_user_usecase.py +++ b/tests/modules/create_user/app/test_create_user_usecase.py @@ -1,3 +1,4 @@ +""" import pytest from src.modules.create_user.app.create_user_usecase import CreateUserUsecase @@ -31,3 +32,4 @@ def test_create_user_invalid_password(self): +""" \ No newline at end of file diff --git a/tests/modules/create_user/app/test_create_user_viewmodel.py b/tests/modules/create_user/app/test_create_user_viewmodel.py index 09bc8a2..ba903bd 100644 --- a/tests/modules/create_user/app/test_create_user_viewmodel.py +++ b/tests/modules/create_user/app/test_create_user_viewmodel.py @@ -1,4 +1,4 @@ -import uuid +""" import uuid from src.modules.create_user.app.create_user_viewmodel import CreateUserViewmodel from src.shared.domain.entities.user import User @@ -40,4 +40,4 @@ def test_create_user_viewmodel_user(self): 'message': 'the user was created successfully' } - assert expected == userViewmodel \ No newline at end of file + assert expected == userViewmodel """ \ No newline at end of file diff --git a/tests/modules/delete_user/app/test_delete_user_controller.py b/tests/modules/delete_user/app/test_delete_user_controller.py index 78ea011..5e3978d 100644 --- a/tests/modules/delete_user/app/test_delete_user_controller.py +++ b/tests/modules/delete_user/app/test_delete_user_controller.py @@ -1,4 +1,4 @@ -from src.modules.delete_user.app.delete_user_controller import DeleteUserController +""" from src.modules.delete_user.app.delete_user_controller import DeleteUserController from src.modules.delete_user.app.delete_user_usecase import DeleteUserUsecase from src.shared.helpers.external_interfaces.http_models import HttpRequest from src.shared.infra.repositories.user_repository_mock import UserRepositoryMock @@ -76,3 +76,4 @@ def test_delete_user_controller_no_items_found(self): assert response.body == 'No items found for user_id' + """ \ No newline at end of file diff --git a/tests/modules/delete_user/app/test_delete_user_presenter.py b/tests/modules/delete_user/app/test_delete_user_presenter.py index 9167bca..f7c38e0 100644 --- a/tests/modules/delete_user/app/test_delete_user_presenter.py +++ b/tests/modules/delete_user/app/test_delete_user_presenter.py @@ -1,4 +1,4 @@ -import json +""" import json from src.modules.delete_user.app.delete_user_presenter import lambda_handler @@ -68,3 +68,4 @@ def test_delete_user(self): assert json.loads(response["body"]) == expected assert response["statusCode"] == 200 + """ \ No newline at end of file diff --git a/tests/modules/delete_user/app/test_delete_user_usecase.py b/tests/modules/delete_user/app/test_delete_user_usecase.py index e7ce5b7..32bdc8d 100644 --- a/tests/modules/delete_user/app/test_delete_user_usecase.py +++ b/tests/modules/delete_user/app/test_delete_user_usecase.py @@ -1,4 +1,4 @@ -import pytest +""" import pytest from src.modules.delete_user.app.delete_user_usecase import DeleteUserUsecase from src.shared.helpers.errors.domain_errors import EntityError @@ -30,3 +30,4 @@ def test_delete_user_invalid_id(self): with pytest.raises(EntityError): user = usecase("invalid") + """ \ No newline at end of file diff --git a/tests/modules/delete_user/app/test_delete_user_viewmodel.py b/tests/modules/delete_user/app/test_delete_user_viewmodel.py index e3c05ee..4f9b196 100644 --- a/tests/modules/delete_user/app/test_delete_user_viewmodel.py +++ b/tests/modules/delete_user/app/test_delete_user_viewmodel.py @@ -1,4 +1,4 @@ -from src.modules.delete_user.app.delete_user_viewmodel import DeleteUserViewmodel +""" from src.modules.delete_user.app.delete_user_viewmodel import DeleteUserViewmodel from src.shared.domain.entities.user import User from src.shared.domain.enums.role_enum import ROLE @@ -21,3 +21,4 @@ def test_delete_user_viewmodel(self): 'message': 'the user was deleted successfully'} assert expected == delete_user_viewmodel.to_dict() + """ \ No newline at end of file diff --git a/tests/modules/get_all_users/app/test_get_all_users_controller.py b/tests/modules/get_all_users/app/test_get_all_users_controller.py index 4c6f70d..c97d059 100644 --- a/tests/modules/get_all_users/app/test_get_all_users_controller.py +++ b/tests/modules/get_all_users/app/test_get_all_users_controller.py @@ -1,4 +1,4 @@ -from src.modules.get_all_users.app.get_all_users_controller import GetAllUsersController +""" from src.modules.get_all_users.app.get_all_users_controller import GetAllUsersController from src.modules.get_all_users.app.get_all_users_usecase import GetAllUsersUsecase from src.shared.infra.repositories.user_repository_mock import UserRepositoryMock @@ -14,3 +14,4 @@ def test_get_all_users_controller(self): assert response.status_code == 200 + """ \ No newline at end of file diff --git a/tests/modules/get_all_users/app/test_get_all_users_presenter.py b/tests/modules/get_all_users/app/test_get_all_users_presenter.py index 63f4ddd..d48d60b 100644 --- a/tests/modules/get_all_users/app/test_get_all_users_presenter.py +++ b/tests/modules/get_all_users/app/test_get_all_users_presenter.py @@ -1,4 +1,4 @@ -import json +""" import json from src.modules.get_all_users.app.get_all_users_presenter import lambda_handler @@ -85,3 +85,4 @@ def test_get_all_users_presenter(self): assert response["statusCode"] == 200 assert json.loads(response["body"]) == expected_body + """ \ No newline at end of file diff --git a/tests/modules/get_all_users/app/test_get_all_users_usecase.py b/tests/modules/get_all_users/app/test_get_all_users_usecase.py index ea08022..bee9f26 100644 --- a/tests/modules/get_all_users/app/test_get_all_users_usecase.py +++ b/tests/modules/get_all_users/app/test_get_all_users_usecase.py @@ -1,4 +1,4 @@ -from src.modules.get_all_users.app.get_all_users_usecase import GetAllUsersUsecase +""" from src.modules.get_all_users.app.get_all_users_usecase import GetAllUsersUsecase from src.shared.infra.repositories.user_repository_mock import UserRepositoryMock @@ -13,3 +13,4 @@ def test_get_all_users_usecase(self): assert all_users_list_returned == repo_mock.users assert len(all_users_list_returned) == len(repo_mock.users) + """ \ No newline at end of file diff --git a/tests/modules/get_all_users/app/test_get_all_users_viewmodel.py b/tests/modules/get_all_users/app/test_get_all_users_viewmodel.py index 9bc5ef0..7bc6beb 100644 --- a/tests/modules/get_all_users/app/test_get_all_users_viewmodel.py +++ b/tests/modules/get_all_users/app/test_get_all_users_viewmodel.py @@ -1,4 +1,4 @@ -import uuid +""" import uuid from src.modules.get_all_users.app.get_all_users_viewmodel import GetAllUsersViewmodel, UserViewmodel from src.shared.domain.entities.user import User @@ -56,3 +56,4 @@ def test_user_viewmodel(self): assert response == expected + """ \ No newline at end of file diff --git a/tests/modules/get_user/app/test_get_user_controller.py b/tests/modules/get_user/app/test_get_user_controller.py index 4747fd1..101bc05 100644 --- a/tests/modules/get_user/app/test_get_user_controller.py +++ b/tests/modules/get_user/app/test_get_user_controller.py @@ -1,4 +1,4 @@ -from src.modules.get_user.app.get_user_controller import GetUserController +""" from src.modules.get_user.app.get_user_controller import GetUserController from src.modules.get_user.app.get_user_usecase import GetUserUsecase from src.shared.helpers.external_interfaces.http_models import HttpRequest from src.shared.infra.external.observability.observability_mock import ObservabilityMock @@ -78,3 +78,4 @@ def test_get_user_controller_no_items_found(self): assert response.status_code == 404 assert response.body == 'No items found for user_id' + """ \ No newline at end of file diff --git a/tests/modules/get_user/app/test_get_user_presenter.py b/tests/modules/get_user/app/test_get_user_presenter.py index 1526231..5e656c8 100644 --- a/tests/modules/get_user/app/test_get_user_presenter.py +++ b/tests/modules/get_user/app/test_get_user_presenter.py @@ -1,4 +1,4 @@ -import json +""" import json from dataclasses import dataclass import pytest @@ -66,3 +66,4 @@ def test_get_user(self): assert json.loads(response["body"])["email"] == "soller@soller.com" assert json.loads(response["body"])["state"] == "APPROVED" assert json.loads(response["body"])["user_id"] == 1 + """ \ No newline at end of file diff --git a/tests/modules/get_user/app/test_get_user_usecase.py b/tests/modules/get_user/app/test_get_user_usecase.py index cadb599..86c75fa 100644 --- a/tests/modules/get_user/app/test_get_user_usecase.py +++ b/tests/modules/get_user/app/test_get_user_usecase.py @@ -1,4 +1,4 @@ -import pytest +""" import pytest from src.modules.get_user.app.get_user_usecase import GetUserUsecase from src.shared.helpers.errors.domain_errors import EntityError @@ -31,3 +31,4 @@ def test_get_user_invalid_id(self): with pytest.raises(EntityError): user = usecase(user_id="invalid") + """ \ No newline at end of file diff --git a/tests/modules/get_user/app/test_get_user_viewmodel.py b/tests/modules/get_user/app/test_get_user_viewmodel.py index ef61e53..5d408b6 100644 --- a/tests/modules/get_user/app/test_get_user_viewmodel.py +++ b/tests/modules/get_user/app/test_get_user_viewmodel.py @@ -1,4 +1,4 @@ -from src.modules.get_user.app.get_user_viewmodel import GetUserViewmodel +""" from src.modules.get_user.app.get_user_viewmodel import GetUserViewmodel from src.shared.domain.entities.user import User from src.shared.domain.enums.role_enum import ROLE @@ -20,3 +20,4 @@ def test_get_user_viewmodel(self): 'message': 'the user was retrieved successfully'} assert expected == userViewmodel + """ \ No newline at end of file diff --git a/tests/modules/update_user/app/test_update_user_controller.py b/tests/modules/update_user/app/test_update_user_controller.py index 928d29d..bfc3789 100644 --- a/tests/modules/update_user/app/test_update_user_controller.py +++ b/tests/modules/update_user/app/test_update_user_controller.py @@ -1,4 +1,4 @@ -from src.modules.update_user.app.update_user_controller import UpdateUserController +""" from src.modules.update_user.app.update_user_controller import UpdateUserController from src.modules.update_user.app.update_user_usecase import UpdateUserUsecase from src.shared.helpers.external_interfaces.http_models import HttpRequest from src.shared.infra.repositories.user_repository_mock import UserRepositoryMock @@ -81,3 +81,4 @@ def test_update_user_not_found(self): assert response.status_code == 404 assert response.body == 'No items found for user_id' + """ \ No newline at end of file diff --git a/tests/modules/update_user/app/test_update_user_presenter.py b/tests/modules/update_user/app/test_update_user_presenter.py index e1eab6f..baad578 100644 --- a/tests/modules/update_user/app/test_update_user_presenter.py +++ b/tests/modules/update_user/app/test_update_user_presenter.py @@ -1,4 +1,4 @@ -import json +""" import json from src.modules.update_user.app.update_user_presenter import lambda_handler @@ -63,3 +63,4 @@ def test_update_user(self): assert response["statusCode"] == 200 assert json.loads(response["body"])['name'] == 'João Soller' + """ \ No newline at end of file diff --git a/tests/modules/update_user/app/test_update_user_usecase.py b/tests/modules/update_user/app/test_update_user_usecase.py index 3e28f9a..df58e56 100644 --- a/tests/modules/update_user/app/test_update_user_usecase.py +++ b/tests/modules/update_user/app/test_update_user_usecase.py @@ -1,4 +1,4 @@ -import pytest +""" import pytest from src.modules.update_user.app.update_user_usecase import UpdateUserUsecase from src.shared.helpers.errors.domain_errors import EntityError @@ -27,3 +27,4 @@ def test_update_user_usecase_wrong_new_name(selfs): with pytest.raises(EntityError): usecase(user_id=1, new_name=1) + """ \ No newline at end of file diff --git a/tests/modules/update_user/app/test_update_user_viewmodel.py b/tests/modules/update_user/app/test_update_user_viewmodel.py index 4400ab5..99f91b6 100644 --- a/tests/modules/update_user/app/test_update_user_viewmodel.py +++ b/tests/modules/update_user/app/test_update_user_viewmodel.py @@ -1,4 +1,4 @@ -from src.modules.update_user.app.update_user_viewmodel import UpdateUserViewmodel +""" from src.modules.update_user.app.update_user_viewmodel import UpdateUserViewmodel from src.shared.domain.entities.user import User from src.shared.domain.enums.role_enum import ROLE @@ -18,3 +18,4 @@ def test_update_user_viewmodel(self): } assert expected == updated_useer_viewmodel.to_dict() + """ \ No newline at end of file diff --git a/tests/shared/domain/entities/test_user.py b/tests/shared/domain/entities/test_user.py index b61bc7c..e690efa 100644 --- a/tests/shared/domain/entities/test_user.py +++ b/tests/shared/domain/entities/test_user.py @@ -26,7 +26,7 @@ def test_user_not_has_password_hash(self): def test_user_with_custom_id(self): user_id = uuid.uuid4() user = User(user_id=user_id, email="usuario@example.com", password_hash="hash_da_senha") - assert user.id == user_id + assert user.user_id == user_id def test_user_role_is_admin(self): user = User(email="admin@example.com", password_hash="hash_da_senha", role="admin") diff --git a/tests/shared/infra/dto/test_user_dynamo_dto.py b/tests/shared/infra/dto/test_user_dynamo_dto.py index 9400dca..e8e84d8 100644 --- a/tests/shared/infra/dto/test_user_dynamo_dto.py +++ b/tests/shared/infra/dto/test_user_dynamo_dto.py @@ -1,4 +1,4 @@ -from decimal import Decimal +""" from decimal import Decimal from src.shared.domain.entities.user import User from src.shared.domain.enums.role_enum import ROLE @@ -130,3 +130,4 @@ def test_from_entity_to_dynamo(self): } assert user_dynamo == expected_dict + """ \ No newline at end of file From 9f8e28fbee8ce39d1734d7ef08967287123fac6b Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Fri, 24 Jul 2026 15:46:40 -0300 Subject: [PATCH 07/50] =?UTF-8?q?Revert=20"feat:=20implementa=20cria=C3=A7?= =?UTF-8?q?=C3=A3o=20de=20usu=C3=A1rio=20com=20valida=C3=A7=C3=A3o=20de=20?= =?UTF-8?q?dados=20e=20gerenciamento=20de=20pap=C3=A9is"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit ca6204b920f7ca807cf555e1de9d48edea58e052. --- .../create_user/app/create_user_controller.py | 25 ++------- .../create_user/app/create_user_usecase.py | 17 ++++-- .../create_user/app/create_user_viewmodel.py | 14 ++--- .../app/get_all_users_viewmodel.py | 10 ++-- .../app/test_create_user_controller.py | 36 ++++++++----- .../app/test_get_all_users_viewmodel.py | 53 ++++++++++--------- 6 files changed, 82 insertions(+), 73 deletions(-) diff --git a/src/modules/create_user/app/create_user_controller.py b/src/modules/create_user/app/create_user_controller.py index db54fe5..29e3a59 100644 --- a/src/modules/create_user/app/create_user_controller.py +++ b/src/modules/create_user/app/create_user_controller.py @@ -1,17 +1,11 @@ -from pydantic import ValidationError - from src.shared.helpers.external_interfaces.external_interface import IResponse, IRequest from .create_user_usecase import CreateUserUsecase from .create_user_viewmodel import CreateUserViewmodel - - from src.shared.helpers.errors.controller_errors import MissingParameters, WrongTypeParameter from src.shared.helpers.errors.domain_errors import EntityError from src.shared.helpers.errors.usecase_errors import NoItemsFound from src.shared.helpers.external_interfaces.http_codes import OK, NotFound, BadRequest, InternalServerError, Created -from src.shared.domain.enums.role_enum import ROLE - class CreateUserController: @@ -20,15 +14,14 @@ def __init__(self, usecase: CreateUserUsecase): def __call__(self, request: IRequest) -> IResponse: try: + if request.data.get('name') is None: + raise MissingParameters('name') if request.data.get('email') is None: raise MissingParameters('email') - if request.data.get('password_hash') is None: - raise MissingParameters('password_hash') user = self.CreateUserUsecase( - email=request.data.get('email'), - password_hash=request.data.get('password_hash'), - role=request.data.get('role') or ROLE.USER + name=request.data.get('name'), + email=request.data.get('email') ) viewmodel = CreateUserViewmodel(user) @@ -46,16 +39,6 @@ def __call__(self, request: IRequest) -> IResponse: except WrongTypeParameter as err: return BadRequest(body=err.message) - - except ValidationError as err: - error = err.errors()[0] - - if error['loc'][0] == "email": - return BadRequest(body="Field email is not valid") - if error['loc'][0] == "password_hash": - return BadRequest(body="Field password_hash is not valid") - - return BadRequest(body="Invalid data") except EntityError as err: diff --git a/src/modules/create_user/app/create_user_usecase.py b/src/modules/create_user/app/create_user_usecase.py index 160e57f..11d0296 100644 --- a/src/modules/create_user/app/create_user_usecase.py +++ b/src/modules/create_user/app/create_user_usecase.py @@ -2,7 +2,7 @@ from pydantic import ValidationError from src.shared.domain.entities.user import User -from src.shared.domain.enums.role_enum import ROLE +from src.shared.domain.enums.state_enum import STATE from src.shared.domain.repositories.user_repository_interface import IUserRepository from src.shared.helpers.errors.domain_errors import EntityError @@ -12,11 +12,18 @@ class CreateUserUsecase: def __init__(self, repo: IUserRepository): self.repo = repo - def __call__(self, email: str, password_hash: str, role: ROLE = ROLE.USER) -> User: + def __call__(self, name: str, email: str) -> User: + + if not User.validate_name(name): + raise EntityError("name") + + if not User.validate_email(email): + raise EntityError("email") + user = User( + name=name, email=email, - password_hash=password_hash, - role=role - ) + state=STATE.PENDING + ) return self.repo.create_user(user) \ No newline at end of file diff --git a/src/modules/create_user/app/create_user_viewmodel.py b/src/modules/create_user/app/create_user_viewmodel.py index d60edd4..fd71ebc 100644 --- a/src/modules/create_user/app/create_user_viewmodel.py +++ b/src/modules/create_user/app/create_user_viewmodel.py @@ -1,22 +1,24 @@ from src.shared.domain.entities.user import User -from src.shared.domain.enums.role_enum import ROLE -from uuid import UUID +from src.shared.domain.enums.state_enum import STATE + class CreateUserViewmodel: - user_id: UUID + user_id: int name: str email: str - role: ROLE + state: STATE def __init__(self, user: User): self.user_id = user.user_id + self.name = user.name self.email = user.email - self.role = user.role + self.state = user.state def to_dict(self): return { 'user_id': self.user_id, + 'name': self.name, 'email': self.email, - 'role': self.role, + 'state': self.state.value, 'message': "the user was created successfully" } diff --git a/src/modules/get_all_users/app/get_all_users_viewmodel.py b/src/modules/get_all_users/app/get_all_users_viewmodel.py index b32173f..32ff93c 100644 --- a/src/modules/get_all_users/app/get_all_users_viewmodel.py +++ b/src/modules/get_all_users/app/get_all_users_viewmodel.py @@ -4,18 +4,18 @@ class UserViewmodel: - def __init__(self, user: User): - self.user_id = user.user_id + self.state = user.state self.email = user.email - self.role = user.role - + self.name = user.name + self.user_id = user.user_id def to_dict(self): return { 'user_id': self.user_id, + 'name': self.name, 'email': self.email, - 'role': self.role, + 'state': self.state.value } diff --git a/tests/modules/create_user/app/test_create_user_controller.py b/tests/modules/create_user/app/test_create_user_controller.py index 6463334..6ca8163 100644 --- a/tests/modules/create_user/app/test_create_user_controller.py +++ b/tests/modules/create_user/app/test_create_user_controller.py @@ -11,31 +11,31 @@ def test_create_user_controller(self): controller = CreateUserController(usecase=usecase) request = HttpRequest(body={ - 'email': 'branco@branco.com', - 'password_hash': 'hash_da_senha' + 'name': 'Branco do Branco Branco da Silva', + 'email': 'branco@branco.com' }) response = controller(request=request) assert response.status_code == 201 assert response.body['user_id'] == repo.users[-1].user_id + assert response.body['name'] == repo.users[-1].name assert response.body['email'] == repo.users[-1].email - assert response.body['role'] == repo.users[-1].role + assert response.body['state'] == repo.users[-1].state.value assert response.body['message'] == "the user was created successfully" - def test_create_user_controller_missing_password_hash(self): + def test_create_user_controller_missing_name(self): repo = UserRepositoryMock() usecase = CreateUserUsecase(repo=repo) controller = CreateUserController(usecase=usecase) request = HttpRequest(body={ - 'email': '21.01444-2@maua.br'} - ) + 'email': '21.01444-2@maua.br'}) response = controller(request=request) assert response.status_code == 400 - assert response.body == "Field password_hash is missing" + assert response.body == "Field name is missing" def test_create_user_controller_missing_email(self): @@ -44,8 +44,7 @@ def test_create_user_controller_missing_email(self): controller = CreateUserController(usecase=usecase) request = HttpRequest(body={ - 'password_hash': 'hash_da_senha' - }) + 'name': 'Branco do Branco Branco da Silva'}) response = controller(request=request) @@ -58,15 +57,28 @@ def test_create_user_controller_invalid_email(self): controller = CreateUserController(usecase=usecase) request = HttpRequest(body={ - 'email': 'branco@branco', - 'password_hash': 'hash_da_senha' - }) + 'name': 'Branco do Branco Branco da Silva', + 'email': 'branco@branco'}) response = controller(request=request) assert response.status_code == 400 assert response.body == "Field email is not valid" + def test_create_user_controller_invalid_name(self): + repo = UserRepositoryMock() + usecase = CreateUserUsecase(repo=repo) + controller = CreateUserController(usecase=usecase) + + request = HttpRequest(body={ + 'name': 'B', + 'email': 'branco@branco.com'}) + + response = controller(request=request) + + assert response.status_code == 400 + assert response.body == "Field name is not valid" + diff --git a/tests/modules/get_all_users/app/test_get_all_users_viewmodel.py b/tests/modules/get_all_users/app/test_get_all_users_viewmodel.py index 7bc6beb..6e14a6e 100644 --- a/tests/modules/get_all_users/app/test_get_all_users_viewmodel.py +++ b/tests/modules/get_all_users/app/test_get_all_users_viewmodel.py @@ -1,18 +1,20 @@ -""" import uuid - from src.modules.get_all_users.app.get_all_users_viewmodel import GetAllUsersViewmodel, UserViewmodel from src.shared.domain.entities.user import User from src.shared.domain.enums.role_enum import ROLE class Test_GetAllUsersViewmodel: - - uuid1 = uuid.uuid4() - uuid2 = uuid.uuid4() all_users_list = [ - User(user_id=uuid1, email="admin@example.com", role="admin", password_hash="hash_da_senha"), - User(user_id=uuid2, email="user@example.com", role="user", password_hash="hash_da_senha"), - ] + User(user_id=1, + name="Lucas Duez", + email="deuzexmachina@gmail.com", + state=STATE.APPROVED), + + User(user_id=2, + name="Laura Blablachan", + email="laurinha@gmail.com", + state=STATE.APPROVED), + ] def test_get_all_users_viewmodel(self): viewmodel = GetAllUsersViewmodel(self.all_users_list) @@ -20,14 +22,16 @@ def test_get_all_users_viewmodel(self): expected = { "all_users": [ { - 'user_id': self.uuid1, - 'email': "admin@example.com", - 'role': 'admin', + 'user_id': 1, + 'name': "Lucas Duez", + 'email': "deuzexmachina@gmail.com", + 'state': 'APPROVED', }, { - 'user_id': self.uuid2, - 'email': "user@example.com", - 'role': 'user', + 'user_id': 2, + 'name': "Laura Blablachan", + 'email': "laurinha@gmail.com", + 'state': 'APPROVED', } ], "message": "all users has been retrieved" @@ -39,21 +43,22 @@ def test_get_all_users_viewmodel(self): def test_user_viewmodel(self): viewmodel = UserViewmodel( - User( - user_id=self.uuid1, - email="admin@example.com", - role="admin", - password_hash="hash_da_senha"), - ) + User(user_id=2, + name="Laura Blablachan", + email="laurinha@gmail.com", + state=STATE.APPROVED), +) response = viewmodel.to_dict() expected = { - 'user_id': self.uuid1, - 'email': "admin@example.com", - 'role': "admin", + 'user_id': 2, + 'name': "Laura Blablachan", + 'email': "laurinha@gmail.com", + 'state': 'APPROVED', } assert response == expected - """ \ No newline at end of file + + From 87321abed94561b7f7b909573b4d54d08ba7e191 Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Fri, 24 Jul 2026 15:58:51 -0300 Subject: [PATCH 08/50] =?UTF-8?q?Revert=20"refactor:=20renomeia=20RoleEnum?= =?UTF-8?q?=20para=20ROLE=20e=20atualiza=20refer=C3=AAncias=20em=20todo=20?= =?UTF-8?q?o=20c=C3=B3digo"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit ec1d0a4f66ee58e77f30d1bbb5eeb38538e80957. --- .../delete_user/app/delete_user_viewmodel.py | 14 +++--- .../get_user/app/get_user_viewmodel.py | 16 +++---- .../update_user/app/update_user_viewmodel.py | 16 +++---- src/shared/infra/dto/user_dynamo_dto.py | 6 +-- .../repositories/user_repository_mock.py | 2 +- .../app/test_create_user_presenter.py | 2 +- .../app/test_create_user_usecase.py | 15 +++---- .../app/test_create_user_viewmodel.py | 43 +++++-------------- .../app/test_delete_user_viewmodel.py | 2 +- .../app/test_get_all_users_viewmodel.py | 2 +- .../get_user/app/test_get_user_viewmodel.py | 2 +- .../app/test_update_user_viewmodel.py | 2 +- .../shared/infra/dto/test_user_dynamo_dto.py | 2 +- 13 files changed, 52 insertions(+), 72 deletions(-) diff --git a/src/modules/delete_user/app/delete_user_viewmodel.py b/src/modules/delete_user/app/delete_user_viewmodel.py index 2df8599..83b6a6e 100644 --- a/src/modules/delete_user/app/delete_user_viewmodel.py +++ b/src/modules/delete_user/app/delete_user_viewmodel.py @@ -1,22 +1,24 @@ -import uuid from src.shared.domain.entities.user import User -from src.shared.domain.enums.role_enum import ROLE +from src.shared.domain.enums.state_enum import STATE class DeleteUserViewmodel: - user_id: uuid.UUID + user_id: int + name: str email: str - role: ROLE + state: STATE def __init__(self, user: User): self.user_id = user.user_id + self.name = user.name self.email = user.email - self.role = user.role + self.state = user.state def to_dict(self): return { 'user_id': self.user_id, + 'name': self.name, 'email': self.email, - 'role': self.role, + 'state': self.state.value, 'message': "the user was deleted successfully" } diff --git a/src/modules/get_user/app/get_user_viewmodel.py b/src/modules/get_user/app/get_user_viewmodel.py index c829f49..bb4be7f 100644 --- a/src/modules/get_user/app/get_user_viewmodel.py +++ b/src/modules/get_user/app/get_user_viewmodel.py @@ -1,24 +1,24 @@ -import uuid - from src.shared.domain.entities.user import User -from src.shared.domain.enums.role_enum import ROLE +from src.shared.domain.enums.state_enum import STATE class GetUserViewmodel: - user_id: uuid.UUID + user_id: int + name: str email: str - role: ROLE + state: STATE def __init__(self, user: User): self.user_id = user.user_id + self.name = user.name self.email = user.email - self.role = user.role + self.state = user.state def to_dict(self): return { 'user_id': self.user_id, - + 'name': self.name, 'email': self.email, - 'role': self.role, + 'state': self.state.value, 'message': "the user was retrieved successfully" } diff --git a/src/modules/update_user/app/update_user_viewmodel.py b/src/modules/update_user/app/update_user_viewmodel.py index 6fb4202..85bcc49 100644 --- a/src/modules/update_user/app/update_user_viewmodel.py +++ b/src/modules/update_user/app/update_user_viewmodel.py @@ -1,25 +1,25 @@ -from uuid import UUID - from src.shared.domain.entities.user import User -from src.shared.domain.enums.role_enum import ROLE +from src.shared.domain.enums.state_enum import STATE class UpdateUserViewmodel: - user_id: UUID + user_id: int name: str email: str - role: ROLE + state: STATE def __init__(self, user: User): self.user_id = user.user_id + self.name = user.name self.email = user.email - self.role = user.role + self.state = user.state def to_dict(self): return { - 'user_id': self.user_id, + 'user_id': self.user_id, + 'name': self.name, 'email': self.email, - 'role': self.role, + 'state': self.state.value, 'message': "the user was updated successfully" } diff --git a/src/shared/infra/dto/user_dynamo_dto.py b/src/shared/infra/dto/user_dynamo_dto.py index 06ed8ca..fee2c45 100644 --- a/src/shared/infra/dto/user_dynamo_dto.py +++ b/src/shared/infra/dto/user_dynamo_dto.py @@ -1,16 +1,16 @@ from decimal import Decimal from src.shared.domain.entities.user import User -from src.shared.domain.enums.role_enum import ROLE +from src.shared.domain.enums.state_enum import STATE class UserDynamoDTO: name: str email: str - role: ROLE + state: STATE user_id: int - def __init__(self, name: str, email: str, role: ROLE, user_id: int): + def __init__(self, name: str, email: str, state: STATE, user_id: int): self.name = name self.email = email self.user_id = user_id diff --git a/src/shared/infra/repositories/user_repository_mock.py b/src/shared/infra/repositories/user_repository_mock.py index f86c9a7..abc498a 100644 --- a/src/shared/infra/repositories/user_repository_mock.py +++ b/src/shared/infra/repositories/user_repository_mock.py @@ -1,7 +1,7 @@ from typing import List from src.shared.domain.entities.user import User -from src.shared.domain.enums.role_enum import ROLE +from src.shared.domain.enums.state_enum import STATE from src.shared.domain.repositories.user_repository_interface import IUserRepository from src.shared.helpers.errors.usecase_errors import NoItemsFound diff --git a/tests/modules/create_user/app/test_create_user_presenter.py b/tests/modules/create_user/app/test_create_user_presenter.py index 4485e95..fb46c51 100644 --- a/tests/modules/create_user/app/test_create_user_presenter.py +++ b/tests/modules/create_user/app/test_create_user_presenter.py @@ -52,7 +52,7 @@ def test_create_user(self): "time": "12/Mar/2020:19:03:58 +0000", "timeEpoch": 1583348638390 }, - "body": '{"email":"user@example.com", "password_hash": "hash_da_senha", "role": "admin"}', + "body": '{"name":"EhOLudjas", "email":"eho@ludjas.com"}', "pathParameters": None, "isBase64Encoded": None, "stageVariables": None diff --git a/tests/modules/create_user/app/test_create_user_usecase.py b/tests/modules/create_user/app/test_create_user_usecase.py index b78083a..ecaa9de 100644 --- a/tests/modules/create_user/app/test_create_user_usecase.py +++ b/tests/modules/create_user/app/test_create_user_usecase.py @@ -12,24 +12,23 @@ def test_create_user(self): repo = UserRepositoryMock() usecase = CreateUserUsecase(repo) - user = usecase(email="user@example.com", password_hash="hash_senha") + user = usecase(name="Vitor Choueri", email="branco@branco.branco") assert repo.users[-1] == user - def test_create_user_invalid_email(self): + def test_create_user_invalid_name(self): repo = UserRepositoryMock() usecase = CreateUserUsecase(repo) with pytest.raises(EntityError): - user = usecase(email="branco@brancobranco", password_hash="hash_senha") - - def test_create_user_invalid_password(self): + user = usecase(name="V", email="branco@branco.branco") + + def test_create_user_invalid_email(self): repo = UserRepositoryMock() - usecase = CreateUserUsecase(repo=repo) + usecase = CreateUserUsecase(repo) with pytest.raises(EntityError): - user = usecase(email="user@example.com", password_hash="h") - + user = usecase(name="Vitor Choueri", email="branco@brancobranco") """ \ No newline at end of file diff --git a/tests/modules/create_user/app/test_create_user_viewmodel.py b/tests/modules/create_user/app/test_create_user_viewmodel.py index ba903bd..2437cc2 100644 --- a/tests/modules/create_user/app/test_create_user_viewmodel.py +++ b/tests/modules/create_user/app/test_create_user_viewmodel.py @@ -1,43 +1,22 @@ -""" import uuid - from src.modules.create_user.app.create_user_viewmodel import CreateUserViewmodel from src.shared.domain.entities.user import User -from src.shared.domain.enums.role_enum import ROLE +from src.shared.domain.enums.state_enum import STATE class Test_CreateUserViewModel: - def test_create_user_viewmodel_admin(self): - user_uuid = uuid.uuid4() + def test_create_user_viewmodel(self): user = User( - user_id=user_uuid, - email="user@example.com", - password_hash="hash_senha", - role=ROLE.ADMIN + user_id=1, + name="Vitor Soller", + email="vitinho@hype.com", + state=STATE.APPROVED ) userViewmodel = CreateUserViewmodel(user=user).to_dict() - expected = {'user_id': user_uuid, - 'email': 'user@example.com', - 'role': 'admin', - 'message': 'the user was created successfully' - } + expected = {'user_id': 1, + 'name': 'Vitor Soller', + 'email': 'vitinho@hype.com', + 'state': 'APPROVED', + 'message': 'the user was created successfully'} assert expected == userViewmodel - - def test_create_user_viewmodel_user(self): - user_uuid = uuid.uuid4() - user = User( - user_id=user_uuid, - email="user@example.com", - password_hash="hash_senha", - role=ROLE.USER - ) - userViewmodel = CreateUserViewmodel(user=user).to_dict() - - expected = {'user_id': user_uuid, - 'email': 'user@example.com', - 'role': 'user', - 'message': 'the user was created successfully' - } - - assert expected == userViewmodel """ \ No newline at end of file diff --git a/tests/modules/delete_user/app/test_delete_user_viewmodel.py b/tests/modules/delete_user/app/test_delete_user_viewmodel.py index 4f9b196..e0bb5ac 100644 --- a/tests/modules/delete_user/app/test_delete_user_viewmodel.py +++ b/tests/modules/delete_user/app/test_delete_user_viewmodel.py @@ -1,6 +1,6 @@ """ from src.modules.delete_user.app.delete_user_viewmodel import DeleteUserViewmodel from src.shared.domain.entities.user import User -from src.shared.domain.enums.role_enum import ROLE +from src.shared.domain.enums.state_enum import STATE class Test_DeleteUserViewmodel: diff --git a/tests/modules/get_all_users/app/test_get_all_users_viewmodel.py b/tests/modules/get_all_users/app/test_get_all_users_viewmodel.py index 6e14a6e..f9b26ba 100644 --- a/tests/modules/get_all_users/app/test_get_all_users_viewmodel.py +++ b/tests/modules/get_all_users/app/test_get_all_users_viewmodel.py @@ -1,6 +1,6 @@ from src.modules.get_all_users.app.get_all_users_viewmodel import GetAllUsersViewmodel, UserViewmodel from src.shared.domain.entities.user import User -from src.shared.domain.enums.role_enum import ROLE +from src.shared.domain.enums.state_enum import STATE class Test_GetAllUsersViewmodel: diff --git a/tests/modules/get_user/app/test_get_user_viewmodel.py b/tests/modules/get_user/app/test_get_user_viewmodel.py index 5d408b6..848cda9 100644 --- a/tests/modules/get_user/app/test_get_user_viewmodel.py +++ b/tests/modules/get_user/app/test_get_user_viewmodel.py @@ -1,6 +1,6 @@ """ from src.modules.get_user.app.get_user_viewmodel import GetUserViewmodel from src.shared.domain.entities.user import User -from src.shared.domain.enums.role_enum import ROLE +from src.shared.domain.enums.state_enum import STATE class Test_GetUserViewModel: diff --git a/tests/modules/update_user/app/test_update_user_viewmodel.py b/tests/modules/update_user/app/test_update_user_viewmodel.py index 99f91b6..167edb0 100644 --- a/tests/modules/update_user/app/test_update_user_viewmodel.py +++ b/tests/modules/update_user/app/test_update_user_viewmodel.py @@ -1,6 +1,6 @@ """ from src.modules.update_user.app.update_user_viewmodel import UpdateUserViewmodel from src.shared.domain.entities.user import User -from src.shared.domain.enums.role_enum import ROLE +from src.shared.domain.enums.state_enum import STATE class Test_UpadateUserViewmodel: diff --git a/tests/shared/infra/dto/test_user_dynamo_dto.py b/tests/shared/infra/dto/test_user_dynamo_dto.py index e8e84d8..8fa83b0 100644 --- a/tests/shared/infra/dto/test_user_dynamo_dto.py +++ b/tests/shared/infra/dto/test_user_dynamo_dto.py @@ -1,7 +1,7 @@ """ from decimal import Decimal from src.shared.domain.entities.user import User -from src.shared.domain.enums.role_enum import ROLE +from src.shared.domain.enums.state_enum import STATE from src.shared.infra.dto.user_dynamo_dto import UserDynamoDTO from src.shared.infra.repositories.user_repository_mock import UserRepositoryMock From d25a3df63e494a3e8f208769e89e9e6310c1ee20 Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Fri, 24 Jul 2026 16:05:11 -0300 Subject: [PATCH 09/50] refactor: comenta testes de rotas --- tests/modules/create_user/app/test_create_user_viewmodel.py | 3 ++- .../modules/get_all_users/app/test_get_all_users_viewmodel.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/modules/create_user/app/test_create_user_viewmodel.py b/tests/modules/create_user/app/test_create_user_viewmodel.py index 2437cc2..7cf813e 100644 --- a/tests/modules/create_user/app/test_create_user_viewmodel.py +++ b/tests/modules/create_user/app/test_create_user_viewmodel.py @@ -1,4 +1,4 @@ -from src.modules.create_user.app.create_user_viewmodel import CreateUserViewmodel +""" from src.modules.create_user.app.create_user_viewmodel import CreateUserViewmodel from src.shared.domain.entities.user import User from src.shared.domain.enums.state_enum import STATE @@ -20,3 +20,4 @@ def test_create_user_viewmodel(self): 'message': 'the user was created successfully'} assert expected == userViewmodel + """ \ No newline at end of file diff --git a/tests/modules/get_all_users/app/test_get_all_users_viewmodel.py b/tests/modules/get_all_users/app/test_get_all_users_viewmodel.py index f9b26ba..0e16eef 100644 --- a/tests/modules/get_all_users/app/test_get_all_users_viewmodel.py +++ b/tests/modules/get_all_users/app/test_get_all_users_viewmodel.py @@ -1,4 +1,4 @@ -from src.modules.get_all_users.app.get_all_users_viewmodel import GetAllUsersViewmodel, UserViewmodel +""" from src.modules.get_all_users.app.get_all_users_viewmodel import GetAllUsersViewmodel, UserViewmodel from src.shared.domain.entities.user import User from src.shared.domain.enums.state_enum import STATE @@ -62,3 +62,4 @@ def test_user_viewmodel(self): + """ \ No newline at end of file From cfe0598cc9fb779f3be18e2cece0608fbf2ad380 Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Tue, 28 Jul 2026 18:31:42 -0300 Subject: [PATCH 10/50] refactor: atualiza UserRepositoryMock para usar UUID em vez de email --- .../repositories/user_repository_mock.py | 26 ++++---- .../repositories/test_user_repository_mock.py | 61 ++++++++++--------- 2 files changed, 43 insertions(+), 44 deletions(-) diff --git a/src/shared/infra/repositories/user_repository_mock.py b/src/shared/infra/repositories/user_repository_mock.py index abc498a..1a904e7 100644 --- a/src/shared/infra/repositories/user_repository_mock.py +++ b/src/shared/infra/repositories/user_repository_mock.py @@ -1,3 +1,4 @@ +import uuid from typing import List from src.shared.domain.entities.user import User @@ -8,44 +9,41 @@ class UserRepositoryMock(IUserRepository): users: List[User] - user_counter: int def __init__(self): self.users = [ User(email="admin@example.com", role="admin", password_hash="hash_da_senha"), User(email="user@example.com", role="user", password_hash="hash_da_senha"), ] - self.user_counter = 2 - def get_user(self, user_email: str) -> User: + + def get_user(self, user_id: uuid.UUID) -> User: for user in self.users: - if user.email == user_email: + if user.user_id == user_id: return user - raise NoItemsFound("user_email") + raise NoItemsFound("user_id") def get_all_user(self) -> List[User]: return self.users def create_user(self, new_user: User) -> User: self.users.append(new_user) - self.user_counter += 1 return new_user - def delete_user(self, user_email: str) -> User: + def delete_user(self, user_id: uuid.UUID) -> User: for idx, user in enumerate(self.users): - if user.email == user_email: - self.user_counter -= 1 + if user.user_id == user_id: return self.users.pop(idx) - raise NoItemsFound("user_email") + raise NoItemsFound("user_id") - def update_user(self, user_email: str, new_password_hash: str) -> User: + def update_user(self, user_id: uuid.UUID, new_password_hash: str) -> User: for user in self.users: - if user.email == user_email: + if user.user_id == user_id: user.password_hash = new_password_hash return user - raise NoItemsFound("user_email") + raise NoItemsFound("user_id") def get_user_counter(self) -> int: - return self.user_counter + return len(self.users) diff --git a/tests/shared/infra/repositories/test_user_repository_mock.py b/tests/shared/infra/repositories/test_user_repository_mock.py index 2b95126..73f4a6e 100644 --- a/tests/shared/infra/repositories/test_user_repository_mock.py +++ b/tests/shared/infra/repositories/test_user_repository_mock.py @@ -1,3 +1,4 @@ +import uuid from src.shared.domain.entities.user import User from src.shared.domain.enums.role_enum import ROLE from src.shared.helpers.errors.usecase_errors import NoItemsFound @@ -6,54 +7,41 @@ class Test_UserRepositoryMock: - def test_get_user(self): - repo = UserRepositoryMock() - user = repo.get_user("admin@example.com") - - assert user.email == "admin@example.com" - assert user.password_hash == "hash_da_senha" - assert user.role == "admin" - - def test_get_user_not_found(self): - repo = UserRepositoryMock() - with pytest.raises(NoItemsFound): - user = repo.get_user("nonexistent@example.com") - - def test_get_all_user(self): - repo = UserRepositoryMock() - users = repo.get_all_user() - assert len(users) == 2 def test_create_user(self): repo = UserRepositoryMock() user = User( - email="dohype@vitin.com", - password_hash="hash_da_senha", - ) - + email="dohype@vitin.com", + password_hash="hash_da_senha", + ) repo.create_user(user) - assert repo.users[-1].email == "dohype@vitin.com" assert repo.users[-1].password_hash == "hash_da_senha" assert repo.users[-1].role == "user" - assert repo.user_counter == 3 - - def test_delete_user(self): + def test_get_user(self): repo = UserRepositoryMock() - user = repo.delete_user("user@example.com") + user_id =repo.get_all_user()[1].user_id + user = repo.get_user(user_id) + assert user.email == "user@example.com" assert user.password_hash == "hash_da_senha" assert user.role == "user" - def test_delete_user_not_found(self): + def test_get_user_not_found(self): repo = UserRepositoryMock() with pytest.raises(NoItemsFound): - user = repo.delete_user("nonexistent@example.com") + user = repo.get_user(uuid.uuid4()) + + def test_get_all_user(self): + repo = UserRepositoryMock() + users = repo.get_all_user() + assert len(users) == 2 def test_update_user(self): repo = UserRepositoryMock() - user = repo.update_user("admin@example.com", "new_hash_da_senha") + user_id = repo.get_all_user()[0].user_id + user = repo.update_user(user_id, "new_hash_da_senha") assert user.password_hash == "new_hash_da_senha" assert repo.users[0].password_hash == "new_hash_da_senha" @@ -61,10 +49,23 @@ def test_update_user(self): def test_update_user_not_found(self): repo = UserRepositoryMock() with pytest.raises(NoItemsFound): - user = repo.update_user("nonexistent@example.com", "new_hash_da_senha") + user = repo.update_user(uuid.uuid4(), "new_hash_da_senha") def test_get_users_counter(self): repo = UserRepositoryMock() assert repo.get_user_counter() == 2 + def test_delete_user(self): + repo = UserRepositoryMock() + user_id = repo.get_all_user()[1].user_id + user = repo.delete_user(user_id) + assert user.email == "user@example.com" + assert user.password_hash == "hash_da_senha" + assert user.role == "user" + assert repo.get_user_counter() == 1 + + def test_delete_user_not_found(self): + repo = UserRepositoryMock() + with pytest.raises(NoItemsFound): + user = repo.delete_user(uuid.uuid4()) From 7485ee6c6ca9ba2d31f5509f14d2ef5643b77f4c Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Fri, 31 Jul 2026 12:28:13 -0300 Subject: [PATCH 11/50] =?UTF-8?q?refactor:=20atualiza=20UserRepositoryMock?= =?UTF-8?q?=20para=20permitir=20atualiza=C3=A7=C3=A3o=20de=20senha=20e=20p?= =?UTF-8?q?apel=20do=20usu=C3=A1rio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements-dev.txt | 37 +++---------------- .../repositories/user_repository_mock.py | 26 ++++++++++--- .../repositories/test_user_repository_mock.py | 27 ++++++++++++-- 3 files changed, 49 insertions(+), 41 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 2902213..c668ae5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,31 +1,6 @@ -annotated-types==0.7.0 -atomicwrites==1.4.1 -attrs==26.1.0 -aws-lambda-powertools==2.9.0 -aws-xray-sdk==2.11.0 -boto3==1.24.88 -botocore==1.27.96 -colorama==0.4.6 -coverage==7.10.7 -dnspython==2.7.0 -email-validator==2.3.0 -idna==3.18 -iniconfig==2.1.0 -jmespath==1.1.0 -packaging==26.2 -pluggy==1.6.0 -py==1.11.0 -pydantic==2.13.4 -pydantic_core==2.46.4 -Pygments==2.20.0 -pytest==8.4.2 -pytest-cov==4.0.0 -python-dateutil==2.9.0.post0 -python-dotenv==0.21.0 -s3transfer==0.6.2 -six==1.17.0 -toml==0.10.2 -typing-inspection==0.4.2 -typing_extensions==4.16.0 -urllib3==1.26.20 -wrapt==2.2.2 +pydantic==2.11.7 +email-validator==2.2.0 +urllib3>=2.0.0 +pytest==8.4.1 +pytest-cov==6.2.1 +boto3==1.43.60 diff --git a/src/shared/infra/repositories/user_repository_mock.py b/src/shared/infra/repositories/user_repository_mock.py index 1a904e7..eb5b71f 100644 --- a/src/shared/infra/repositories/user_repository_mock.py +++ b/src/shared/infra/repositories/user_repository_mock.py @@ -2,9 +2,10 @@ from typing import List from src.shared.domain.entities.user import User -from src.shared.domain.enums.state_enum import STATE +from src.shared.domain.enums.role_enum import ROLE from src.shared.domain.repositories.user_repository_interface import IUserRepository from src.shared.helpers.errors.usecase_errors import NoItemsFound +from src.shared.helpers.errors.controller_errors import MissingParameters class UserRepositoryMock(IUserRepository): @@ -37,11 +38,24 @@ def delete_user(self, user_id: uuid.UUID) -> User: raise NoItemsFound("user_id") - def update_user(self, user_id: uuid.UUID, new_password_hash: str) -> User: - for user in self.users: - if user.user_id == user_id: - user.password_hash = new_password_hash - return user + def update_user( + self, + user: User, + new_password_hash: str | None = None, + user_role: ROLE | None = None + ) -> User: + + if new_password_hash is None and user_role is None: + raise MissingParameters("At least one parameter must be provided for update.") + + for stored_user in self.users: + if stored_user.user_id == user.user_id: + if new_password_hash is not None: + stored_user.password_hash = new_password_hash + + if user_role is not None: + stored_user.role = user_role + return stored_user raise NoItemsFound("user_id") diff --git a/tests/shared/infra/repositories/test_user_repository_mock.py b/tests/shared/infra/repositories/test_user_repository_mock.py index 73f4a6e..254f3d9 100644 --- a/tests/shared/infra/repositories/test_user_repository_mock.py +++ b/tests/shared/infra/repositories/test_user_repository_mock.py @@ -2,6 +2,7 @@ from src.shared.domain.entities.user import User from src.shared.domain.enums.role_enum import ROLE from src.shared.helpers.errors.usecase_errors import NoItemsFound +from src.shared.helpers.errors.controller_errors import MissingParameters from src.shared.infra.repositories.user_repository_mock import UserRepositoryMock import pytest @@ -38,18 +39,36 @@ def test_get_all_user(self): users = repo.get_all_user() assert len(users) == 2 - def test_update_user(self): + def test_update_user_password(self): repo = UserRepositoryMock() - user_id = repo.get_all_user()[0].user_id - user = repo.update_user(user_id, "new_hash_da_senha") + user = repo.get_all_user()[0] + user = repo.update_user(user, new_password_hash="new_hash_da_senha") + assert user is not None assert user.password_hash == "new_hash_da_senha" assert repo.users[0].password_hash == "new_hash_da_senha" + def test_update_user_role(self): + repo = UserRepositoryMock() + user = repo.get_all_user()[0] + user = repo.update_user(user, user_role='admin') + + assert user is not None + assert user.role == ROLE.ADMIN + assert repo.users[0].role == ROLE.ADMIN + + def test_update_user_not_found(self): repo = UserRepositoryMock() + user = User(email="user@email.com", password_hash="password_hash" ) with pytest.raises(NoItemsFound): - user = repo.update_user(uuid.uuid4(), "new_hash_da_senha") + user = repo.update_user(user, "new_hash_da_senha") + + def test_update_missing_parameters(self): + repo = UserRepositoryMock() + user = User(email="user@email.com", password_hash="password_hash" ) + with pytest.raises(MissingParameters): + user = repo.update_user(user) def test_get_users_counter(self): repo = UserRepositoryMock() From 25014d159894158901e6d85379e8e5c8e614fc02 Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Fri, 31 Jul 2026 12:37:09 -0300 Subject: [PATCH 12/50] refactor: atualiza IUserRepository e UserRepositoryMock para usar UUID e ROLE adequadamente --- .../domain/repositories/user_repository_interface.py | 8 +++++--- .../infra/repositories/user_repository_mock.py | 12 ++++++------ .../infra/repositories/test_user_repository_mock.py | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/shared/domain/repositories/user_repository_interface.py b/src/shared/domain/repositories/user_repository_interface.py index b9c407e..82637ac 100644 --- a/src/shared/domain/repositories/user_repository_interface.py +++ b/src/shared/domain/repositories/user_repository_interface.py @@ -1,13 +1,15 @@ +import uuid from abc import ABC, abstractmethod from typing import List from src.shared.domain.entities.user import User +from src.shared.domain.enums.role_enum import ROLE class IUserRepository(ABC): @abstractmethod - def get_user(self, user_id: int) -> User: + def get_user(self, user_id: uuid.UUID) -> User: """ If user not found raise NoItemsFound """ @@ -22,14 +24,14 @@ def create_user(self, new_user: User) -> User: pass @abstractmethod - def delete_user(self, user_id: int) -> User: + def delete_user(self, user_id: uuid.UUID) -> User: """ If user not found raise NoItemsFound """ pass @abstractmethod - def update_user(self, user_id: int, new_name: str) -> User: + def update_user(self, user: User, new_password: str | None, new_user_role: ROLE | None) -> User: """ If user not found raise NoItemsFound """ diff --git a/src/shared/infra/repositories/user_repository_mock.py b/src/shared/infra/repositories/user_repository_mock.py index eb5b71f..d0230b0 100644 --- a/src/shared/infra/repositories/user_repository_mock.py +++ b/src/shared/infra/repositories/user_repository_mock.py @@ -13,8 +13,8 @@ class UserRepositoryMock(IUserRepository): def __init__(self): self.users = [ - User(email="admin@example.com", role="admin", password_hash="hash_da_senha"), - User(email="user@example.com", role="user", password_hash="hash_da_senha"), + User(email="admin@example.com", role=ROLE.ADMIN, password_hash="hash_da_senha"), + User(email="user@example.com", role=ROLE.USER, password_hash="hash_da_senha"), ] @@ -42,10 +42,10 @@ def update_user( self, user: User, new_password_hash: str | None = None, - user_role: ROLE | None = None + new_user_role: ROLE | None = None ) -> User: - if new_password_hash is None and user_role is None: + if new_password_hash is None and new_user_role is None: raise MissingParameters("At least one parameter must be provided for update.") for stored_user in self.users: @@ -53,8 +53,8 @@ def update_user( if new_password_hash is not None: stored_user.password_hash = new_password_hash - if user_role is not None: - stored_user.role = user_role + if new_user_role is not None: + stored_user.role = new_user_role return stored_user raise NoItemsFound("user_id") diff --git a/tests/shared/infra/repositories/test_user_repository_mock.py b/tests/shared/infra/repositories/test_user_repository_mock.py index 254f3d9..f256787 100644 --- a/tests/shared/infra/repositories/test_user_repository_mock.py +++ b/tests/shared/infra/repositories/test_user_repository_mock.py @@ -51,7 +51,7 @@ def test_update_user_password(self): def test_update_user_role(self): repo = UserRepositoryMock() user = repo.get_all_user()[0] - user = repo.update_user(user, user_role='admin') + user = repo.update_user(user, new_user_role=ROLE.ADMIN) assert user is not None assert user.role == ROLE.ADMIN From 4d6f23cd51da2e0bb0cb39ecf4eef96e0720911a Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Mon, 3 Aug 2026 17:33:24 -0300 Subject: [PATCH 13/50] =?UTF-8?q?refactor:=20atualiza=20m=C3=A9todo=20upda?= =?UTF-8?q?te=5Fuser=20para=20simplificar=20a=20atualiza=C3=A7=C3=A3o=20de?= =?UTF-8?q?=20usu=C3=A1rios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repositories/user_repository_mock.py | 19 ++++----------- .../test_user_repository_dynamo.py | 3 ++- .../repositories/test_user_repository_mock.py | 23 ++++++++----------- 3 files changed, 17 insertions(+), 28 deletions(-) diff --git a/src/shared/infra/repositories/user_repository_mock.py b/src/shared/infra/repositories/user_repository_mock.py index d0230b0..0917f67 100644 --- a/src/shared/infra/repositories/user_repository_mock.py +++ b/src/shared/infra/repositories/user_repository_mock.py @@ -41,21 +41,12 @@ def delete_user(self, user_id: uuid.UUID) -> User: def update_user( self, user: User, - new_password_hash: str | None = None, - new_user_role: ROLE | None = None - ) -> User: - - if new_password_hash is None and new_user_role is None: - raise MissingParameters("At least one parameter must be provided for update.") - - for stored_user in self.users: - if stored_user.user_id == user.user_id: - if new_password_hash is not None: - stored_user.password_hash = new_password_hash - if new_user_role is not None: - stored_user.role = new_user_role - return stored_user + ) -> User: + for idx, stored_user in enumerate(self.users): + if stored_user.user_id == user.user_id: + self.users[idx] = user + return self.users[idx] raise NoItemsFound("user_id") diff --git a/tests/shared/infra/repositories/test_user_repository_dynamo.py b/tests/shared/infra/repositories/test_user_repository_dynamo.py index 2c3c519..e158c18 100644 --- a/tests/shared/infra/repositories/test_user_repository_dynamo.py +++ b/tests/shared/infra/repositories/test_user_repository_dynamo.py @@ -1,4 +1,4 @@ -import os +""" import os import pytest @@ -56,3 +56,4 @@ def test_update_user(self): resp = user_repository.update_user(user_id=1, new_name="Vitor Soller Soller") assert resp.name == "Vitor Soller Soller" + """ \ No newline at end of file diff --git a/tests/shared/infra/repositories/test_user_repository_mock.py b/tests/shared/infra/repositories/test_user_repository_mock.py index f256787..d0067de 100644 --- a/tests/shared/infra/repositories/test_user_repository_mock.py +++ b/tests/shared/infra/repositories/test_user_repository_mock.py @@ -42,19 +42,22 @@ def test_get_all_user(self): def test_update_user_password(self): repo = UserRepositoryMock() user = repo.get_all_user()[0] - user = repo.update_user(user, new_password_hash="new_hash_da_senha") + user = User(user_id=user.user_id, email=user.email, password_hash="new_hash_da_senha", role=user.role) - assert user is not None - assert user.password_hash == "new_hash_da_senha" + updated_user = repo.update_user(user) + + assert updated_user is not None + assert updated_user.password_hash == "new_hash_da_senha" assert repo.users[0].password_hash == "new_hash_da_senha" def test_update_user_role(self): repo = UserRepositoryMock() user = repo.get_all_user()[0] - user = repo.update_user(user, new_user_role=ROLE.ADMIN) - - assert user is not None - assert user.role == ROLE.ADMIN + user = User(user_id=user.user_id, email=user.email, password_hash=user.password_hash, role=ROLE.ADMIN) + + updated_user = repo.update_user(user) + assert updated_user is not None + assert updated_user.role == ROLE.ADMIN assert repo.users[0].role == ROLE.ADMIN @@ -62,12 +65,6 @@ def test_update_user_not_found(self): repo = UserRepositoryMock() user = User(email="user@email.com", password_hash="password_hash" ) with pytest.raises(NoItemsFound): - user = repo.update_user(user, "new_hash_da_senha") - - def test_update_missing_parameters(self): - repo = UserRepositoryMock() - user = User(email="user@email.com", password_hash="password_hash" ) - with pytest.raises(MissingParameters): user = repo.update_user(user) def test_get_users_counter(self): From 4a35cb4c80746ac7ca9fe5a5e0c1b32fbf91f480 Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Mon, 3 Aug 2026 17:42:40 -0300 Subject: [PATCH 14/50] =?UTF-8?q?refactor:=20troca=20assinatura=20do=20met?= =?UTF-8?q?=C3=B3do=20update=20user=20para=20fins=20de=20documenta=C3=A7?= =?UTF-8?q?=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/domain/repositories/user_repository_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/domain/repositories/user_repository_interface.py b/src/shared/domain/repositories/user_repository_interface.py index 82637ac..d3d17b7 100644 --- a/src/shared/domain/repositories/user_repository_interface.py +++ b/src/shared/domain/repositories/user_repository_interface.py @@ -31,7 +31,7 @@ def delete_user(self, user_id: uuid.UUID) -> User: pass @abstractmethod - def update_user(self, user: User, new_password: str | None, new_user_role: ROLE | None) -> User: + def update_user(self, user: User) -> User: """ If user not found raise NoItemsFound """ From 6cc23da5480dcfc72cd247866f5bfc39e32f4939 Mon Sep 17 00:00:00 2001 From: Leo Iorio Date: Mon, 10 Aug 2026 18:35:05 -0300 Subject: [PATCH 15/50] refactor: alinha entidade User e mock com EntityError e requirements do template Converte ValidationError em EntityError, remove get_user_counter e limita o escopo a entity/mock/interface. --- .gitignore | 6 -- requirements-app.txt | 6 ++ requirements-dev.txt | 9 ++- .../create_user/app/create_user_usecase.py | 4 +- src/shared/domain/entities/user.py | 10 +++- src/shared/domain/enums/role_enum.py | 2 +- .../repositories/user_repository_interface.py | 8 --- .../repositories/user_repository_mock.py | 11 +--- tests/shared/domain/entities/test_user.py | 23 ++++---- .../repositories/test_user_repository_mock.py | 55 ++++++++++--------- 10 files changed, 62 insertions(+), 72 deletions(-) create mode 100644 requirements-app.txt diff --git a/.gitignore b/.gitignore index 5963e86..111f3c8 100644 --- a/.gitignore +++ b/.gitignore @@ -134,9 +134,3 @@ dmypy.json /.vscode/ /.idea/ iac/local/docker/dynamodb/shared-local-instance.db - -# Docker -.dockerignore -compose.yaml -Dockerfile -README.Docker.md diff --git a/requirements-app.txt b/requirements-app.txt new file mode 100644 index 0000000..8ddb8fa --- /dev/null +++ b/requirements-app.txt @@ -0,0 +1,6 @@ +# pacotes necessários na lambda +pydantic==2.11.7 +email-validator==2.2.0 +urllib3>=2.0.0 +# só usado por ObservabilityAWS (wrapper); testes locais usam ObservabilityMock +aws-lambda-powertools>=3.0.0 diff --git a/requirements-dev.txt b/requirements-dev.txt index c668ae5..4678fdc 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,5 @@ -pydantic==2.11.7 -email-validator==2.2.0 -urllib3>=2.0.0 -pytest==8.4.1 -pytest-cov==6.2.1 +# pacotes necessários em local (lambda não usa) +-r requirements-app.txt +pytest==8.4.1 +pytest-cov==6.2.1 boto3==1.43.60 diff --git a/src/modules/create_user/app/create_user_usecase.py b/src/modules/create_user/app/create_user_usecase.py index 11d0296..e1e9e80 100644 --- a/src/modules/create_user/app/create_user_usecase.py +++ b/src/modules/create_user/app/create_user_usecase.py @@ -1,10 +1,8 @@ -from pydantic import ValidationError from src.shared.domain.entities.user import User from src.shared.domain.enums.state_enum import STATE from src.shared.domain.repositories.user_repository_interface import IUserRepository - from src.shared.helpers.errors.domain_errors import EntityError @@ -26,4 +24,4 @@ def __call__(self, name: str, email: str) -> User: state=STATE.PENDING ) - return self.repo.create_user(user) \ No newline at end of file + return self.repo.create_user(user) diff --git a/src/shared/domain/entities/user.py b/src/shared/domain/entities/user.py index bc6eb23..9c67b7b 100644 --- a/src/shared/domain/entities/user.py +++ b/src/shared/domain/entities/user.py @@ -1,11 +1,17 @@ - import uuid -from pydantic import BaseModel, Field, EmailStr, ConfigDict +from pydantic import BaseModel, Field, EmailStr, ConfigDict, ValidationError from src.shared.domain.enums.role_enum import ROLE +from src.shared.helpers.errors.domain_errors import EntityError class User(BaseModel): + def __init__(self, **data): + try: + super().__init__(**data) + except ValidationError as err: + raise EntityError(str(err.errors()[0]["loc"][0])) from err + user_id: uuid.UUID = Field( default_factory=uuid.uuid4, description="Identificador único de usuário" diff --git a/src/shared/domain/enums/role_enum.py b/src/shared/domain/enums/role_enum.py index eb84cf1..1319f62 100644 --- a/src/shared/domain/enums/role_enum.py +++ b/src/shared/domain/enums/role_enum.py @@ -2,4 +2,4 @@ class ROLE(str, Enum): ADMIN = "admin" - USER = "user" \ No newline at end of file + USER = "user" diff --git a/src/shared/domain/repositories/user_repository_interface.py b/src/shared/domain/repositories/user_repository_interface.py index d3d17b7..78315ca 100644 --- a/src/shared/domain/repositories/user_repository_interface.py +++ b/src/shared/domain/repositories/user_repository_interface.py @@ -3,7 +3,6 @@ from typing import List from src.shared.domain.entities.user import User -from src.shared.domain.enums.role_enum import ROLE class IUserRepository(ABC): @@ -36,10 +35,3 @@ def update_user(self, user: User) -> User: If user not found raise NoItemsFound """ pass - - @abstractmethod - def get_user_counter(self) -> int: - """ - Returns the number of all users that have ever been created - """ - pass diff --git a/src/shared/infra/repositories/user_repository_mock.py b/src/shared/infra/repositories/user_repository_mock.py index 0917f67..2fe229e 100644 --- a/src/shared/infra/repositories/user_repository_mock.py +++ b/src/shared/infra/repositories/user_repository_mock.py @@ -5,7 +5,6 @@ from src.shared.domain.enums.role_enum import ROLE from src.shared.domain.repositories.user_repository_interface import IUserRepository from src.shared.helpers.errors.usecase_errors import NoItemsFound -from src.shared.helpers.errors.controller_errors import MissingParameters class UserRepositoryMock(IUserRepository): @@ -17,7 +16,6 @@ def __init__(self): User(email="user@example.com", role=ROLE.USER, password_hash="hash_da_senha"), ] - def get_user(self, user_id: uuid.UUID) -> User: for user in self.users: if user.user_id == user_id: @@ -38,17 +36,10 @@ def delete_user(self, user_id: uuid.UUID) -> User: raise NoItemsFound("user_id") - def update_user( - self, - user: User, - - ) -> User: + def update_user(self, user: User) -> User: for idx, stored_user in enumerate(self.users): if stored_user.user_id == user.user_id: self.users[idx] = user return self.users[idx] raise NoItemsFound("user_id") - - def get_user_counter(self) -> int: - return len(self.users) diff --git a/tests/shared/domain/entities/test_user.py b/tests/shared/domain/entities/test_user.py index e690efa..0bee212 100644 --- a/tests/shared/domain/entities/test_user.py +++ b/tests/shared/domain/entities/test_user.py @@ -1,26 +1,25 @@ import uuid import pytest -from pydantic import ValidationError from src.shared.domain.entities.user import User -from src.shared.domain.enums.role_enum import ROLE +from src.shared.helpers.errors.domain_errors import EntityError class Test_User: def test_user(self): user = User(email="usuario@example.com", password_hash="hash_da_senha") id_user = user.user_id - assert isinstance(id_user,uuid.UUID) + assert isinstance(id_user, uuid.UUID) assert user.email == "usuario@example.com" assert user.password_hash == "hash_da_senha" assert user.role == "user" def test_user_not_has_email(self): - with pytest.raises(ValidationError): + with pytest.raises(EntityError): User(password_hash="hash_da_senha") - + def test_user_not_has_password_hash(self): - with pytest.raises(ValidationError): + with pytest.raises(EntityError): User(email="usuario@example.com") def test_user_with_custom_id(self): @@ -33,17 +32,17 @@ def test_user_role_is_admin(self): assert user.role == "admin" def test_user_role_is_invalid(self): - with pytest.raises(ValidationError): + with pytest.raises(EntityError): User(email="usuario@example.com", password_hash="hash_da_senha", role="invalid_role") def test_user_email_is_none(self): - with pytest.raises(ValidationError): + with pytest.raises(EntityError): User(email=None, password_hash="hash_da_senha") def test_user_email_not_has_at_symbol(self): - with pytest.raises(ValidationError): + with pytest.raises(EntityError): User(email="usuarioexample.com", password_hash="hash_da_senha") - + def test_user_email_not_has_domain(self): - with pytest.raises(ValidationError): - User(email="usuario@", password_hash="hash_da_senha") \ No newline at end of file + with pytest.raises(EntityError): + User(email="usuario@", password_hash="hash_da_senha") diff --git a/tests/shared/infra/repositories/test_user_repository_mock.py b/tests/shared/infra/repositories/test_user_repository_mock.py index d0067de..ec97e0d 100644 --- a/tests/shared/infra/repositories/test_user_repository_mock.py +++ b/tests/shared/infra/repositories/test_user_repository_mock.py @@ -1,10 +1,10 @@ import uuid +import pytest + from src.shared.domain.entities.user import User from src.shared.domain.enums.role_enum import ROLE from src.shared.helpers.errors.usecase_errors import NoItemsFound -from src.shared.helpers.errors.controller_errors import MissingParameters from src.shared.infra.repositories.user_repository_mock import UserRepositoryMock -import pytest class Test_UserRepositoryMock: @@ -12,17 +12,18 @@ class Test_UserRepositoryMock: def test_create_user(self): repo = UserRepositoryMock() user = User( - email="dohype@vitin.com", - password_hash="hash_da_senha", - ) + email="dohype@vitin.com", + password_hash="hash_da_senha", + ) repo.create_user(user) assert repo.users[-1].email == "dohype@vitin.com" assert repo.users[-1].password_hash == "hash_da_senha" assert repo.users[-1].role == "user" + assert len(repo.users) == 3 def test_get_user(self): repo = UserRepositoryMock() - user_id =repo.get_all_user()[1].user_id + user_id = repo.get_all_user()[1].user_id user = repo.get_user(user_id) assert user.email == "user@example.com" @@ -32,7 +33,7 @@ def test_get_user(self): def test_get_user_not_found(self): repo = UserRepositoryMock() with pytest.raises(NoItemsFound): - user = repo.get_user(uuid.uuid4()) + repo.get_user(uuid.uuid4()) def test_get_all_user(self): repo = UserRepositoryMock() @@ -42,7 +43,12 @@ def test_get_all_user(self): def test_update_user_password(self): repo = UserRepositoryMock() user = repo.get_all_user()[0] - user = User(user_id=user.user_id, email=user.email, password_hash="new_hash_da_senha", role=user.role) + user = User( + user_id=user.user_id, + email=user.email, + password_hash="new_hash_da_senha", + role=user.role, + ) updated_user = repo.update_user(user) @@ -51,26 +57,25 @@ def test_update_user_password(self): assert repo.users[0].password_hash == "new_hash_da_senha" def test_update_user_role(self): - repo = UserRepositoryMock() - user = repo.get_all_user()[0] - user = User(user_id=user.user_id, email=user.email, password_hash=user.password_hash, role=ROLE.ADMIN) + repo = UserRepositoryMock() + user = repo.get_all_user()[0] + user = User( + user_id=user.user_id, + email=user.email, + password_hash=user.password_hash, + role=ROLE.ADMIN, + ) - updated_user = repo.update_user(user) - assert updated_user is not None - assert updated_user.role == ROLE.ADMIN - assert repo.users[0].role == ROLE.ADMIN + updated_user = repo.update_user(user) + assert updated_user is not None + assert updated_user.role == "admin" + assert repo.users[0].role == "admin" - def test_update_user_not_found(self): repo = UserRepositoryMock() - user = User(email="user@email.com", password_hash="password_hash" ) + user = User(email="user@email.com", password_hash="password_hash") with pytest.raises(NoItemsFound): - user = repo.update_user(user) - - def test_get_users_counter(self): - repo = UserRepositoryMock() - - assert repo.get_user_counter() == 2 + repo.update_user(user) def test_delete_user(self): repo = UserRepositoryMock() @@ -79,9 +84,9 @@ def test_delete_user(self): assert user.email == "user@example.com" assert user.password_hash == "hash_da_senha" assert user.role == "user" - assert repo.get_user_counter() == 1 + assert len(repo.users) == 1 def test_delete_user_not_found(self): repo = UserRepositoryMock() with pytest.raises(NoItemsFound): - user = repo.delete_user(uuid.uuid4()) + repo.delete_user(uuid.uuid4()) From 3a16239c2f6e466cdd4ec78e6100d0ab96a23ee0 Mon Sep 17 00:00:00 2001 From: Leo Iorio Date: Mon, 10 Aug 2026 18:42:32 -0300 Subject: [PATCH 16/50] fix: updated ci to python 311, meeting requirements new packages --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 4d12145..615e003 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -7,4 +7,4 @@ on: jobs: testApp: - uses: maua-dev/ci_workflows_reusable/.github/workflows/pytest_ci.yml@V1.0 + uses: maua-dev/ci_workflows_reusable/.github/workflows/pytest_ci_311.yml@V1.2 From f6427399c9426789ade90e5cdbc2a91d46966777 Mon Sep 17 00:00:00 2001 From: Leo Iorio Date: Mon, 10 Aug 2026 18:53:18 -0300 Subject: [PATCH 17/50] chore: updated ci to 1.3 --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 615e003..0a63b60 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -7,4 +7,4 @@ on: jobs: testApp: - uses: maua-dev/ci_workflows_reusable/.github/workflows/pytest_ci_311.yml@V1.2 + uses: maua-dev/ci_workflows_reusable/.github/workflows/pytest_ci_313.yml@V1.3 From 3ae39f1c9213ab62ec5cba1dd627b0f33a52a19b Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Wed, 12 Aug 2026 18:55:31 -0300 Subject: [PATCH 18/50] refactor: remove password_hash da entidade user, refatora testes --- src/shared/domain/entities/user.py | 6 ----- .../repositories/user_repository_mock.py | 4 ++-- tests/shared/domain/entities/test_user.py | 21 +++++++----------- .../repositories/test_user_repository_mock.py | 22 +------------------ 4 files changed, 11 insertions(+), 42 deletions(-) diff --git a/src/shared/domain/entities/user.py b/src/shared/domain/entities/user.py index 9c67b7b..b12d29e 100644 --- a/src/shared/domain/entities/user.py +++ b/src/shared/domain/entities/user.py @@ -29,12 +29,6 @@ def __init__(self, **data): description="Função do usuário", ) - password_hash: str = Field( - ..., - description="Hash da senha do usuário", - min_length=4 - ) - model_config = ConfigDict( use_enum_values=True, extra="forbid", diff --git a/src/shared/infra/repositories/user_repository_mock.py b/src/shared/infra/repositories/user_repository_mock.py index 2fe229e..b113c95 100644 --- a/src/shared/infra/repositories/user_repository_mock.py +++ b/src/shared/infra/repositories/user_repository_mock.py @@ -12,8 +12,8 @@ class UserRepositoryMock(IUserRepository): def __init__(self): self.users = [ - User(email="admin@example.com", role=ROLE.ADMIN, password_hash="hash_da_senha"), - User(email="user@example.com", role=ROLE.USER, password_hash="hash_da_senha"), + User(email="admin@example.com", role=ROLE.ADMIN), + User(email="user@example.com", role=ROLE.USER), ] def get_user(self, user_id: uuid.UUID) -> User: diff --git a/tests/shared/domain/entities/test_user.py b/tests/shared/domain/entities/test_user.py index 0bee212..85bbef6 100644 --- a/tests/shared/domain/entities/test_user.py +++ b/tests/shared/domain/entities/test_user.py @@ -7,42 +7,37 @@ class Test_User: def test_user(self): - user = User(email="usuario@example.com", password_hash="hash_da_senha") + user = User(email="usuario@example.com", ) id_user = user.user_id assert isinstance(id_user, uuid.UUID) assert user.email == "usuario@example.com" - assert user.password_hash == "hash_da_senha" assert user.role == "user" def test_user_not_has_email(self): with pytest.raises(EntityError): - User(password_hash="hash_da_senha") - - def test_user_not_has_password_hash(self): - with pytest.raises(EntityError): - User(email="usuario@example.com") + User(role="admin") def test_user_with_custom_id(self): user_id = uuid.uuid4() - user = User(user_id=user_id, email="usuario@example.com", password_hash="hash_da_senha") + user = User(user_id=user_id, email="usuario@example.com", ) assert user.user_id == user_id def test_user_role_is_admin(self): - user = User(email="admin@example.com", password_hash="hash_da_senha", role="admin") + user = User(email="admin@example.com", role="admin") assert user.role == "admin" def test_user_role_is_invalid(self): with pytest.raises(EntityError): - User(email="usuario@example.com", password_hash="hash_da_senha", role="invalid_role") + User(email="usuario@example.com", role="invalid_role") def test_user_email_is_none(self): with pytest.raises(EntityError): - User(email=None, password_hash="hash_da_senha") + User(email=None) def test_user_email_not_has_at_symbol(self): with pytest.raises(EntityError): - User(email="usuarioexample.com", password_hash="hash_da_senha") + User(email="usuarioexample.com") def test_user_email_not_has_domain(self): with pytest.raises(EntityError): - User(email="usuario@", password_hash="hash_da_senha") + User(email="usuario@") diff --git a/tests/shared/infra/repositories/test_user_repository_mock.py b/tests/shared/infra/repositories/test_user_repository_mock.py index ec97e0d..4f14420 100644 --- a/tests/shared/infra/repositories/test_user_repository_mock.py +++ b/tests/shared/infra/repositories/test_user_repository_mock.py @@ -13,11 +13,9 @@ def test_create_user(self): repo = UserRepositoryMock() user = User( email="dohype@vitin.com", - password_hash="hash_da_senha", ) repo.create_user(user) assert repo.users[-1].email == "dohype@vitin.com" - assert repo.users[-1].password_hash == "hash_da_senha" assert repo.users[-1].role == "user" assert len(repo.users) == 3 @@ -27,7 +25,6 @@ def test_get_user(self): user = repo.get_user(user_id) assert user.email == "user@example.com" - assert user.password_hash == "hash_da_senha" assert user.role == "user" def test_get_user_not_found(self): @@ -40,21 +37,6 @@ def test_get_all_user(self): users = repo.get_all_user() assert len(users) == 2 - def test_update_user_password(self): - repo = UserRepositoryMock() - user = repo.get_all_user()[0] - user = User( - user_id=user.user_id, - email=user.email, - password_hash="new_hash_da_senha", - role=user.role, - ) - - updated_user = repo.update_user(user) - - assert updated_user is not None - assert updated_user.password_hash == "new_hash_da_senha" - assert repo.users[0].password_hash == "new_hash_da_senha" def test_update_user_role(self): repo = UserRepositoryMock() @@ -62,7 +44,6 @@ def test_update_user_role(self): user = User( user_id=user.user_id, email=user.email, - password_hash=user.password_hash, role=ROLE.ADMIN, ) @@ -73,7 +54,7 @@ def test_update_user_role(self): def test_update_user_not_found(self): repo = UserRepositoryMock() - user = User(email="user@email.com", password_hash="password_hash") + user = User(email="user@email.com") with pytest.raises(NoItemsFound): repo.update_user(user) @@ -82,7 +63,6 @@ def test_delete_user(self): user_id = repo.get_all_user()[1].user_id user = repo.delete_user(user_id) assert user.email == "user@example.com" - assert user.password_hash == "hash_da_senha" assert user.role == "user" assert len(repo.users) == 1 From 8847c811f1289ab5786a9c6d83a5b24817a7fba1 Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Fri, 14 Aug 2026 10:50:14 -0300 Subject: [PATCH 19/50] =?UTF-8?q?feat:=20ad=C3=A9qua=20rota=20create=5Fuse?= =?UTF-8?q?r=20a=20nova=20estrutura=20de=20user=20e=20refatora=20os=20test?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../create_user/app/create_user_controller.py | 5 +- .../create_user/app/create_user_usecase.py | 15 ++---- .../create_user/app/create_user_viewmodel.py | 23 ++++----- .../app/test_create_user_controller.py | 47 ++----------------- .../app/test_create_user_presenter.py | 5 +- .../app/test_create_user_usecase.py | 15 +----- .../app/test_create_user_viewmodel.py | 19 ++++---- 7 files changed, 30 insertions(+), 99 deletions(-) diff --git a/src/modules/create_user/app/create_user_controller.py b/src/modules/create_user/app/create_user_controller.py index 29e3a59..c0d65ea 100644 --- a/src/modules/create_user/app/create_user_controller.py +++ b/src/modules/create_user/app/create_user_controller.py @@ -14,13 +14,10 @@ def __init__(self, usecase: CreateUserUsecase): def __call__(self, request: IRequest) -> IResponse: try: - if request.data.get('name') is None: - raise MissingParameters('name') if request.data.get('email') is None: raise MissingParameters('email') - + user = self.CreateUserUsecase( - name=request.data.get('name'), email=request.data.get('email') ) diff --git a/src/modules/create_user/app/create_user_usecase.py b/src/modules/create_user/app/create_user_usecase.py index e1e9e80..f1f2d17 100644 --- a/src/modules/create_user/app/create_user_usecase.py +++ b/src/modules/create_user/app/create_user_usecase.py @@ -1,7 +1,7 @@ - +from pydantic import EmailStr from src.shared.domain.entities.user import User -from src.shared.domain.enums.state_enum import STATE +from src.shared.domain.enums.role_enum import ROLE from src.shared.domain.repositories.user_repository_interface import IUserRepository from src.shared.helpers.errors.domain_errors import EntityError @@ -10,18 +10,11 @@ class CreateUserUsecase: def __init__(self, repo: IUserRepository): self.repo = repo - def __call__(self, name: str, email: str) -> User: - - if not User.validate_name(name): - raise EntityError("name") - - if not User.validate_email(email): - raise EntityError("email") + def __call__(self, email: str) -> User: user = User( - name=name, email=email, - state=STATE.PENDING + role=ROLE.USER ) return self.repo.create_user(user) diff --git a/src/modules/create_user/app/create_user_viewmodel.py b/src/modules/create_user/app/create_user_viewmodel.py index fd71ebc..d374350 100644 --- a/src/modules/create_user/app/create_user_viewmodel.py +++ b/src/modules/create_user/app/create_user_viewmodel.py @@ -1,24 +1,17 @@ +from uuid import UUID from src.shared.domain.entities.user import User -from src.shared.domain.enums.state_enum import STATE +from src.shared.domain.enums.role_enum import ROLE class CreateUserViewmodel: - user_id: int - name: str + user_id: UUID email: str - state: STATE + role: ROLE def __init__(self, user: User): - self.user_id = user.user_id - self.name = user.name - self.email = user.email - self.state = user.state + self.user = user def to_dict(self): - return { - 'user_id': self.user_id, - 'name': self.name, - 'email': self.email, - 'state': self.state.value, - 'message': "the user was created successfully" - } + data = self.user.model_dump(mode='json') + data['message'] = "the user was created successfully" + return data diff --git a/tests/modules/create_user/app/test_create_user_controller.py b/tests/modules/create_user/app/test_create_user_controller.py index 6ca8163..873b7e0 100644 --- a/tests/modules/create_user/app/test_create_user_controller.py +++ b/tests/modules/create_user/app/test_create_user_controller.py @@ -1,4 +1,4 @@ -""" from src.modules.create_user.app.create_user_controller import CreateUserController +from src.modules.create_user.app.create_user_controller import CreateUserController from src.modules.create_user.app.create_user_usecase import CreateUserUsecase from src.shared.helpers.external_interfaces.http_models import HttpRequest from src.shared.infra.repositories.user_repository_mock import UserRepositoryMock @@ -11,40 +11,21 @@ def test_create_user_controller(self): controller = CreateUserController(usecase=usecase) request = HttpRequest(body={ - 'name': 'Branco do Branco Branco da Silva', 'email': 'branco@branco.com' }) response = controller(request=request) assert response.status_code == 201 - assert response.body['user_id'] == repo.users[-1].user_id - assert response.body['name'] == repo.users[-1].name assert response.body['email'] == repo.users[-1].email - assert response.body['state'] == repo.users[-1].state.value assert response.body['message'] == "the user was created successfully" - def test_create_user_controller_missing_name(self): - repo = UserRepositoryMock() - usecase = CreateUserUsecase(repo=repo) - controller = CreateUserController(usecase=usecase) - - request = HttpRequest(body={ - 'email': '21.01444-2@maua.br'}) - - response = controller(request=request) - - assert response.status_code == 400 - assert response.body == "Field name is missing" - - def test_create_user_controller_missing_email(self): repo = UserRepositoryMock() usecase = CreateUserUsecase(repo=repo) controller = CreateUserController(usecase=usecase) - request = HttpRequest(body={ - 'name': 'Branco do Branco Branco da Silva'}) + request = HttpRequest(body={}) response = controller(request=request) @@ -57,30 +38,10 @@ def test_create_user_controller_invalid_email(self): controller = CreateUserController(usecase=usecase) request = HttpRequest(body={ - 'name': 'Branco do Branco Branco da Silva', - 'email': 'branco@branco'}) + 'email': 'branco@branco' + }) response = controller(request=request) assert response.status_code == 400 assert response.body == "Field email is not valid" - - def test_create_user_controller_invalid_name(self): - repo = UserRepositoryMock() - usecase = CreateUserUsecase(repo=repo) - controller = CreateUserController(usecase=usecase) - - request = HttpRequest(body={ - 'name': 'B', - 'email': 'branco@branco.com'}) - - response = controller(request=request) - - assert response.status_code == 400 - assert response.body == "Field name is not valid" - - - - - - """ \ No newline at end of file diff --git a/tests/modules/create_user/app/test_create_user_presenter.py b/tests/modules/create_user/app/test_create_user_presenter.py index fb46c51..bd38fb7 100644 --- a/tests/modules/create_user/app/test_create_user_presenter.py +++ b/tests/modules/create_user/app/test_create_user_presenter.py @@ -1,4 +1,4 @@ -""" import json +import json from src.modules.create_user.app.create_user_presenter import lambda_handler @@ -52,7 +52,7 @@ def test_create_user(self): "time": "12/Mar/2020:19:03:58 +0000", "timeEpoch": 1583348638390 }, - "body": '{"name":"EhOLudjas", "email":"eho@ludjas.com"}', + "body": '{"email":"eho@ludjas.com"}', "pathParameters": None, "isBase64Encoded": None, "stageVariables": None @@ -119,4 +119,3 @@ def test_create_user(self): assert response["statusCode"] == 201 assert json.loads(response["body"])["message"] == "the user was created successfully" - """ \ No newline at end of file diff --git a/tests/modules/create_user/app/test_create_user_usecase.py b/tests/modules/create_user/app/test_create_user_usecase.py index ecaa9de..007e8fc 100644 --- a/tests/modules/create_user/app/test_create_user_usecase.py +++ b/tests/modules/create_user/app/test_create_user_usecase.py @@ -1,4 +1,3 @@ -""" import pytest from src.modules.create_user.app.create_user_usecase import CreateUserUsecase @@ -12,23 +11,13 @@ def test_create_user(self): repo = UserRepositoryMock() usecase = CreateUserUsecase(repo) - user = usecase(name="Vitor Choueri", email="branco@branco.branco") + user = usecase(email="branco@branco.branco") assert repo.users[-1] == user - def test_create_user_invalid_name(self): - repo = UserRepositoryMock() - usecase = CreateUserUsecase(repo) - - with pytest.raises(EntityError): - user = usecase(name="V", email="branco@branco.branco") - def test_create_user_invalid_email(self): repo = UserRepositoryMock() usecase = CreateUserUsecase(repo) with pytest.raises(EntityError): - user = usecase(name="Vitor Choueri", email="branco@brancobranco") - - -""" \ No newline at end of file + user = usecase( email="branco@brancobranco") \ No newline at end of file diff --git a/tests/modules/create_user/app/test_create_user_viewmodel.py b/tests/modules/create_user/app/test_create_user_viewmodel.py index 7cf813e..37905bd 100644 --- a/tests/modules/create_user/app/test_create_user_viewmodel.py +++ b/tests/modules/create_user/app/test_create_user_viewmodel.py @@ -1,23 +1,22 @@ -""" from src.modules.create_user.app.create_user_viewmodel import CreateUserViewmodel +from uuid import uuid4 +from src.modules.create_user.app.create_user_viewmodel import CreateUserViewmodel from src.shared.domain.entities.user import User -from src.shared.domain.enums.state_enum import STATE +from src.shared.domain.enums.role_enum import ROLE class Test_CreateUserViewModel: def test_create_user_viewmodel(self): + user_id = uuid4() user = User( - user_id=1, - name="Vitor Soller", + user_id=user_id, email="vitinho@hype.com", - state=STATE.APPROVED + role=ROLE.ADMIN ) userViewmodel = CreateUserViewmodel(user=user).to_dict() - expected = {'user_id': 1, - 'name': 'Vitor Soller', + expected = {'user_id': str(user_id), 'email': 'vitinho@hype.com', - 'state': 'APPROVED', + 'role': 'admin', 'message': 'the user was created successfully'} - assert expected == userViewmodel - """ \ No newline at end of file + assert expected == userViewmodel \ No newline at end of file From 5856175c17154d371f946de0e24153ecae1dd7f6 Mon Sep 17 00:00:00 2001 From: Leo Iorio Date: Sat, 15 Aug 2026 10:14:56 -0300 Subject: [PATCH 20/50] feat: alinha helpers, observability, HTTP e stack local com o template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Traz errors, códigos HTTP, auth, wrap_handler, requirements-infra e Docker local (Dynamo + MinIO) do clean_mss_template, sem a infra Dynamo. --- .gitignore | 2 + iac/README.md | 4 +- iac/local/README.md | 45 ++++++- iac/local/configure_dynamodb.md | 40 +------ iac/local/docker-compose.yml | 45 ++++++- iac/requirements-dev.txt | 1 - iac/requirements-infra.txt | 1 + iac/requirements.txt | 5 - .../create_user/app/create_user_presenter.py | 22 ++-- .../delete_user/app/delete_user_presenter.py | 15 ++- .../app/get_all_users_presenter.py | 15 ++- .../get_user/app/get_user_presenter.py | 27 ++--- .../update_user/app/update_user_presenter.py | 15 ++- .../observability/observability_interface.py | 39 +++--- src/shared/helpers/auth/authorizer_user.py | 72 ++++++++++++ src/shared/helpers/auth/iam_policy.py | 39 ++++++ .../helpers/enum/http_status_code_enum.py | 12 +- .../helpers/errors/controller_errors.py | 6 +- src/shared/helpers/errors/usecase_errors.py | 11 +- .../helpers/external_interfaces/http_codes.py | 111 ++++++++++++++++-- .../http_lambda_requests.py | 60 ++++++---- .../helpers/observability/wrap_handler.py | 51 ++++++++ tests/shared/helpers/auth/test_iam_policy.py | 21 ++++ .../test_lambda_http_request_authorizer.py | 56 +++++++++ 24 files changed, 565 insertions(+), 150 deletions(-) delete mode 100644 iac/requirements-dev.txt create mode 100644 iac/requirements-infra.txt delete mode 100644 iac/requirements.txt create mode 100644 src/shared/helpers/auth/authorizer_user.py create mode 100644 src/shared/helpers/auth/iam_policy.py create mode 100644 src/shared/helpers/observability/wrap_handler.py create mode 100644 tests/shared/helpers/auth/test_iam_policy.py create mode 100644 tests/shared/helpers/external_interfaces/test_lambda_http_request_authorizer.py diff --git a/.gitignore b/.gitignore index 111f3c8..454d09d 100644 --- a/.gitignore +++ b/.gitignore @@ -133,4 +133,6 @@ dmypy.json /.vscode/ /.idea/ +.DS_Store iac/local/docker/dynamodb/shared-local-instance.db +iac/local/docker/minio/ diff --git a/iac/README.md b/iac/README.md index e3a001c..7387c8b 100644 --- a/iac/README.md +++ b/iac/README.md @@ -34,7 +34,7 @@ If you are a Windows platform, you would activate the virtualenv like this: Once the virtualenv is activated, you can install the required dependencies. ``` -$ pip install -r requirements.txt +$ pip install -r requirements-infra.txt ``` At this point you can now synthesize the CloudFormation template for this code. @@ -44,7 +44,7 @@ $ cdk synth ``` To add additional dependencies, for example other CDK libraries, just add -them to your `setup.py` file and rerun the `pip install -r requirements.txt` +them to your `setup.py` file and rerun the `pip install -r requirements-infra.txt` command. ## Useful commands diff --git a/iac/local/README.md b/iac/local/README.md index facf220..c887e0d 100644 --- a/iac/local/README.md +++ b/iac/local/README.md @@ -4,13 +4,50 @@ - have docker installed and running - have [sam](https://docs.aws.amazon.com/serverless-application-model/index.html) installed -## Setup dynamodb local -- `docker run -p 8000:8000 amazon/dynamodb-local -sharedDB -inMemory` -- run `src/shared/infra/repositories/load_user_mock_to_dynamo.py` +## Start local services (DynamoDB + MinIO) +From `iac/local`: + +```bash +docker compose up -d +``` + +| Service | URL / port | Notes | +|---------|------------|--------| +| DynamoDB Local | `http://localhost:8000` | persistence in `./docker/dynamodb` | +| MinIO S3 API | `http://localhost:9000` | credentials `minioadmin` / `minioadmin` | +| MinIO Console | http://localhost:9001 | web UI | + +On first boot, `minio-init` creates the bucket `bucket-test` (same name as `Environments.s3_bucket_name` in `STAGE=TEST`). + +### Seed DynamoDB +From the repo root, with `STAGE=TEST` in `.env`: + +```bash +python -m src.shared.infra.repositories.load_user_mock_to_dynamo +``` + +### Integration tests (DynamoDB Local) +Os testes `test_*_repository_dynamo.py` estão comentados — **não rodam no CI**. + +Para rodar localmente: suba o Docker, rode o seed, reative o teste e execute o pytest normalmente. Sem Dynamo up, o teste falha. + +### Point boto3 / code at MinIO (example) + +```python +import boto3 + +s3 = boto3.client( + "s3", + endpoint_url="http://localhost:9000", + aws_access_key_id="minioadmin", + aws_secret_access_key="minioadmin", + region_name="us-east-1", +) +s3.list_objects_v2(Bucket="bucket-test") +``` ## Build cdk (/iac) - `cdk synth` - `sam build -t ./cdk.out/IacStack.template.json` - `sam local start-api -t ./cdk.out/IacStack.template.json` -- diff --git a/iac/local/configure_dynamodb.md b/iac/local/configure_dynamodb.md index 0e80d1d..5ace3d4 100644 --- a/iac/local/configure_dynamodb.md +++ b/iac/local/configure_dynamodb.md @@ -38,7 +38,7 @@ Next, you need to configure DynamoDB for your project and Docker compose. Follow ``` cd iac cd local - docker-compose up -d + docker compose up -d ``` ### Create virtual enviroments in python (once for project) @@ -49,7 +49,7 @@ Next, you need to configure DynamoDB for your project and Docker compose. Follow ###### Linux - virtualenv -p python3.9 venv + virtualenv -p python3.11 venv ### Activate the venv @@ -81,41 +81,13 @@ Start the DynamoDB Local container using Docker: ## Running the `load_user_mock_to_dynamo` Script -Finally, you can run the `load_user_mock_to_dynamo` script to load mock data into DynamoDB. Follow the steps below: +Seed mock `User` data into DynamoDB Local. From the repo root, with `STAGE=TEST`: -1. Locate the directory or file named `load_user_mock_to_dynamo` within your project. This directory or file is responsible for loading mock data into DynamoDB -2. If the `load_user_mock_to_dynamo` file doesn't exist, you need to create it. -3. Once you have located or created the `load_user_mock_to_dynamo` file, make sure it is in the correct location within your project structure. The file should be located in the `src/shared/infra/repositories` ```bash -. -├── iac -├── src -│ ├── ... -│ │ -│ │ -│ └── shared -│ ├── domain -│ │ └── ... -│ │ -│ ├── helpers -│ │ └── ... -│ │ -│ └── infra -│ ├── dto -│ ├── external -│ └── repositories -│ └── -> [load_user_mock_to_dynamo] <- -... +python -m src.shared.infra.repositories.load_user_mock_to_dynamo ``` -4. This file is responsible for populating DynamoDB with mock data -5. Then, make sure you are in the root directory of your project again -6. Run the following command to execute the script: - ``` - py -m src.shared.infra.repositories.load_user_mock_to_dynamo - ``` - -This command will run the `load_user_mock_to_dynamo` script and populate DynamoDB with the provided mock data +The script lives at `src/shared/infra/repositories/load_user_mock_to_dynamo.py`. It creates the local table if missing, then loads users from `UserRepositoryMock`. ## Launch NoSQL WorkBench @@ -124,4 +96,4 @@ Lastly, you need to set NoSQL WorkBench to receive and visualize data runned. Fo 1. Launch DynamoDB 2. Open Operation builder and add a connection 3. Select DynamoDB Local -4. Open Local \ No newline at end of file +4. Open Local diff --git a/iac/local/docker-compose.yml b/iac/local/docker-compose.yml index 28b9de0..c5db5f6 100644 --- a/iac/local/docker-compose.yml +++ b/iac/local/docker-compose.yml @@ -1,4 +1,3 @@ -version: '3.8' services: dynamodb-local: command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data" @@ -10,8 +9,46 @@ services: - "./docker/dynamodb:/home/dynamodblocal/data" working_dir: /home/dynamodblocal networks: - - dynamodb-local + - local + + minio: + image: minio/minio:latest + container_name: minio + command: server /data --console-address ":9001" + ports: + - "9000:9000" # S3 API + - "9001:9001" # Console web + environment: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin + volumes: + - "./docker/minio:/data" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] + interval: 5s + timeout: 5s + retries: 10 + start_period: 5s + networks: + - local + + # Cria o bucket alinhado a Environments.s3_bucket_name (STAGE=TEST) + minio-init: + image: minio/mc:latest + container_name: minio-init + depends_on: + minio: + condition: service_healthy + entrypoint: > + /bin/sh -c " + mc alias set local http://minio:9000 minioadmin minioadmin && + mc mb --ignore-existing local/bucket-test && + mc anonymous set download local/bucket-test && + echo 'MinIO ready: bucket bucket-test' + " + networks: + - local networks: - dynamodb-local: - name: dynamodb-local + local: + name: local diff --git a/iac/requirements-dev.txt b/iac/requirements-dev.txt deleted file mode 100644 index 9270945..0000000 --- a/iac/requirements-dev.txt +++ /dev/null @@ -1 +0,0 @@ -pytest==6.2.5 diff --git a/iac/requirements-infra.txt b/iac/requirements-infra.txt new file mode 100644 index 0000000..eb15e80 --- /dev/null +++ b/iac/requirements-infra.txt @@ -0,0 +1 @@ +aws-cdk-lib==2.211.0 diff --git a/iac/requirements.txt b/iac/requirements.txt deleted file mode 100644 index 82ff4b9..0000000 --- a/iac/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -aws-cdk-lib==2.25.0 -constructs>=10.0.0,<11.0.0 -aws-lambda-powertools==2.9.0 -aws-xray-sdk==2.11.0 -pytest==6.2.5 \ No newline at end of file diff --git a/src/modules/create_user/app/create_user_presenter.py b/src/modules/create_user/app/create_user_presenter.py index 042f5de..c279e83 100644 --- a/src/modules/create_user/app/create_user_presenter.py +++ b/src/modules/create_user/app/create_user_presenter.py @@ -2,20 +2,20 @@ from .create_user_usecase import CreateUserUsecase from src.shared.environments import Environments from src.shared.helpers.external_interfaces.http_lambda_requests import LambdaHttpRequest, LambdaHttpResponse +from src.shared.helpers.observability.wrap_handler import observed_handler repo = Environments.get_user_repo()() usecase = CreateUserUsecase(repo) controller = CreateUserController(usecase) -def lambda_handler(event, context): - - from pprint import pprint - - pprint(event) - - httpRequest = LambdaHttpRequest(data=event) - response = controller(httpRequest) - httpResponse = LambdaHttpResponse(status_code=response.status_code, body=response.body, headers=response.headers) - - return httpResponse.toDict() +@observed_handler("create_user") +def lambda_handler(event, context): + http_request = LambdaHttpRequest(data=event) + response = controller(http_request) + http_response = LambdaHttpResponse( + status_code=response.status_code, + body=response.body, + headers=response.headers + ) + return http_response.toDict() diff --git a/src/modules/delete_user/app/delete_user_presenter.py b/src/modules/delete_user/app/delete_user_presenter.py index edd46de..1ef7db5 100644 --- a/src/modules/delete_user/app/delete_user_presenter.py +++ b/src/modules/delete_user/app/delete_user_presenter.py @@ -2,15 +2,20 @@ from .delete_user_usecase import DeleteUserUsecase from src.shared.environments import Environments from src.shared.helpers.external_interfaces.http_lambda_requests import LambdaHttpRequest, LambdaHttpResponse +from src.shared.helpers.observability.wrap_handler import observed_handler repo = Environments.get_user_repo()() usecase = DeleteUserUsecase(repo) controller = DeleteUserController(usecase) +@observed_handler("delete_user") def lambda_handler(event, context): - httpRequest = LambdaHttpRequest(data=event) - response = controller(httpRequest) - httpResponse = LambdaHttpResponse(status_code=response.status_code, body=response.body, headers=response.headers) - - return httpResponse.toDict() + http_request = LambdaHttpRequest(data=event) + response = controller(http_request) + http_response = LambdaHttpResponse( + status_code=response.status_code, + body=response.body, + headers=response.headers + ) + return http_response.toDict() diff --git a/src/modules/get_all_users/app/get_all_users_presenter.py b/src/modules/get_all_users/app/get_all_users_presenter.py index 114735b..7154de8 100644 --- a/src/modules/get_all_users/app/get_all_users_presenter.py +++ b/src/modules/get_all_users/app/get_all_users_presenter.py @@ -3,15 +3,20 @@ from src.shared.domain.repositories.user_repository_interface import IUserRepository from src.shared.environments import Environments from src.shared.helpers.external_interfaces.http_lambda_requests import LambdaHttpRequest, LambdaHttpResponse +from src.shared.helpers.observability.wrap_handler import observed_handler repo: IUserRepository = Environments.get_user_repo()() usecase = GetAllUsersUsecase(repo) controller = GetAllUsersController(usecase) +@observed_handler("get_all_users") def lambda_handler(event, context): - httpRequest = LambdaHttpRequest(data=event) - response = controller(httpRequest) - httpResponse = LambdaHttpResponse(status_code=response.status_code, body=response.body, headers=response.headers) - - return httpResponse.toDict() + http_request = LambdaHttpRequest(data=event) + response = controller(http_request) + http_response = LambdaHttpResponse( + status_code=response.status_code, + body=response.body, + headers=response.headers + ) + return http_response.toDict() diff --git a/src/modules/get_user/app/get_user_presenter.py b/src/modules/get_user/app/get_user_presenter.py index c00d098..b87634a 100644 --- a/src/modules/get_user/app/get_user_presenter.py +++ b/src/modules/get_user/app/get_user_presenter.py @@ -2,9 +2,7 @@ from .get_user_usecase import GetUserUsecase from src.shared.environments import Environments from src.shared.helpers.external_interfaces.http_lambda_requests import LambdaHttpRequest, LambdaHttpResponse -from src.shared.infra.external.observability.observability_aws import ObservabilityAWS -from aws_lambda_powertools import Logger, Tracer, Metrics - +from src.shared.helpers.observability.wrap_handler import observed_handler observability = Environments.get_observability()(module_name="get_user") @@ -12,19 +10,14 @@ usecase = GetUserUsecase(repo, observability=observability) controller = GetUserController(usecase, observability=observability) -@observability.presenter_decorators -def get_user_presenter(event): - httpRequest = LambdaHttpRequest(data=event) - response = controller(httpRequest) - httpResponse = LambdaHttpResponse(status_code=response.status_code, body=response.body, headers=response.headers) - return httpResponse.toDict() -@observability.handler_decorators +@observed_handler("get_user") def lambda_handler(event, context): - - response = get_user_presenter(event) - - observability.add_metric(name="ErrorCount", unit="Count", value=1) if response["statusCode"] != 200 else None # ErrorCount metrics - - return response - + http_request = LambdaHttpRequest(data=event) + response = controller(http_request) + http_response = LambdaHttpResponse( + status_code=response.status_code, + body=response.body, + headers=response.headers + ) + return http_response.toDict() diff --git a/src/modules/update_user/app/update_user_presenter.py b/src/modules/update_user/app/update_user_presenter.py index d651263..7cb2ac3 100644 --- a/src/modules/update_user/app/update_user_presenter.py +++ b/src/modules/update_user/app/update_user_presenter.py @@ -2,15 +2,20 @@ from .update_user_usecase import UpdateUserUsecase from src.shared.environments import Environments from src.shared.helpers.external_interfaces.http_lambda_requests import LambdaHttpRequest, LambdaHttpResponse +from src.shared.helpers.observability.wrap_handler import observed_handler repo = Environments.get_user_repo()() usecase = UpdateUserUsecase(repo) controller = UpdateUserController(usecase) +@observed_handler("update_user") def lambda_handler(event, context): - httpRequest = LambdaHttpRequest(data=event) - response = controller(httpRequest) - httpResponse = LambdaHttpResponse(status_code=response.status_code, body=response.body, headers=response.headers) - - return httpResponse.toDict() + http_request = LambdaHttpRequest(data=event) + response = controller(http_request) + http_response = LambdaHttpResponse( + status_code=response.status_code, + body=response.body, + headers=response.headers + ) + return http_response.toDict() diff --git a/src/shared/domain/observability/observability_interface.py b/src/shared/domain/observability/observability_interface.py index 1a5b1e4..9e26046 100644 --- a/src/shared/domain/observability/observability_interface.py +++ b/src/shared/domain/observability/observability_interface.py @@ -1,45 +1,44 @@ import os -import time -from aws_lambda_powertools import Logger, Tracer, Metrics from abc import ABC, abstractmethod + class IObservability(ABC): module_name: str mss_name: str - + @abstractmethod def __init__(self, module_name: str) -> None: self.module_name = module_name self.mss_name = os.environ.get("MSS_NAME") - - @abstractmethod + + @abstractmethod def _log_info(self, message: str) -> None: pass - - @abstractmethod + + @abstractmethod def log_controller_in(self) -> None: pass - - @abstractmethod + + @abstractmethod def log_controller_out(self) -> None: pass - - @abstractmethod + + @abstractmethod def log_usecase_in(self) -> None: pass - - @abstractmethod + + @abstractmethod def log_usecase_out(self) -> None: pass - + @abstractmethod def log_exception(self, message: str) -> None: pass - + @abstractmethod def add_metric(self, name: str, unit: str, value: float) -> None: pass - + @abstractmethod def presenter_decorators(self, presenter) -> None: """ @@ -47,10 +46,10 @@ def presenter_decorators(self, presenter) -> None: 1. calculate the processing time of the presenter 2. use the @tracer.capture_method to trace the presenter """ - def presenter_wrapper(event): + def presenter_wrapper(event): pass pass - + @abstractmethod def handler_decorators(self, handler) -> None: """ @@ -59,6 +58,6 @@ def handler_decorators(self, handler) -> None: 2. use the @metrics.log_metrics(capture_cold_start_metric=True, default_dimensions={"environment": os.getenv("STAGE", "dev"), "another": "one"}) # ColdStart metrics and adding dimensions 3. use the @logger.inject_lambda_context(log_event=True) # Log event """ - def handler_wrapper(event, context): + def handler_wrapper(event, context): pass - pass \ No newline at end of file + pass diff --git a/src/shared/helpers/auth/authorizer_user.py b/src/shared/helpers/auth/authorizer_user.py new file mode 100644 index 0000000..7de754f --- /dev/null +++ b/src/shared/helpers/auth/authorizer_user.py @@ -0,0 +1,72 @@ +"""Helpers for API Gateway authorizer → Lambda request. + +Contrato do context: apenas claims Microsoft — sub, mail, name. +Role / User de domínio NÃO entram aqui; isso é responsabilidade do usecase. +""" +from __future__ import annotations + +import json +from typing import Any, Optional + +# chave única em request.data — controllers usam request.data.get(USER_FROM_AUTHORIZER_KEY) +USER_FROM_AUTHORIZER_KEY = "user_from_authorizer" + + +def build_authorizer_user_context(*, sub: str, mail: str, name: str) -> dict[str, str]: + """ + Monta o context do TOKEN authorizer só com claims Microsoft. + + Args: + sub: identificador do usuário no IdP (Graph `id` / JWT `sub`) + mail: e-mail + name: nome de exibição + """ + return { + "user": json.dumps( + { + "sub": sub, + "mail": mail, + "name": name, + } + ) + } + + +def parse_authorizer_user_from_event(event: dict | None) -> Optional[dict[str, Any]]: + """ + Lê o usuário injetado pelo TOKEN authorizer. + + API Gateway REST coloca o context em: + event["requestContext"]["authorizer"]["user"] # string JSON + + Retorno esperado: {"sub", "mail", "name"} ou None. + """ + if not event: + return None + + authorizer = (event.get("requestContext") or {}).get("authorizer") or {} + raw_user = authorizer.get("user") + if raw_user is None: + return None + + if isinstance(raw_user, dict): + parsed = raw_user + elif isinstance(raw_user, str): + try: + parsed = json.loads(raw_user) + except (TypeError, json.JSONDecodeError): + return None + if not isinstance(parsed, dict): + return None + else: + return None + + sub = parsed.get("sub") + mail = parsed.get("mail") + name = parsed.get("name") + if not isinstance(sub, str) or not isinstance(mail, str) or not isinstance(name, str): + return None + if not sub.strip() or not mail.strip(): + return None + + return {"sub": sub, "mail": mail, "name": name} diff --git a/src/shared/helpers/auth/iam_policy.py b/src/shared/helpers/auth/iam_policy.py new file mode 100644 index 0000000..aed1f56 --- /dev/null +++ b/src/shared/helpers/auth/iam_policy.py @@ -0,0 +1,39 @@ +from typing import Any, Optional + + +def generate_policy( + principal_id: str, + effect: str, + method_arn: str, + context: Optional[dict[str, Any]] = None, +) -> dict[str, Any]: + """ + Build an API Gateway TOKEN authorizer IAM policy response. + + Args: + principal_id: Caller principal (usually Microsoft Graph user id). + effect: "Allow" or "Deny". + method_arn: ARN of the method being authorized. + context: Optional string-keyed context passed to the downstream Lambda. + + Returns: + Authorizer response with principalId, policyDocument and optional context. + """ + auth_response: dict[str, Any] = {"principalId": principal_id} + + if effect: + auth_response["policyDocument"] = { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "execute-api:Invoke", + "Effect": effect, + "Resource": method_arn, + } + ], + } + + if context: + auth_response["context"] = context + + return auth_response diff --git a/src/shared/helpers/enum/http_status_code_enum.py b/src/shared/helpers/enum/http_status_code_enum.py index 8fac6ca..9ffed88 100644 --- a/src/shared/helpers/enum/http_status_code_enum.py +++ b/src/shared/helpers/enum/http_status_code_enum.py @@ -4,15 +4,25 @@ class HttpStatusCodeEnum(Enum): OK = 200 CREATED = 201 + ACCEPTED = 202 NO_CONTENT = 204 + MOVED_PERMANENTLY = 301 + FOUND = 302 REDIRECT = 303 + NOT_MODIFIED = 304 BAD_REQUEST = 400 UNAUTHORIZED = 401 + PAYMENT_REQUIRED = 402 FORBIDDEN = 403 NOT_FOUND = 404 METHOD_NOT_ALLOWED = 405 + NOT_ACCEPTABLE = 406 CONFLICT = 409 + UNSUPPORTED_MEDIA_TYPE = 415 + UNPROCESSABLE_ENTITY = 422 + TOO_MANY_REQUESTS = 429 INTERNAL_SERVER_ERROR = 500 + NOT_IMPLEMENTED = 501 BAD_GATEWAY = 502 SERVICE_UNAVAILABLE = 503 - GATEWAY_TIMEOUT = 504 \ No newline at end of file + GATEWAY_TIMEOUT = 504 diff --git a/src/shared/helpers/errors/controller_errors.py b/src/shared/helpers/errors/controller_errors.py index c319375..4523d9b 100644 --- a/src/shared/helpers/errors/controller_errors.py +++ b/src/shared/helpers/errors/controller_errors.py @@ -6,4 +6,8 @@ def __init__(self, message: str): super().__init__(f'Field {message} is missing') class WrongTypeParameter(BaseError): def __init__(self, fieldName: str, fieldTypeExpected: str, fieldTypeReceived: str): - super().__init__(f'Field {fieldName} isn\'t in the right type.\n Received: {fieldTypeReceived}.\n Expected: {fieldTypeExpected}') \ No newline at end of file + message = ( + f"The field '{fieldName}' has the wrong type. " + f"Received: '{fieldTypeReceived}'. Expected: '{fieldTypeExpected}'." + ) + super().__init__(message) diff --git a/src/shared/helpers/errors/usecase_errors.py b/src/shared/helpers/errors/usecase_errors.py index bb403d0..6782d14 100644 --- a/src/shared/helpers/errors/usecase_errors.py +++ b/src/shared/helpers/errors/usecase_errors.py @@ -7,8 +7,15 @@ def __init__(self, message: str): class DuplicatedItem(BaseError): def __init__(self, message: str): super().__init__(f'The item alredy exists for this {message}') - + +class NoUsersFound(BaseError): + def __init__(self, message: str): + super().__init__(f'No users found for {message}') + +class DuplicatedUser(BaseError): + def __init__(self, message: str): + super().__init__(f'The user already exists for this {message}') + class ForbiddenAction(BaseError): def __init__(self, message: str): super().__init__(f'That action is forbidden for this {message}') - diff --git a/src/shared/helpers/external_interfaces/http_codes.py b/src/shared/helpers/external_interfaces/http_codes.py index 3bb8126..21d9166 100644 --- a/src/shared/helpers/external_interfaces/http_codes.py +++ b/src/shared/helpers/external_interfaces/http_codes.py @@ -4,6 +4,7 @@ from src.shared.helpers.external_interfaces.http_models import HttpResponse +# 2xx - Success class OK(HttpResponse): def __init__(self, body: Any = None) -> None: super().__init__(HttpStatusCodeEnum.OK.value, body) @@ -14,19 +15,71 @@ def __init__(self, body: Any = None) -> None: super().__init__(HttpStatusCodeEnum.CREATED.value, body) +class Accepted(HttpResponse): + def __init__(self, body: Any = None) -> None: + super().__init__(HttpStatusCodeEnum.ACCEPTED.value, body) + + class NoContent(HttpResponse): def __init__(self) -> None: super().__init__(HttpStatusCodeEnum.NO_CONTENT.value, None) +# 3xx - Redirection +class MovedPermanently(HttpResponse): + def __init__(self, location: str) -> None: + super().__init__( + HttpStatusCodeEnum.MOVED_PERMANENTLY.value, + None, + headers={"Location": location} + ) + self.location = location + + +class Found(HttpResponse): + def __init__(self, location: str) -> None: + super().__init__( + HttpStatusCodeEnum.FOUND.value, + None, + headers={"Location": location} + ) + self.location = location + + +class RedirectResponse(HttpResponse): + def __init__(self, location: str) -> None: + super().__init__( + HttpStatusCodeEnum.REDIRECT.value, + None, + headers={"Location": location} + ) + self.location = location + + +class NotModified(HttpResponse): + def __init__(self) -> None: + super().__init__(HttpStatusCodeEnum.NOT_MODIFIED.value, None) + + +# 4xx - Client errors class BadRequest(HttpResponse): def __init__(self, body: Any) -> None: super().__init__(HttpStatusCodeEnum.BAD_REQUEST.value, body) -class InternalServerError(HttpResponse): +class Unauthorized(HttpResponse): def __init__(self, body: Any) -> None: - super().__init__(HttpStatusCodeEnum.INTERNAL_SERVER_ERROR.value, body) + super().__init__(HttpStatusCodeEnum.UNAUTHORIZED.value, body) + + +class PaymentRequired(HttpResponse): + def __init__(self, body: Any) -> None: + super().__init__(HttpStatusCodeEnum.PAYMENT_REQUIRED.value, body) + + +class Forbidden(HttpResponse): + def __init__(self, body: Any) -> None: + super().__init__(HttpStatusCodeEnum.FORBIDDEN.value, body) class NotFound(HttpResponse): @@ -34,17 +87,57 @@ def __init__(self, body: Any) -> None: super().__init__(HttpStatusCodeEnum.NOT_FOUND.value, body) +class MethodNotAllowed(HttpResponse): + def __init__(self, body: Any) -> None: + super().__init__(HttpStatusCodeEnum.METHOD_NOT_ALLOWED.value, body) + + +class NotAcceptable(HttpResponse): + def __init__(self, body: Any) -> None: + super().__init__(HttpStatusCodeEnum.NOT_ACCEPTABLE.value, body) + + class Conflict(HttpResponse): def __init__(self, body: Any) -> None: super().__init__(HttpStatusCodeEnum.CONFLICT.value, body) -class RedirectResponse(HttpResponse): - def __init__(self, body: dict) -> None: - super().__init__(HttpStatusCodeEnum.REDIRECT.value, None) - self.location = body +class UnsupportedMediaType(HttpResponse): + def __init__(self, body: Any) -> None: + super().__init__(HttpStatusCodeEnum.UNSUPPORTED_MEDIA_TYPE.value, body) -class Forbidden(HttpResponse): - def __init__(self, body: dict) -> None: - super().__init__(HttpStatusCodeEnum.FORBIDDEN.value, body) +class UnprocessableEntity(HttpResponse): + def __init__(self, body: Any) -> None: + super().__init__(HttpStatusCodeEnum.UNPROCESSABLE_ENTITY.value, body) + + +class TooManyRequests(HttpResponse): + def __init__(self, body: Any) -> None: + super().__init__(HttpStatusCodeEnum.TOO_MANY_REQUESTS.value, body) + + +# 5xx - Server errors +class InternalServerError(HttpResponse): + def __init__(self, body: Any) -> None: + super().__init__(HttpStatusCodeEnum.INTERNAL_SERVER_ERROR.value, body) + + +class NotImplementedResponse(HttpResponse): + def __init__(self, body: Any) -> None: + super().__init__(HttpStatusCodeEnum.NOT_IMPLEMENTED.value, body) + + +class BadGateway(HttpResponse): + def __init__(self, body: Any) -> None: + super().__init__(HttpStatusCodeEnum.BAD_GATEWAY.value, body) + + +class ServiceUnavailable(HttpResponse): + def __init__(self, body: Any) -> None: + super().__init__(HttpStatusCodeEnum.SERVICE_UNAVAILABLE.value, body) + + +class GatewayTimeout(HttpResponse): + def __init__(self, body: Any) -> None: + super().__init__(HttpStatusCodeEnum.GATEWAY_TIMEOUT.value, body) diff --git a/src/shared/helpers/external_interfaces/http_lambda_requests.py b/src/shared/helpers/external_interfaces/http_lambda_requests.py index 8d002ea..194bf23 100644 --- a/src/shared/helpers/external_interfaces/http_lambda_requests.py +++ b/src/shared/helpers/external_interfaces/http_lambda_requests.py @@ -1,5 +1,10 @@ import json +from typing import Any +from src.shared.helpers.auth.authorizer_user import ( + USER_FROM_AUTHORIZER_KEY, + parse_authorizer_user_from_event, +) from src.shared.helpers.external_interfaces.http_models import HttpRequest, HttpResponse @@ -9,26 +14,30 @@ class LambdaHttpResponse(HttpResponse): docs: https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html """ status_code: int = 200 - body: any = {"message": "No response"} + body: Any = {"message": "No response"} headers: dict = {"Content-Type": "application/json"} - def __init__(self, body: any = None, status_code: int = None, headers: dict = None, **kwargs) -> None: + def __init__( + self, + body: Any = None, + status_code: int = None, + headers: dict = None, + **kwargs + ) -> None: """ - Constructor for HttpResponse. + Constructor for LambdaHttpResponse. Args: body: The body of the response. Can be a string or a dict. status_code: The status code of the response. Defaults to 200. headers: The headers of the response. Defaults to {"Content-Type": "application/json"}. **kwargs: Configuration of the HTTP response. Possible values: add_default_cors_headers (default is True) """ - _body = body or LambdaHttpResponse.body - _headers = headers or LambdaHttpResponse.headers - _headers['Access-Control-Allow-Origin'] = '*' - - _status_code = status_code or LambdaHttpResponse.status_code + _body = LambdaHttpResponse.body if body is None else body + _headers = dict(headers) if headers is not None else dict(LambdaHttpResponse.headers) + _status_code = LambdaHttpResponse.status_code if status_code is None else status_code if kwargs.get("add_default_cors_headers", True): - _headers.update({"Access-Control-Allow-Origin": "*"}) + _headers["Access-Control-Allow-Origin"] = "*" super().__init__(body=_body, headers=_headers, status_code=_status_code) @@ -37,7 +46,7 @@ def toDict(self) -> dict: Returns a dict representation of the HttpResponse. Returns: { - 'statuCode': int + 'statusCode': int 'body': str or dict 'headers': dict 'isBase64Encoded': bool @@ -65,9 +74,9 @@ class LambdaDefaultHTTP: def __init__(self, data: dict = None) -> None: """ - Constructor for LambdaHttp. + Constructor for LambdaDefaultHTTP. Args: - event: dict - the event passed to the lambda function. + data: dict - the "http" section of the lambda event requestContext. """ if not data: return @@ -85,9 +94,9 @@ def __eq__(self, other): class LambdaHttpRequest(HttpRequest): """ - A class to represent an HTTP request for lambda URL. - docs: https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html - """ + A class to represent an HTTP request for lambda URL. + docs: https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html + """ version: str raw_path: str raw_query_string: str @@ -95,11 +104,13 @@ class LambdaHttpRequest(HttpRequest): query_string_parameters: dict request_context: dict http: LambdaDefaultHTTP - body: any + body: Any def __init__(self, data: dict = None) -> None: """ - Constructor for HttpResponse. + Constructor for LambdaHttpRequest. + Args: + data: dict - the event passed to the lambda function. """ _headers = data.get("headers") _query_string_parameters = data.get("queryStringParameters") @@ -118,10 +129,11 @@ def __init__(self, data: dict = None) -> None: self.raw_query_string = data.get("rawQueryString") self.query_string_parameters = data.get("queryStringParameters") self.request_context = data.get("requestContext") - self.http = LambdaDefaultHTTP(self.request_context.get("external_interfaces") if self.request_context else None) - - -class HttpResponseRedirect(HttpResponse): - - def __init__(self, location: str) -> None: - super().__init__(status_code=302, headers={"Location": location}) \ No newline at end of file + self.http = LambdaDefaultHTTP(self.request_context.get("http") if self.request_context else None) + + # injeta DEPOIS do body/query para o cliente não spoofar via payload + authorizer_user = parse_authorizer_user_from_event(data) + if authorizer_user is not None: + self.data[USER_FROM_AUTHORIZER_KEY] = authorizer_user + else: + self.data.pop(USER_FROM_AUTHORIZER_KEY, None) diff --git a/src/shared/helpers/observability/wrap_handler.py b/src/shared/helpers/observability/wrap_handler.py new file mode 100644 index 0000000..416c028 --- /dev/null +++ b/src/shared/helpers/observability/wrap_handler.py @@ -0,0 +1,51 @@ +""" +Observability invisível para quem escreve módulos. + +Novos devs só precisam disto no presenter: + + from src.shared.helpers.observability.wrap_handler import observed_handler + + @observed_handler("create_user") + def lambda_handler(event, context): + ... + +Controller / usecase / viewmodel não recebem observability. +Powertools (quando STAGE != TEST) fica encapsulado em ObservabilityAWS. +""" +from __future__ import annotations + +import time +from collections.abc import Callable +from functools import wraps +from typing import Any, TypeVar + +from src.shared.environments import Environments + +F = TypeVar("F", bound=Callable[..., Any]) + + +def observed_handler(module_name: str) -> Callable[[F], F]: + """Aplica logging / metrics / tracing sem poluir o código de negócio.""" + + def decorator(handler: F) -> F: + observability = Environments.get_observability()(module_name=module_name) + + @wraps(handler) + def inner(event: Any, context: Any) -> Any: + start = time.monotonic() + response = handler(event, context) + elapsed_ms = (time.monotonic() - start) * 1000 + observability.add_metric( + name="ProcessingTime", unit="Milliseconds", value=elapsed_ms + ) + if ( + isinstance(response, dict) + and response.get("statusCode") is not None + and response["statusCode"] >= 400 + ): + observability.add_metric(name="ErrorCount", unit="Count", value=1) + return response + + return observability.handler_decorators(inner) + + return decorator diff --git a/tests/shared/helpers/auth/test_iam_policy.py b/tests/shared/helpers/auth/test_iam_policy.py new file mode 100644 index 0000000..3769dda --- /dev/null +++ b/tests/shared/helpers/auth/test_iam_policy.py @@ -0,0 +1,21 @@ +from src.shared.helpers.auth.iam_policy import generate_policy + + +class Test_GeneratePolicy: + def test_allow_policy(self): + policy = generate_policy("principal-1", "Allow", "arn:aws:execute-api:sa-east-1:123:api/GET/users") + + assert policy["principalId"] == "principal-1" + assert policy["policyDocument"]["Statement"][0]["Effect"] == "Allow" + assert "context" not in policy + + def test_deny_policy_with_context(self): + policy = generate_policy( + "user", + "Deny", + "arn:aws:execute-api:*", + context={"user": "{}"}, + ) + + assert policy["policyDocument"]["Statement"][0]["Effect"] == "Deny" + assert policy["context"]["user"] == "{}" diff --git a/tests/shared/helpers/external_interfaces/test_lambda_http_request_authorizer.py b/tests/shared/helpers/external_interfaces/test_lambda_http_request_authorizer.py new file mode 100644 index 0000000..7f4aa2b --- /dev/null +++ b/tests/shared/helpers/external_interfaces/test_lambda_http_request_authorizer.py @@ -0,0 +1,56 @@ +import json + +from src.shared.helpers.auth.authorizer_user import USER_FROM_AUTHORIZER_KEY +from src.shared.helpers.external_interfaces.http_lambda_requests import LambdaHttpRequest + + +class Test_LambdaHttpRequestAuthorizerUser: + def test_injects_user_from_authorizer_into_data(self): + user = { + "sub": "ms-admin", + "mail": "alice@example.com", + "name": "Alice Admin", + } + event = { + "headers": {}, + "queryStringParameters": None, + "body": json.dumps({"email": "payload@example.com"}), + "requestContext": { + "http": {"method": "POST", "path": "/create-user"}, + "authorizer": {"user": json.dumps(user)}, + }, + } + + request = LambdaHttpRequest(data=event) + + assert request.data.get(USER_FROM_AUTHORIZER_KEY) == user + assert request.data.get("email") == "payload@example.com" + + def test_overwrites_spoofed_body_user_from_authorizer(self): + event = { + "headers": {}, + "queryStringParameters": None, + "body": json.dumps({ + USER_FROM_AUTHORIZER_KEY: { + "sub": "spoof", + "mail": "spoof@example.com", + "name": "Spoof", + }, + "email": "payload@example.com", + }), + "requestContext": { + "http": {"method": "POST", "path": "/create-user"}, + "authorizer": { + "user": json.dumps({ + "sub": "ms-user", + "mail": "bob@example.com", + "name": "Bob User", + }) + }, + }, + } + + request = LambdaHttpRequest(data=event) + + assert request.data[USER_FROM_AUTHORIZER_KEY]["mail"] == "bob@example.com" + assert request.data[USER_FROM_AUTHORIZER_KEY]["sub"] == "ms-user" From f340ce643a8b7deee46274cf1592209c120b1b45 Mon Sep 17 00:00:00 2001 From: Leo Iorio Date: Sat, 15 Aug 2026 10:20:35 -0300 Subject: [PATCH 21/50] feat(infra): adiciona dynamo_keys e alinha tabela CDK com pk/sk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prepara a single-table para USER/MEMBER/SUBSCRIBER e o GSI UserEmailIndex, alinhado ao template, sem reescrever o repositório Dynamo ainda. --- iac/iac/template_dynamo_table.py | 51 +++++++--- iac/iac/template_stack.py | 6 +- src/shared/environments.py | 12 +-- .../infra/external/dynamo/dynamo_keys.py | 96 +++++++++++++++++++ .../repositories/load_user_mock_to_dynamo.py | 54 ++++++++--- .../repositories/user_repository_dynamo.py | 13 ++- .../infra/external/dynamo/test_dynamo_keys.py | 53 ++++++++++ 7 files changed, 244 insertions(+), 41 deletions(-) create mode 100644 src/shared/infra/external/dynamo/dynamo_keys.py create mode 100644 tests/shared/infra/external/dynamo/test_dynamo_keys.py diff --git a/iac/iac/template_dynamo_table.py b/iac/iac/template_dynamo_table.py index 9bc82dc..9b274c9 100644 --- a/iac/iac/template_dynamo_table.py +++ b/iac/iac/template_dynamo_table.py @@ -1,30 +1,55 @@ -from decimal import Decimal from aws_cdk import ( - aws_dynamodb as dynamodb, RemovalPolicy, + RemovalPolicy, + aws_dynamodb as dynamodb, ) from constructs import Construct +# Mantenha alinhado com src.shared.infra.external.dynamo.dynamo_keys / Environments +_EPEP_TABLE_PREFIX = "EpepTable" +_USER_EMAIL_INDEX_NAME = "UserEmailIndex" + +RETAINED_STAGES = {"prod", "homolog"} + class TemplateDynamoTable(Construct): table: dynamodb.Table - def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: + def __init__(self, scope: Construct, construct_id: str, stage: str = "DEV", **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) + stage_lower = stage.lower() + removal_policy = ( + RemovalPolicy.RETAIN if stage_lower in RETAINED_STAGES else RemovalPolicy.DESTROY + ) + self.table = dynamodb.Table( - self, "TemplateDynamoTable", + self, + "EpepDynamoTable", partition_key=dynamodb.Attribute( - name="PK", - type=dynamodb.AttributeType.STRING + name="pk", + type=dynamodb.AttributeType.STRING, ), sort_key=dynamodb.Attribute( - name="SK", - type=dynamodb.AttributeType.STRING - ), + name="sk", + type=dynamodb.AttributeType.STRING, + ), billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST, - removal_policy=RemovalPolicy.DESTROY + removal_policy=removal_policy, + table_name=f"{_EPEP_TABLE_PREFIX}-{stage_lower}", + point_in_time_recovery_specification=dynamodb.PointInTimeRecoverySpecification( + point_in_time_recovery_enabled=(stage_lower == "prod") + ), ) - - - + self.table.add_global_secondary_index( + index_name=_USER_EMAIL_INDEX_NAME, + partition_key=dynamodb.Attribute( + name="gsi2pk", + type=dynamodb.AttributeType.STRING, + ), + sort_key=dynamodb.Attribute( + name="gsi2sk", + type=dynamodb.AttributeType.STRING, + ), + projection_type=dynamodb.ProjectionType.ALL, + ) diff --git a/iac/iac/template_stack.py b/iac/iac/template_stack.py index dbaf9b4..0556518 100644 --- a/iac/iac/template_stack.py +++ b/iac/iac/template_stack.py @@ -34,13 +34,13 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: } ) - self.dynamo_table = TemplateDynamoTable(self, "TemplateDynamoTable") + self.dynamo_table = TemplateDynamoTable(self, "TemplateDynamoTable", stage="DEV") ENVIRONMENT_VARIABLES = { "STAGE": "DEV", "DYNAMO_TABLE_NAME": self.dynamo_table.table.table_name, - "DYNAMO_PARTITION_KEY": "PK", - "DYNAMO_SORT_KEY": "SK", + "DYNAMO_PARTITION_KEY": "pk", + "DYNAMO_SORT_KEY": "sk", "REGION": self.region, } diff --git a/src/shared/environments.py b/src/shared/environments.py index ce8c825..0dcb61a 100644 --- a/src/shared/environments.py +++ b/src/shared/environments.py @@ -24,7 +24,7 @@ class Environments: stage: STAGE s3_bucket_name: str region: str - endpoint_url: str = None + dynamo_endpoint_url: str = None # DynamoDB Local (ex: http://localhost:8000); None na AWS dynamo_table_name: str dynamo_partition_key: str dynamo_sort_key: str @@ -46,16 +46,16 @@ def load_envs(self): if self.stage == STAGE.TEST: self.s3_bucket_name = "bucket-test" self.region = "sa-east-1" - self.endpoint_url = "http://localhost:8000" + self.dynamo_endpoint_url = "http://localhost:8000" self.dynamo_table_name = "user_mss_template-table" - self.dynamo_partition_key = "PK" - self.dynamo_sort_key = "SK" + self.dynamo_partition_key = "pk" + self.dynamo_sort_key = "sk" self.cloud_front_distribution_domain = "https://d3q9q9q9q9q9q9.cloudfront.net" else: self.s3_bucket_name = os.environ.get("S3_BUCKET_NAME") self.region = os.environ.get("REGION") - self.endpoint_url = os.environ.get("ENDPOINT_URL") + self.dynamo_endpoint_url = os.environ.get("DYNAMO_ENDPOINT_URL") self.dynamo_table_name = os.environ.get("DYNAMO_TABLE_NAME") self.dynamo_partition_key = os.environ.get("DYNAMO_PARTITION_KEY") self.dynamo_sort_key = os.environ.get("DYNAMO_SORT_KEY") @@ -86,7 +86,7 @@ def get_observability() -> IObservability: def get_envs() -> "Environments": """ Returns the Environments object. This method should be used to get the Environments object instead of instantiating it directly. - :return: Environments (stage={self.stage}, s3_bucket_name={self.s3_bucket_name}, region={self.region}, endpoint_url={self.endpoint_url}) + :return: Environments (stage={self.stage}, region={self.region}, dynamo_table_name={self.dynamo_table_name}, dynamo_endpoint_url={self.dynamo_endpoint_url}) """ envs = Environments() diff --git a/src/shared/infra/external/dynamo/dynamo_keys.py b/src/shared/infra/external/dynamo/dynamo_keys.py new file mode 100644 index 0000000..c4f3d25 --- /dev/null +++ b/src/shared/infra/external/dynamo/dynamo_keys.py @@ -0,0 +1,96 @@ +"""Convenções de chaves Dynamo (tabela base + GSI UserEmailIndex). + +Tabela base (single-table): + pk = USER | MEMBER | SUBSCRIBER + sk = USER# | MEMBER# | SUBSCRIBER# + +GSI2 (UserEmailIndex) — access pattern "user por email": + gsi2pk = EMAIL# + gsi2sk = USER# + +Uso no repository Dynamo (exemplo):: + + from boto3.dynamodb.conditions import Key + from src.shared.infra.external.dynamo.dynamo_keys import ( + GSI2_NAME, GSI2_PK_ATTR, gsi2_partition_key, + ) + + resp = self.dynamo.query( + KeyConditionExpression=Key(GSI2_PK_ATTR).eq(gsi2_partition_key(user_email)), + IndexName=GSI2_NAME, + ) +""" + +from enum import Enum +from typing import Any +from uuid import UUID + +from pydantic import EmailStr + +# Nomes dos atributos da tabela base (alinhados ao CDK: pk/sk) +PK_ATTR = "pk" +SK_ATTR = "sk" + +# GSI2 — access pattern: buscar user por email (denso: email sempre presente) +# Alinhado a iac/iac/template_dynamo_table.py (UserEmailIndex) +GSI2_NAME = "UserEmailIndex" +GSI2_PK_ATTR = "gsi2pk" +GSI2_SK_ATTR = "gsi2sk" + +STORAGE_KEY_ATTRS = ( + PK_ATTR, SK_ATTR, + GSI2_PK_ATTR, GSI2_SK_ATTR, +) + + +class EntityKind(str, Enum): + USER = "USER" + MEMBER = "MEMBER" + SUBSCRIBER = "SUBSCRIBER" + + +def partition_key(kind: EntityKind) -> str: + """PK da tabela base — coleção (se repete para todos os items do kind).""" + return kind.value + + +def sort_key(id: UUID, kind: EntityKind) -> str: + """SK da tabela base — identidade única dentro da coleção.""" + return f"{kind.value}#{id}" + + +def gsi2_partition_key(user_email: EmailStr) -> str: + """ + PK do GSI2 — agrupa por email. + + Ex.: EMAIL#user@example.com + """ + return f"EMAIL#{user_email}" + + +def gsi2_sort_key(user_id: UUID) -> str: + """ + SK do GSI2 — identidade do user no índice de email. + + Ex.: USER# + """ + return sort_key(id=user_id, kind=EntityKind.USER) + + +def build_gsi2_attributes( + user_email: EmailStr, + user_id: UUID, +) -> dict[str, str]: + """ + GSI denso: email é obrigatório na entidade User, então todo user + recebe gsi2pk/gsi2sk e entra no UserEmailIndex. + """ + return { + GSI2_PK_ATTR: gsi2_partition_key(user_email=user_email), + GSI2_SK_ATTR: gsi2_sort_key(user_id=user_id), + } + + +def strip_keys(item: dict[str, Any]) -> dict[str, Any]: + """Remove atributos de storage (pk/sk/gsi) antes do model_validate.""" + return {k: v for k, v in item.items() if k not in STORAGE_KEY_ATTRS} diff --git a/src/shared/infra/repositories/load_user_mock_to_dynamo.py b/src/shared/infra/repositories/load_user_mock_to_dynamo.py index 8c55305..2ad0852 100644 --- a/src/shared/infra/repositories/load_user_mock_to_dynamo.py +++ b/src/shared/infra/repositories/load_user_mock_to_dynamo.py @@ -5,11 +5,19 @@ from src.shared.infra.repositories.user_repository_dynamo import UserRepositoryDynamo from src.shared.infra.repositories.user_repository_mock import UserRepositoryMock from src.shared.environments import Environments +from src.shared.infra.external.dynamo.dynamo_keys import ( + GSI2_NAME, + GSI2_PK_ATTR, + GSI2_SK_ATTR, +) def setup_dynamo_table(): - dynamo_table_name = "user_mss_template-table" - endpoint_url = "http://localhost:8000" + envs = Environments.get_envs() + dynamo_table_name = envs.dynamo_table_name + endpoint_url = envs.dynamo_endpoint_url + pk = envs.dynamo_partition_key + sk = envs.dynamo_sort_key print("Setting up DynamoDB table...") dynamo_client = boto3.client('dynamodb', endpoint_url=endpoint_url) @@ -22,24 +30,41 @@ def setup_dynamo_table(): TableName=dynamo_table_name, KeySchema=[ { - 'AttributeName': 'PK', + 'AttributeName': pk, 'KeyType': 'HASH' }, { - 'AttributeName': 'SK', + 'AttributeName': sk, 'KeyType': 'RANGE' } ], AttributeDefinitions=[ { - 'AttributeName': 'PK', + 'AttributeName': pk, 'AttributeType': 'S' }, { - 'AttributeName': 'SK', + 'AttributeName': sk, 'AttributeType': 'S' - } - + }, + { + 'AttributeName': GSI2_PK_ATTR, + 'AttributeType': 'S' + }, + { + 'AttributeName': GSI2_SK_ATTR, + 'AttributeType': 'S' + }, + ], + GlobalSecondaryIndexes=[ + { + "IndexName": GSI2_NAME, + "KeySchema": [ + {"AttributeName": GSI2_PK_ATTR, "KeyType": "HASH"}, + {"AttributeName": GSI2_SK_ATTR, "KeyType": "RANGE"}, + ], + "Projection": {"ProjectionType": "ALL"}, + }, ], BillingMode='PAY_PER_REQUEST', ) @@ -56,13 +81,13 @@ def setup_dynamo_table(): table.put_item( Item={ - 'PK': 'COUNTER', - 'SK': 'COUNTER', + pk: 'COUNTER', + sk: 'COUNTER', 'COUNTER': Decimal(0) } ) - print('Table "user_mss_template-table" created!') + print(f'Table "{dynamo_table_name}" created!') else: print("Table already exists!") @@ -89,15 +114,16 @@ def load_mock_to_real_dynamo(): count = 0 + envs = Environments.get_envs() dynamodb = boto3.resource('dynamodb') - table = dynamodb.Table(dynamo_table_name=Environments.get_envs().dynamo_table_name) + table = dynamodb.Table(envs.dynamo_table_name) print("Adding counter to table") table.put_item( Item={ - 'PK': 'COUNTER', - 'SK': 'COUNTER', + envs.dynamo_partition_key: 'COUNTER', + envs.dynamo_sort_key: 'COUNTER', 'COUNTER': Decimal(0) } ) diff --git a/src/shared/infra/repositories/user_repository_dynamo.py b/src/shared/infra/repositories/user_repository_dynamo.py index 15accd9..17408c7 100644 --- a/src/shared/infra/repositories/user_repository_dynamo.py +++ b/src/shared/infra/repositories/user_repository_dynamo.py @@ -20,11 +20,14 @@ def sort_key_format(user_id: int) -> str: return f"#{user_id}" def __init__(self): - self.dynamo = DynamoDatasource(endpoint_url=Environments.get_envs().endpoint_url, - dynamo_table_name=Environments.get_envs().dynamo_table_name, - region=Environments.get_envs().region, - partition_key=Environments.get_envs().dynamo_partition_key, - sort_key=Environments.get_envs().dynamo_sort_key) + envs = Environments.get_envs() + self.dynamo = DynamoDatasource( + endpoint_url=envs.dynamo_endpoint_url, + dynamo_table_name=envs.dynamo_table_name, + region=envs.region, + partition_key=envs.dynamo_partition_key, + sort_key=envs.dynamo_sort_key, + ) def get_user(self, user_id: int) -> User: resp = self.dynamo.get_item(partition_key=self.partition_key_format(user_id), sort_key=self.sort_key_format(user_id)) diff --git a/tests/shared/infra/external/dynamo/test_dynamo_keys.py b/tests/shared/infra/external/dynamo/test_dynamo_keys.py new file mode 100644 index 0000000..88752cc --- /dev/null +++ b/tests/shared/infra/external/dynamo/test_dynamo_keys.py @@ -0,0 +1,53 @@ +from uuid import UUID + +from src.shared.infra.external.dynamo.dynamo_keys import ( + EntityKind, + GSI2_NAME, + GSI2_PK_ATTR, + GSI2_SK_ATTR, + PK_ATTR, + SK_ATTR, + build_gsi2_attributes, + partition_key, + sort_key, + strip_keys, +) + + +class Test_DynamoKeys: + def test_user_base_keys(self): + user_id = UUID("11111111-1111-1111-1111-111111111111") + + assert PK_ATTR == "pk" + assert SK_ATTR == "sk" + assert partition_key(EntityKind.USER) == "USER" + assert sort_key(user_id, EntityKind.USER) == f"USER#{user_id}" + + def test_member_and_subscriber_kinds(self): + member_id = UUID("22222222-2222-2222-2222-222222222222") + subscriber_id = UUID("33333333-3333-3333-3333-333333333333") + + assert partition_key(EntityKind.MEMBER) == "MEMBER" + assert sort_key(member_id, EntityKind.MEMBER) == f"MEMBER#{member_id}" + assert partition_key(EntityKind.SUBSCRIBER) == "SUBSCRIBER" + assert sort_key(subscriber_id, EntityKind.SUBSCRIBER) == f"SUBSCRIBER#{subscriber_id}" + + def test_user_email_gsi(self): + user_id = UUID("11111111-1111-1111-1111-111111111111") + attrs = build_gsi2_attributes("user@example.com", user_id) + + assert GSI2_NAME == "UserEmailIndex" + assert attrs[GSI2_PK_ATTR] == "EMAIL#user@example.com" + assert attrs[GSI2_SK_ATTR] == f"USER#{user_id}" + + def test_strip_keys(self): + user_id = UUID("11111111-1111-1111-1111-111111111111") + item = { + "pk": "USER", + "sk": f"USER#{user_id}", + "gsi2pk": "EMAIL#user@example.com", + "gsi2sk": f"USER#{user_id}", + "email": "user@example.com", + } + + assert strip_keys(item) == {"email": "user@example.com"} From 29b6091730b8e1c1696f6be8453f5c6d56e2a3b2 Mon Sep 17 00:00:00 2001 From: Teitz Date: Sat, 15 Aug 2026 14:41:56 -0300 Subject: [PATCH 22/50] feat: Adiciona entidade Member, enum dos cargos e o teste da nova entidade --- src/shared/domain/entities/member.py | 66 +++ .../domain/enums/member_function_enum.py | 5 + tests/shared/domain/entities/test_member.py | 401 ++++++++++++++++++ 3 files changed, 472 insertions(+) create mode 100644 src/shared/domain/entities/member.py create mode 100644 src/shared/domain/enums/member_function_enum.py create mode 100644 tests/shared/domain/entities/test_member.py diff --git a/src/shared/domain/entities/member.py b/src/shared/domain/entities/member.py new file mode 100644 index 0000000..7deab90 --- /dev/null +++ b/src/shared/domain/entities/member.py @@ -0,0 +1,66 @@ +import uuid +from pydantic import BaseModel, Field, ValidationError, HttpUrl, ConfigDict, field_validator + +from src.shared.helpers.errors.domain_errors import EntityError +from src.shared.domain.enums.member_function_enum import MemberFunctionEnum + +class Member(BaseModel): + def __init__(self, **data): + try: + super().__init__(**data) + except ValidationError as err: + raise EntityError(str(err.errors()[0]["loc"][0])) from err + + + member_id: uuid.UUID = Field( + description="Identificador único de membro", + default_factory=uuid.uuid4 + ) + + + name: str = Field( + ..., + description="Nome do membro", + min_length=2, + pattern=r"^[a-zA-ZÀ-ÿ\s]+$" + ) + + + member_function: MemberFunctionEnum = Field( + ..., + description="Cargo do membro" + ) + + + linkedin: HttpUrl = Field( + ..., + description="Perfil do LinkedIn do membro" + ) + + + member_photo: HttpUrl = Field( + ..., + description="Foto do membro" + ) + @field_validator("member_photo") + @classmethod + def validate_photo_extension(cls, url: HttpUrl) -> HttpUrl: + allowed_extensions = (".png", ".jpg", ".jpeg", ".webp") + if not str(url).lower().endswith(allowed_extensions): + raise ValueError("member_photo must end with .png, .jpg, .jpeg, or .webp") + return url + # Checks the URL's extension format, but not its content. It can still be a non-existent URL, for example + + + description: str = Field( + ..., + description="Descrição do membro", + min_length=5 + ) + + + model_config = ConfigDict( + use_enum_values=True, + extra="forbid", + populate_by_name=True, + ) \ No newline at end of file diff --git a/src/shared/domain/enums/member_function_enum.py b/src/shared/domain/enums/member_function_enum.py new file mode 100644 index 0000000..ca28658 --- /dev/null +++ b/src/shared/domain/enums/member_function_enum.py @@ -0,0 +1,5 @@ +from enum import Enum + +class MemberFunctionEnum(Enum): + REDACAO = "Redacao" + MARKETING = "Marketing" \ No newline at end of file diff --git a/tests/shared/domain/entities/test_member.py b/tests/shared/domain/entities/test_member.py new file mode 100644 index 0000000..372a089 --- /dev/null +++ b/tests/shared/domain/entities/test_member.py @@ -0,0 +1,401 @@ +import pytest +import uuid + +from src.shared.domain.entities.member import Member +from src.shared.domain.enums.member_function_enum import MemberFunctionEnum +from src.shared.helpers.errors.domain_errors import EntityError + + +class Test_Member: + + def test_member(self): + member = Member( + name="Nome Completo do Membro", + member_function="Marketing", + linkedin="https://www.linkedin.com/", + member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + description="Exemplo de descrição do membro" + ) + assert isinstance(member.member_id, uuid.UUID) + assert member.name == "Nome Completo do Membro" + assert member.member_function == "Marketing" + assert str(member.linkedin) == "https://www.linkedin.com/" + assert str(member.member_photo) == "https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png" + assert member.description == "Exemplo de descrição do membro" + + + + # model_config + + def test_member_extra_field(self): + with pytest.raises(EntityError): + member = Member( + name="Nome Completo do Membro", + member_function="Marketing", + linkedin="https://www.linkedin.com/", + member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + description="Exemplo de descrição do membro", + extra_field="Campo extra" + ) + + + + # name + + def test_member_name_with_numbers(self): + with pytest.raises(EntityError): + member = Member( + name="N0me 1nválido", + member_function="Marketing", + linkedin="https://www.linkedin.com/", + member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + description="Exemplo de descrição do membro" + ) + + + def test_member_name_with_invalid_symbols(self): + with pytest.raises(EntityError): + member = Member( + name="[Nome Inválido!]", + member_function="Marketing", + linkedin="https://www.linkedin.com/", + member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + description="Exemplo de descrição do membro" + ) + + + def test_member_name_too_short(self): + with pytest.raises(EntityError): + member = Member( + name="N", + member_function="Marketing", + linkedin="https://www.linkedin.com/", + member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + description="Exemplo de descrição do membro" + ) + + + def test_member_name_min_length(self): + member = Member( + name="Ju", + member_function="Marketing", + linkedin="https://www.linkedin.com/", + member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + description="Exemplo de descrição do membro" + ) + assert member.name == "Ju" + + + def test_member_name_empty(self): + with pytest.raises(EntityError): + member = Member( + name="", + member_function="Marketing", + linkedin="https://www.linkedin.com/", + member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + description="Exemplo de descrição do membro" + ) + + + def test_member_has_no_name(self): + with pytest.raises(EntityError): + member = Member( + member_function="Marketing", + linkedin="https://www.linkedin.com/", + member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + description="Exemplo de descrição do membro" + ) + + + + # member_function + + def test_member_function_invalid(self): + with pytest.raises(EntityError): + member = Member( + name="Nome Completo do Membro", + member_function="Cargo Inválido", + linkedin="https://www.linkedin.com/", + member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + description="Exemplo de descrição do membro" + ) + + + def test_member_function_redacao_enum(self): + member = Member( + name="Nome Completo do Membro", + member_function=MemberFunctionEnum.REDACAO, + linkedin="https://www.linkedin.com/", + member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + description="Exemplo de descrição do membro" + ) + assert member.member_function == "Redacao" + + + def test_member_function_marketing_enum(self): + member = Member( + name="Nome Completo do Membro", + member_function=MemberFunctionEnum.MARKETING, + linkedin="https://www.linkedin.com/", + member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + description="Exemplo de descrição do membro" + ) + assert member.member_function == "Marketing" + + + def test_member_function_empty(self): + with pytest.raises(EntityError): + member = Member( + name="Nome Completo do Membro", + member_function="", + linkedin="https://www.linkedin.com/", + member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + description="Exemplo de descrição do membro" + ) + + + def test_member_has_no_function(self): + with pytest.raises(EntityError): + member = Member( + name="Nome Completo do Membro", + linkedin="https://www.linkedin.com/", + member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + description="Exemplo de descrição do membro" + ) + + + + # linkedin + + def test_member_linkedin_invalid_url(self): + with pytest.raises(EntityError): + member = Member( + name="Nome Completo do Membro", + member_function="Marketing", + linkedin="Não é uma url", + member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + description="Exemplo de descrição do membro" + ) + + + def test_member_linkedin_no_scheme(self): + with pytest.raises(EntityError): + member = Member( + name="Nome Completo do Membro", + member_function="Marketing", + linkedin="www.linkedin.com", + member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + description="Exemplo de descrição do membro" + ) + + + def test_member_linkedin_empty(self): + with pytest.raises(EntityError): + member = Member( + name="Nome Completo do Membro", + member_function="Marketing", + linkedin="", + member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + description="Exemplo de descrição do membro" + ) + + + def test_member_linkedin_normalizes_slash(self): + member = Member( + name="Nome Completo do Membro", + member_function="Marketing", + linkedin="https://www.linkedin.com", + member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + description="Exemplo de descrição do membro" + ) + assert str(member.linkedin) == "https://www.linkedin.com/" + + + def test_member_has_no_linkedin(self): + with pytest.raises(EntityError): + member = Member( + name="Nome Completo do Membro", + member_function="Marketing", + member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + description="Exemplo de descrição do membro" + ) + + + + # member_photo + + def test_member_photo_invalid_url(self): + with pytest.raises(EntityError): + member = Member( + name="Nome Completo do Membro", + member_function="Marketing", + linkedin="https://www.linkedin.com/", + member_photo="Não é URL", + description="Exemplo de descrição do membro" + ) + + + def test_member_photo_no_scheme(self): + with pytest.raises(EntityError): + member = Member( + name="Nome Completo do Membro", + member_function="Marketing", + linkedin="https://www.linkedin.com/", + member_photo="portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + description="Exemplo de descrição do membro" + ) + + + def test_member_photo_url_no_file_extension(self): + with pytest.raises(EntityError): + member = Member( + name="Nome Completo do Membro", + member_function="Marketing", + linkedin="https://www.linkedin.com/", + member_photo="https://portalinterno.devmaua.com/", + description="Exemplo de descrição do membro" + ) + + + def test_member_photo_accepts_png(self): + member = Member( + name="Nome Completo do Membro", + member_function="Marketing", + linkedin="https://www.linkedin.com/", + member_photo="https://www.exemplo.com/imagem.png", + description="Exemplo de descrição do membro" + ) + assert str(member.member_photo).endswith(".png") + + + def test_member_photo_accepts_jpg(self): + member = Member( + name="Nome Completo do Membro", + member_function="Marketing", + linkedin="https://www.linkedin.com/", + member_photo="https://www.exemplo.com/imagem.jpg", + description="Exemplo de descrição do membro" + ) + assert str(member.member_photo).endswith(".jpg") + + + def test_member_photo_accepts_jpeg(self): + member = Member( + name="Nome Completo do Membro", + member_function="Marketing", + linkedin="https://www.linkedin.com/", + member_photo="https://www.exemplo.com/imagem.jpeg", + description="Exemplo de descrição do membro" + ) + assert str(member.member_photo).endswith(".jpeg") + + + def test_member_photo_accepts_webp(self): + member = Member( + name="Nome Completo do Membro", + member_function="Marketing", + linkedin="https://www.linkedin.com/", + member_photo="https://www.exemplo.com/imagem.webp", + description="Exemplo de descrição do membro" + ) + assert str(member.member_photo).endswith(".webp") + + + def test_member_photo_rejects_other_extensions(self): + with pytest.raises(EntityError): + member = Member( + name="Nome Completo do Membro", + member_function="Marketing", + linkedin="https://www.linkedin.com/", + member_photo="https://www.exemplo.com/imagem.pdf", + description="Exemplo de descrição do membro" + ) + + + def test_member_photo_case_insensitive(self): + member = Member( + name="Nome Completo do Membro", + member_function="Marketing", + linkedin="https://www.linkedin.com/", + member_photo="https://www.exemplo.com/imagem.PNG", + description="Exemplo de descrição do membro" + ) + assert str(member.member_photo).endswith(".PNG") + + + def test_member_photo_empty(self): + with pytest.raises(EntityError): + member = Member( + name="Nome Completo do Membro", + member_function="Marketing", + linkedin="https://www.linkedin.com/", + member_photo="", + description="Exemplo de descrição do membro" + ) + + + def test_member_has_no_photo(self): + with pytest.raises(EntityError): + member = Member( + name="Nome Completo do Membro", + member_function="Marketing", + linkedin="https://www.linkedin.com/", + description="Exemplo de descrição do membro" + ) + + + + # description + + def test_member_description_with_numbers(self): + member = Member( + name="Nome Completo do Membro", + member_function="Marketing", + linkedin="https://www.linkedin.com/", + member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + description="Descrição: membro melhorou em 10%. (com parênteses)!" + ) + assert member.description == "Descrição: membro melhorou em 10%. (com parênteses)!" + + + def test_member_description_too_short(self): + with pytest.raises(EntityError): + member = Member( + name="Nome Completo do Membro", + member_function="Marketing", + linkedin="https://www.linkedin.com/", + member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + description="Abcd" + ) + + + def test_member_description_min_length(self): + member = Member( + name="Nome Completo do Membro", + member_function="Marketing", + linkedin="https://www.linkedin.com/", + member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + description="Curto" + ) + assert member.description == "Curto" + + + def test_member_description_empty(self): + with pytest.raises(EntityError): + member = Member( + name="Nome Completo do Membro", + member_function="Marketing", + linkedin="https://www.linkedin.com/", + member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + description="" + ) + + + def test_member_has_no_description(self): + with pytest.raises(EntityError): + member = Member( + name="Nome Completo do Membro", + member_function="Marketing", + linkedin="https://www.linkedin.com/", + member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png" + ) From 8a85aca16a1fdf8cb43bdc01111d2c92c187fcbd Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 18 Aug 2026 00:43:48 -0300 Subject: [PATCH 23/50] feat: member interface and mock, with tests --- .../member_repository_interface.py | 37 ++++ .../repositories/member_repository_mock.py | 56 ++++++ .../test_member_repository_mock.py | 181 ++++++++++++++++++ 3 files changed, 274 insertions(+) create mode 100644 src/shared/domain/repositories/member_repository_interface.py create mode 100644 src/shared/infra/repositories/member_repository_mock.py create mode 100644 tests/shared/infra/repositories/test_member_repository_mock.py diff --git a/src/shared/domain/repositories/member_repository_interface.py b/src/shared/domain/repositories/member_repository_interface.py new file mode 100644 index 0000000..bc7251a --- /dev/null +++ b/src/shared/domain/repositories/member_repository_interface.py @@ -0,0 +1,37 @@ +import uuid +from abc import ABC, abstractmethod +from typing import List + +from src.shared.domain.entities.member import Member + + +class IMemberRepository(ABC): + + @abstractmethod + def get_member(self, member_id: uuid.UUID) -> Member: + """ + If member not found raise NoItemsFound + """ + pass + + @abstractmethod + def get_all_member(self) -> List[Member]: + pass + + @abstractmethod + def create_member(self, new_member: Member) -> Member: + pass + + @abstractmethod + def delete_member(self, member_id: uuid.UUID) -> Member: + """ + If member not found raise NoItemsFound + """ + pass + + @abstractmethod + def update_member(self, member: Member) -> Member: + """ + If member not found raise NoItemsFound + """ + pass diff --git a/src/shared/infra/repositories/member_repository_mock.py b/src/shared/infra/repositories/member_repository_mock.py new file mode 100644 index 0000000..4fbac8c --- /dev/null +++ b/src/shared/infra/repositories/member_repository_mock.py @@ -0,0 +1,56 @@ +import uuid +from typing import List + +from src.shared.domain.entities.member import Member +from src.shared.domain.repositories.member_repository_interface import IMemberRepository +from src.shared.helpers.errors.usecase_errors import NoItemsFound + + +class MemberRepositoryMock(IMemberRepository): + members: List[Member] + + def __init__(self): + self.members = [ + Member( + name="Nome do Membro Um", + member_function="Marketing", + linkedin="https://www.linkedin.com/1", + member_photo="https://portalinterno.devmaua.com/assets/logo_fake1.png", + description="Exemplo de descrição do membro 1" + ), + Member( + name="Nome do Membro Dois", + member_function="Redacao", + linkedin="https://www.linkedin.com/2", + member_photo="https://portalinterno.devmaua.com/assets/logo_fake2.png", + description="Exemplo de descrição do membro 2" + ) + ] + + def get_member(self, member_id: uuid.UUID) -> Member: + for member in self.members: + if member.member_id == member_id: + return member + raise NoItemsFound("member_id") + + def get_all_member(self) -> List[Member]: + return self.members + + def create_member(self, new_member: Member) -> Member: + self.members.append(new_member) + return new_member + + def delete_member(self, member_id: uuid.UUID) -> Member: + for idx, member in enumerate(self.members): + if member.member_id == member_id: + return self.members.pop(idx) + + raise NoItemsFound("member_id") + + def update_member(self, member: Member) -> Member: + for idx, stored_member in enumerate(self.members): + if stored_member.member_id == member.member_id: + self.members[idx] = member + return self.members[idx] + + raise NoItemsFound("member_id") diff --git a/tests/shared/infra/repositories/test_member_repository_mock.py b/tests/shared/infra/repositories/test_member_repository_mock.py new file mode 100644 index 0000000..8e266ab --- /dev/null +++ b/tests/shared/infra/repositories/test_member_repository_mock.py @@ -0,0 +1,181 @@ +import uuid +import pytest + +from src.shared.domain.entities.member import Member +from src.shared.domain.enums.member_function_enum import MemberFunctionEnum +from src.shared.helpers.errors.usecase_errors import NoItemsFound +from src.shared.infra.repositories.member_repository_mock import MemberRepositoryMock + + +class Test_MemberRepositoryMock: + + def test_create_member(self): + repo = MemberRepositoryMock() + member = Member( + name="Membro Três", + member_function="Marketing", + linkedin="https://www.linkedin.com/3", + member_photo="https://portalinterno.devmaua.com/assets/logo_fake3.png", + description="Descrição do terceiro membro do repo" + ) + repo.create_member(member) + assert repo.members[-1].name == "Membro Três" + assert repo.members[-1].member_function == "Marketing" + assert str(repo.members[-1].linkedin) =="https://www.linkedin.com/3" + assert str(repo.members[-1].member_photo) =="https://portalinterno.devmaua.com/assets/logo_fake3.png" + assert repo.members[-1].description =="Descrição do terceiro membro do repo" + assert len(repo.members) == 3 + + + def test_get_member(self): + repo = MemberRepositoryMock() + member_id = repo.get_all_member()[1].member_id + member = repo.get_member(member_id) + + assert member.name == "Nome do Membro Dois" + assert member.member_function == "Redacao" + assert str(member.linkedin) == "https://www.linkedin.com/2" + assert str(member.member_photo) == "https://portalinterno.devmaua.com/assets/logo_fake2.png" + assert member.description == "Exemplo de descrição do membro 2" + + + def test_get_member_not_found(self): + repo = MemberRepositoryMock() + with pytest.raises(NoItemsFound): + repo.get_member(uuid.uuid4()) + + + def test_get_all_member(self): + repo = MemberRepositoryMock() + members = repo.get_all_member() + + assert len(members) == 2 + + + def test_update_member_function(self): + repo = MemberRepositoryMock() + member = repo.get_all_member()[0] + + member_old_function = member.member_function + member_name = member.name + + member = Member( + member_id=member.member_id, + name=member.name, + member_function="Redacao", + linkedin=member.linkedin, + member_photo=member.member_photo, + description=member.description + ) + updated_member = repo.update_member(member) + + assert updated_member is not None + assert updated_member.name == member_name + assert updated_member.member_function != member_old_function + assert updated_member.member_function == "Redacao" + assert repo.members[0].member_function == "Redacao" + + + def test_update_member_linkedin(self): + repo = MemberRepositoryMock() + member = repo.get_all_member()[0] + + member_old_linkedin = member.linkedin + member_name = member.name + + member = Member( + member_id=member.member_id, + name=member.name, + member_function=member.member_function, + linkedin="https://www.linkedin.com/novo", + member_photo=member.member_photo, + description=member.description + ) + updated_member = repo.update_member(member) + + assert updated_member is not None + assert updated_member.name == member_name + assert updated_member.linkedin != member_old_linkedin + assert str(updated_member.linkedin) == "https://www.linkedin.com/novo" + assert str(repo.members[0].linkedin) == "https://www.linkedin.com/novo" + + + def test_update_member_photo(self): + repo = MemberRepositoryMock() + member = repo.get_all_member()[0] + + member_old_photo = member.member_photo + member_name = member.name + + member = Member( + member_id=member.member_id, + name=member.name, + member_function=member.member_function, + linkedin=member.linkedin, + member_photo="https://www.photo.com/photo1.jpg", + description=member.description + ) + updated_member = repo.update_member(member) + + assert updated_member is not None + assert updated_member.name == member_name + assert updated_member.member_photo != member_old_photo + assert str(updated_member.member_photo) == "https://www.photo.com/photo1.jpg" + assert str(repo.members[0].member_photo) == "https://www.photo.com/photo1.jpg" + + + def test_update_member_description(self): + repo = MemberRepositoryMock() + member = repo.get_all_member()[0] + + member_old_description = member.description + member_name = member.name + + member = Member( + member_id=member.member_id, + name=member.name, + member_function=member.member_function, + linkedin=member.linkedin, + member_photo=member.member_photo, + description="Nova descrição do membro" + ) + updated_member = repo.update_member(member) + + assert updated_member is not None + assert updated_member.name == member_name + assert updated_member.description != member_old_description + assert updated_member.description == "Nova descrição do membro" + assert repo.members[0].description == "Nova descrição do membro" + + + def test_update_member_not_found(self): + repo = MemberRepositoryMock() + member = repo.get_all_member()[0] + + member = Member( + # new member_id + name=member.name, + member_function=member.member_function, + linkedin=member.linkedin, + member_photo=member.member_photo, + description="Nova descrição do membro" + ) + + with pytest.raises(NoItemsFound): + repo.update_member(member) + + + def test_delete_member(self): + repo = MemberRepositoryMock() + member_id = repo.get_all_member()[1].member_id + member = repo.delete_member(member_id) + + assert member.name == "Nome do Membro Dois" + assert len(repo.members) == 1 + + + def test_delete_member_not_found(self): + repo = MemberRepositoryMock() + with pytest.raises(NoItemsFound): + repo.delete_member(uuid.uuid4()) + From b8848effcf4e98280ac5de4a8737c793966c4520 Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Tue, 18 Aug 2026 10:19:18 -0300 Subject: [PATCH 24/50] refactor: altera GetUserController e GetUserUsecase para usar UUID, simplifica GetUserViewmodel --- src/modules/get_user/app/get_user_controller.py | 7 ++----- src/modules/get_user/app/get_user_usecase.py | 5 ++--- src/modules/get_user/app/get_user_viewmodel.py | 15 ++++----------- 3 files changed, 8 insertions(+), 19 deletions(-) diff --git a/src/modules/get_user/app/get_user_controller.py b/src/modules/get_user/app/get_user_controller.py index 392869e..47e057c 100644 --- a/src/modules/get_user/app/get_user_controller.py +++ b/src/modules/get_user/app/get_user_controller.py @@ -1,3 +1,4 @@ +from uuid import UUID from src.shared.infra.external.observability.observability_aws import ObservabilityAWS from .get_user_usecase import GetUserUsecase from .get_user_viewmodel import GetUserViewmodel @@ -29,13 +30,9 @@ def __call__(self, request: IRequest) -> IResponse: fieldTypeReceived=request.data.get('user_id').__class__.__name__ ) - if not request.data.get('user_id').isdecimal(): - raise EntityError("user_id") - user = self.GetUserUsecase( - user_id=int(request.data.get('user_id')) - ) + user_id=UUID(request.data.get('user_id'))) viewmodel = GetUserViewmodel(user) diff --git a/src/modules/get_user/app/get_user_usecase.py b/src/modules/get_user/app/get_user_usecase.py index 4377d1d..bcb496f 100644 --- a/src/modules/get_user/app/get_user_usecase.py +++ b/src/modules/get_user/app/get_user_usecase.py @@ -1,3 +1,4 @@ +from uuid import UUID from src.shared.domain.entities.user import User from src.shared.domain.repositories.user_repository_interface import IUserRepository from src.shared.helpers.errors.domain_errors import EntityError @@ -9,10 +10,8 @@ def __init__(self, repo: IUserRepository, observability: ObservabilityAWS): self.repo = repo self.observability = observability - def __call__(self, user_id: int) -> User: + def __call__(self, user_id: UUID) -> User: self.observability.log_usecase_in() - if type(user_id) != int: - raise EntityError("user_id") user = self.repo.get_user(user_id) self.observability.log_usecase_out() return user diff --git a/src/modules/get_user/app/get_user_viewmodel.py b/src/modules/get_user/app/get_user_viewmodel.py index bb4be7f..1ce34aa 100644 --- a/src/modules/get_user/app/get_user_viewmodel.py +++ b/src/modules/get_user/app/get_user_viewmodel.py @@ -9,16 +9,9 @@ class GetUserViewmodel: state: STATE def __init__(self, user: User): - self.user_id = user.user_id - self.name = user.name - self.email = user.email - self.state = user.state + self.user = user def to_dict(self): - return { - 'user_id': self.user_id, - 'name': self.name, - 'email': self.email, - 'state': self.state.value, - 'message': "the user was retrieved successfully" - } + data = self.user.model_dump(mode='j') + data.update({'message': "the user was retrieved successfully"}) + return data From 033e2bcb50eab729794d2ddf3f2f4f530f76637b Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Thu, 20 Aug 2026 15:16:46 -0300 Subject: [PATCH 25/50] refactor: adiciona tratamento de erro para user_id e adequa testes da rota Get --- src/modules/get_user/app/get_user_controller.py | 6 +++++- src/modules/get_user/app/get_user_viewmodel.py | 2 +- .../get_user/app/test_get_user_controller.py | 15 +++++++-------- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/modules/get_user/app/get_user_controller.py b/src/modules/get_user/app/get_user_controller.py index 47e057c..d0ef4e5 100644 --- a/src/modules/get_user/app/get_user_controller.py +++ b/src/modules/get_user/app/get_user_controller.py @@ -30,9 +30,13 @@ def __call__(self, request: IRequest) -> IResponse: fieldTypeReceived=request.data.get('user_id').__class__.__name__ ) + try: + user_id = UUID(request.data.get('user_id')) + except: + raise EntityError('user_id') user = self.GetUserUsecase( - user_id=UUID(request.data.get('user_id'))) + user_id=user_id) viewmodel = GetUserViewmodel(user) diff --git a/src/modules/get_user/app/get_user_viewmodel.py b/src/modules/get_user/app/get_user_viewmodel.py index 1ce34aa..7687fb6 100644 --- a/src/modules/get_user/app/get_user_viewmodel.py +++ b/src/modules/get_user/app/get_user_viewmodel.py @@ -12,6 +12,6 @@ def __init__(self, user: User): self.user = user def to_dict(self): - data = self.user.model_dump(mode='j') + data = self.user.model_dump(mode='json') data.update({'message': "the user was retrieved successfully"}) return data diff --git a/tests/modules/get_user/app/test_get_user_controller.py b/tests/modules/get_user/app/test_get_user_controller.py index 101bc05..509fe2c 100644 --- a/tests/modules/get_user/app/test_get_user_controller.py +++ b/tests/modules/get_user/app/test_get_user_controller.py @@ -1,4 +1,5 @@ -""" from src.modules.get_user.app.get_user_controller import GetUserController +import uuid +from src.modules.get_user.app.get_user_controller import GetUserController from src.modules.get_user.app.get_user_usecase import GetUserUsecase from src.shared.helpers.external_interfaces.http_models import HttpRequest from src.shared.infra.external.observability.observability_mock import ObservabilityMock @@ -19,10 +20,9 @@ def test_get_user_controller(self): response = controller(request=request) assert response.status_code == 200 - assert response.body['user_id'] == repo.users[1].user_id - assert response.body['name'] == repo.users[1].name + assert response.body['user_id'] == str(repo.users[1].user_id) assert response.body['email'] == repo.users[1].email - assert response.body['state'] == repo.users[1].state.value + assert response.body['role'] == repo.users[1].role def test_get_user_controller_missing_parameters(self): repo = UserRepositoryMock() @@ -51,13 +51,13 @@ def test_get_user_contoller_wrong_type_parameter(self): assert response.status_code == 400 assert response.body == "Field user_id isn't in the right type.\n Received: int.\n Expected: str" - def test_get_user_contoller_entity_error(self): + def test_get_user_controller_entity_error(self): repo = UserRepositoryMock() usecase = GetUserUsecase(repo=repo, observability=observability) controller = GetUserController(usecase=usecase, observability=observability) request = HttpRequest(query_params={ - 'user_id': 'abc' + 'user_id': 'a' }) response = controller(request=request) @@ -71,11 +71,10 @@ def test_get_user_controller_no_items_found(self): controller = GetUserController(usecase=usecase, observability=observability) request = HttpRequest(query_params={ - 'user_id': str(999) + 'user_id': str(uuid.uuid4()) }) response = controller(request=request) assert response.status_code == 404 assert response.body == 'No items found for user_id' - """ \ No newline at end of file From b79b2e6243be9405830ad8eb1ee72f0a74a32afd Mon Sep 17 00:00:00 2001 From: Leo Iorio Date: Thu, 20 Aug 2026 22:14:33 -0300 Subject: [PATCH 26/50] =?UTF-8?q?feat(infra):=20adiciona=20template=20Dyna?= =?UTF-8?q?mo=20DTO/repo=20com=20model=5Fdump=20Alinha=20dto=20e=20reposit?= =?UTF-8?q?=C3=B3rio=20ao=20padr=C3=A3o=20do=20template=20(pk/sk,=20GSI=20?= =?UTF-8?q?email)=20e=20atualiza=20o=20loader=20local,=20sem=20alterar=20o?= =?UTF-8?q?=20IAC.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- iac/iac/template_dynamo_table.py | 51 ++--- iac/iac/template_stack.py | 6 +- src/shared/environments.py | 4 +- src/shared/infra/dto/template_dynamo_dto.py | 35 ++++ src/shared/infra/dto/user_dynamo_dto.py | 71 ------- .../repositories/load_user_mock_to_dynamo.py | 191 ++++++++---------- .../template_repository_dynamo.py | 112 ++++++++++ .../repositories/user_repository_dynamo.py | 95 --------- .../infra/dto/test_template_dynamo_dto.py | 28 +++ .../shared/infra/dto/test_user_dynamo_dto.py | 133 ------------ ....py => test_template_repository_dynamo.py} | 31 +-- 11 files changed, 295 insertions(+), 462 deletions(-) create mode 100644 src/shared/infra/dto/template_dynamo_dto.py delete mode 100644 src/shared/infra/dto/user_dynamo_dto.py create mode 100644 src/shared/infra/repositories/template_repository_dynamo.py delete mode 100644 src/shared/infra/repositories/user_repository_dynamo.py create mode 100644 tests/shared/infra/dto/test_template_dynamo_dto.py delete mode 100644 tests/shared/infra/dto/test_user_dynamo_dto.py rename tests/shared/infra/repositories/{test_user_repository_dynamo.py => test_template_repository_dynamo.py} (54%) diff --git a/iac/iac/template_dynamo_table.py b/iac/iac/template_dynamo_table.py index 9b274c9..9bc82dc 100644 --- a/iac/iac/template_dynamo_table.py +++ b/iac/iac/template_dynamo_table.py @@ -1,55 +1,30 @@ +from decimal import Decimal from aws_cdk import ( - RemovalPolicy, - aws_dynamodb as dynamodb, + aws_dynamodb as dynamodb, RemovalPolicy, ) from constructs import Construct -# Mantenha alinhado com src.shared.infra.external.dynamo.dynamo_keys / Environments -_EPEP_TABLE_PREFIX = "EpepTable" -_USER_EMAIL_INDEX_NAME = "UserEmailIndex" - -RETAINED_STAGES = {"prod", "homolog"} - class TemplateDynamoTable(Construct): table: dynamodb.Table - def __init__(self, scope: Construct, construct_id: str, stage: str = "DEV", **kwargs) -> None: + def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) - stage_lower = stage.lower() - removal_policy = ( - RemovalPolicy.RETAIN if stage_lower in RETAINED_STAGES else RemovalPolicy.DESTROY - ) - self.table = dynamodb.Table( - self, - "EpepDynamoTable", + self, "TemplateDynamoTable", partition_key=dynamodb.Attribute( - name="pk", - type=dynamodb.AttributeType.STRING, + name="PK", + type=dynamodb.AttributeType.STRING ), sort_key=dynamodb.Attribute( - name="sk", - type=dynamodb.AttributeType.STRING, - ), + name="SK", + type=dynamodb.AttributeType.STRING + ), billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST, - removal_policy=removal_policy, - table_name=f"{_EPEP_TABLE_PREFIX}-{stage_lower}", - point_in_time_recovery_specification=dynamodb.PointInTimeRecoverySpecification( - point_in_time_recovery_enabled=(stage_lower == "prod") - ), + removal_policy=RemovalPolicy.DESTROY ) + + + - self.table.add_global_secondary_index( - index_name=_USER_EMAIL_INDEX_NAME, - partition_key=dynamodb.Attribute( - name="gsi2pk", - type=dynamodb.AttributeType.STRING, - ), - sort_key=dynamodb.Attribute( - name="gsi2sk", - type=dynamodb.AttributeType.STRING, - ), - projection_type=dynamodb.ProjectionType.ALL, - ) diff --git a/iac/iac/template_stack.py b/iac/iac/template_stack.py index 0556518..dbaf9b4 100644 --- a/iac/iac/template_stack.py +++ b/iac/iac/template_stack.py @@ -34,13 +34,13 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: } ) - self.dynamo_table = TemplateDynamoTable(self, "TemplateDynamoTable", stage="DEV") + self.dynamo_table = TemplateDynamoTable(self, "TemplateDynamoTable") ENVIRONMENT_VARIABLES = { "STAGE": "DEV", "DYNAMO_TABLE_NAME": self.dynamo_table.table.table_name, - "DYNAMO_PARTITION_KEY": "pk", - "DYNAMO_SORT_KEY": "sk", + "DYNAMO_PARTITION_KEY": "PK", + "DYNAMO_SORT_KEY": "SK", "REGION": self.region, } diff --git a/src/shared/environments.py b/src/shared/environments.py index 0dcb61a..77e81be 100644 --- a/src/shared/environments.py +++ b/src/shared/environments.py @@ -67,8 +67,8 @@ def get_user_repo() -> IUserRepository: from src.shared.infra.repositories.user_repository_mock import UserRepositoryMock return UserRepositoryMock elif Environments.get_envs().stage in [STAGE.DEV, STAGE.HOMOLOG, STAGE.PROD]: - from src.shared.infra.repositories.user_repository_dynamo import UserRepositoryDynamo - return UserRepositoryDynamo + from src.shared.infra.repositories.template_repository_dynamo import TemplateRepositoryDynamo + return TemplateRepositoryDynamo else: raise Exception("No repository found for this stage") diff --git a/src/shared/infra/dto/template_dynamo_dto.py b/src/shared/infra/dto/template_dynamo_dto.py new file mode 100644 index 0000000..ae2247d --- /dev/null +++ b/src/shared/infra/dto/template_dynamo_dto.py @@ -0,0 +1,35 @@ +from src.shared.domain.entities.user import User +from src.shared.infra.external.dynamo.dynamo_keys import ( + EntityKind, + build_gsi2_attributes, + partition_key, + sort_key, + strip_keys, +) + + +class UserDynamoDTO: + """Template DTO: serializa entidade via model_dump + keys da single-table.""" + + @staticmethod + def from_entity_to_dynamo(user: User) -> dict: + """ + Converts a User entity to a dictionary compatible with DynamoDB. + + Includes base keys (pk/sk) and GSI2 email attributes. + """ + return { + **user.model_dump(mode="json"), + "pk": partition_key(kind=EntityKind.USER), + "sk": sort_key(id=user.user_id, kind=EntityKind.USER), + **build_gsi2_attributes(user_email=user.email, user_id=user.user_id), + } + + @staticmethod + def from_dynamo_to_entity(user_data: dict) -> User: + """ + Converts a DynamoDB item dict into a User entity. + + Storage keys (pk/sk/gsi) are stripped before model_validate. + """ + return User.model_validate(obj=strip_keys(user_data)) diff --git a/src/shared/infra/dto/user_dynamo_dto.py b/src/shared/infra/dto/user_dynamo_dto.py deleted file mode 100644 index fee2c45..0000000 --- a/src/shared/infra/dto/user_dynamo_dto.py +++ /dev/null @@ -1,71 +0,0 @@ -from decimal import Decimal - -from src.shared.domain.entities.user import User -from src.shared.domain.enums.state_enum import STATE - - -class UserDynamoDTO: - name: str - email: str - state: STATE - user_id: int - - def __init__(self, name: str, email: str, state: STATE, user_id: int): - self.name = name - self.email = email - self.user_id = user_id - self.state = state - - @staticmethod - def from_entity(user: User) -> "UserDynamoDTO": - """ - Parse data from User to UserDynamoDTO - """ - return UserDynamoDTO( - name=user.name, - email=user.email, - user_id=user.user_id, - state=user.state - ) - - def to_dynamo(self) -> dict: - """ - Parse data from UserDynamoDTO to dict - """ - return { - "entity": "user", - "name": self.name, - "email": self.email, - "user_id": Decimal(self.user_id), - "state": self.state.value - } - - @staticmethod - def from_dynamo(user_data: dict) -> "UserDynamoDTO": - """ - Parse data from DynamoDB to UserDynamoDTO - @param user_data: dict from DynamoDB - """ - return UserDynamoDTO( - name=user_data["name"], - email=user_data["email"], - user_id=int(user_data["user_id"]), - state=STATE(user_data["state"]) - ) - - def to_entity(self) -> User: - """ - Parse data from UserDynamoDTO to User - """ - return User( - name=self.name, - email=self.email, - user_id=self.user_id, - state=self.state - ) - - def __repr__(self): - return f"UserDynamoDto(name={self.name}, email={self.email}, user_id={self.user_id}, state={self.state})" - - def __eq__(self, other): - return self.__dict__ == other.__dict__ diff --git a/src/shared/infra/repositories/load_user_mock_to_dynamo.py b/src/shared/infra/repositories/load_user_mock_to_dynamo.py index 2ad0852..ee5f8cf 100644 --- a/src/shared/infra/repositories/load_user_mock_to_dynamo.py +++ b/src/shared/infra/repositories/load_user_mock_to_dynamo.py @@ -1,141 +1,122 @@ -from decimal import Decimal +import argparse import boto3 import dotenv -from src.shared.infra.repositories.user_repository_dynamo import UserRepositoryDynamo -from src.shared.infra.repositories.user_repository_mock import UserRepositoryMock -from src.shared.environments import Environments + +from src.shared.environments import Environments, STAGE +from src.shared.helpers.errors.usecase_errors import DuplicatedUser from src.shared.infra.external.dynamo.dynamo_keys import ( GSI2_NAME, GSI2_PK_ATTR, GSI2_SK_ATTR, ) +from src.shared.infra.repositories.template_repository_dynamo import TemplateRepositoryDynamo +from src.shared.infra.repositories.user_repository_mock import UserRepositoryMock def setup_dynamo_table(): envs = Environments.get_envs() - dynamo_table_name = envs.dynamo_table_name + table_name = envs.dynamo_table_name endpoint_url = envs.dynamo_endpoint_url pk = envs.dynamo_partition_key sk = envs.dynamo_sort_key - print("Setting up DynamoDB table...") - dynamo_client = boto3.client('dynamodb', endpoint_url=endpoint_url) - print("DynamoDB client created") - tables = dynamo_client.list_tables()['TableNames'] - - if dynamo_table_name not in tables: - print("Creating table...") - dynamo_client.create_table( - TableName=dynamo_table_name, - KeySchema=[ - { - 'AttributeName': pk, - 'KeyType': 'HASH' - }, - { - 'AttributeName': sk, - 'KeyType': 'RANGE' - } - ], - AttributeDefinitions=[ - { - 'AttributeName': pk, - 'AttributeType': 'S' - }, - { - 'AttributeName': sk, - 'AttributeType': 'S' - }, - { - 'AttributeName': GSI2_PK_ATTR, - 'AttributeType': 'S' - }, - { - 'AttributeName': GSI2_SK_ATTR, - 'AttributeType': 'S' - }, - ], - GlobalSecondaryIndexes=[ - { - "IndexName": GSI2_NAME, - "KeySchema": [ - {"AttributeName": GSI2_PK_ATTR, "KeyType": "HASH"}, - {"AttributeName": GSI2_SK_ATTR, "KeyType": "RANGE"}, - ], - "Projection": {"ProjectionType": "ALL"}, - }, - ], - BillingMode='PAY_PER_REQUEST', - ) - print("Waiting for table to be created...") - dynamo_client.get_waiter('table_exists').wait(TableName=dynamo_table_name) - - print('Loading table...') + print("Setting up DynamoDB table...") + dynamo_client = boto3.client("dynamodb", endpoint_url=endpoint_url) + tables = dynamo_client.list_tables()["TableNames"] - dynamodb = boto3.resource('dynamodb', endpoint_url=endpoint_url) + if table_name in tables: + print("Table already exists!") + return + + print("Creating table...") + dynamo_client.create_table( + TableName=table_name, + KeySchema=[ + {"AttributeName": pk, "KeyType": "HASH"}, + {"AttributeName": sk, "KeyType": "RANGE"}, + ], + AttributeDefinitions=[ + {"AttributeName": pk, "AttributeType": "S"}, + {"AttributeName": sk, "AttributeType": "S"}, + {"AttributeName": GSI2_PK_ATTR, "AttributeType": "S"}, + {"AttributeName": GSI2_SK_ATTR, "AttributeType": "S"}, + ], + GlobalSecondaryIndexes=[ + { + "IndexName": GSI2_NAME, + "KeySchema": [ + {"AttributeName": GSI2_PK_ATTR, "KeyType": "HASH"}, + {"AttributeName": GSI2_SK_ATTR, "KeyType": "RANGE"}, + ], + "Projection": {"ProjectionType": "ALL"}, + }, + ], + BillingMode="PAY_PER_REQUEST", + ) - table = dynamodb.Table(dynamo_table_name) + print("Waiting for table to be created...") + dynamo_client.get_waiter("table_exists").wait(TableName=table_name) + print(f'Table "{table_name}" created!') - print("Adding counter to table") - table.put_item( - Item={ - pk: 'COUNTER', - sk: 'COUNTER', - 'COUNTER': Decimal(0) - } - ) +def _load_users(dynamo_repo: TemplateRepositoryDynamo) -> int: + mock_repo = UserRepositoryMock() + count = 0 - print(f'Table "{dynamo_table_name}" created!') + print("Loading mock data to dynamo...") + for user in mock_repo.users: + print(f"Loading user {user.user_id} | {user.email} to dynamo") + try: + dynamo_repo.create_user(user) + count += 1 + except DuplicatedUser: + print(f" user {user.user_id} already exists, skipping") - else: - print("Table already exists!") + print(f"{count} users loaded to dynamo!") + return count def load_mock_to_local_dynamo(): + """Create local table (if needed) and seed DynamoDB Local.""" setup_dynamo_table() - mock_repo = UserRepositoryMock() - dynamo_repo = UserRepositoryDynamo() + _load_users(TemplateRepositoryDynamo()) - count = 0 - - print('Loading mock data to dynamo...') - for user in mock_repo.users: - print(f"Loading user {user.user_id} | {user.name} to dynamo") - dynamo_repo.create_user(user) - count += 1 - - print(f"{count} users loaded to dynamo!") def load_mock_to_real_dynamo(): - mock_repo = UserRepositoryMock() - dynamo_repo = UserRepositoryDynamo() - - count = 0 + """ + Seed an already-deployed AWS table. + Run manually after `cdk deploy` on DEV/HOMOLOG. + Does not create the table — CDK owns that. + Blocks PROD by default. + """ envs = Environments.get_envs() - dynamodb = boto3.resource('dynamodb') - table = dynamodb.Table(envs.dynamo_table_name) - - print("Adding counter to table") + if envs.stage == STAGE.PROD: + raise RuntimeError( + "Refusing to seed PROD. Use DEV or HOMOLOG (or override intentionally)." + ) - table.put_item( - Item={ - envs.dynamo_partition_key: 'COUNTER', - envs.dynamo_sort_key: 'COUNTER', - 'COUNTER': Decimal(0) - } + print( + f"Seeding AWS DynamoDB " + f"(stage={envs.stage.value}, table={envs.dynamo_table_name}, region={envs.region})" ) + _load_users(TemplateRepositoryDynamo()) - print('Loading mock data to dynamo...') - for user in mock_repo.users: - print(f"Loading user {user.user_id} | {user.name} to dynamo") - dynamo_repo.create_user(user) - count += 1 - print(f"{count} users loaded to dynamo!") - -if __name__ == '__main__': +if __name__ == "__main__": dotenv.load_dotenv() - load_mock_to_local_dynamo() + + parser = argparse.ArgumentParser(description="Seed User mock data into DynamoDB") + parser.add_argument( + "--target", + choices=["local", "aws"], + default="local", + help="local = DynamoDB Local (creates table). aws = deployed table (DEV/HOMOLOG only).", + ) + args = parser.parse_args() + + if args.target == "aws": + load_mock_to_real_dynamo() + else: + load_mock_to_local_dynamo() diff --git a/src/shared/infra/repositories/template_repository_dynamo.py b/src/shared/infra/repositories/template_repository_dynamo.py new file mode 100644 index 0000000..5b67e49 --- /dev/null +++ b/src/shared/infra/repositories/template_repository_dynamo.py @@ -0,0 +1,112 @@ +from typing import List +from uuid import UUID + +from boto3.dynamodb.conditions import Key + +from src.shared.domain.entities.user import User +from src.shared.domain.repositories.user_repository_interface import IUserRepository +from src.shared.environments import Environments +from src.shared.helpers.errors.usecase_errors import DuplicatedUser, NoItemsFound +from src.shared.infra.dto.template_dynamo_dto import UserDynamoDTO +from src.shared.infra.external.dynamo.datasources.dynamo_datasource import DynamoDatasource +from src.shared.infra.external.dynamo.dynamo_keys import ( + EntityKind, + GSI2_NAME, + GSI2_PK_ATTR, + PK_ATTR, + gsi2_partition_key, + partition_key, + sort_key, +) + + +class TemplateRepositoryDynamo(IUserRepository): + """Template de repositório Dynamo (single-table) para a entidade User.""" + + def __init__(self): + envs = Environments.get_envs() + self.dynamo = DynamoDatasource( + dynamo_table_name=envs.dynamo_table_name, + region=envs.region, + partition_key=envs.dynamo_partition_key, + sort_key=envs.dynamo_sort_key, + endpoint_url=envs.dynamo_endpoint_url, + ) + + def _pk(self) -> str: + return partition_key(kind=EntityKind.USER) + + def _sk(self, user_id: UUID) -> str: + return sort_key(id=user_id, kind=EntityKind.USER) + + def get_user(self, user_id: UUID) -> User: + resp = self.dynamo.get_item( + partition_key=self._pk(), + sort_key=self._sk(user_id), + ) + + if resp.get("Item") is None: + raise NoItemsFound("user_id") + + return UserDynamoDTO.from_dynamo_to_entity(resp["Item"]) + + def get_all_user(self) -> List[User]: + resp = self.dynamo.query( + key_condition_expression=Key(PK_ATTR).eq(self._pk()), + ) + + return [ + UserDynamoDTO.from_dynamo_to_entity(user) + for user in resp.get("Items", []) + ] + + def create_user(self, new_user: User) -> User: + existing = self.dynamo.get_item( + partition_key=self._pk(), + sort_key=self._sk(new_user.user_id), + ) + if existing.get("Item") is not None: + raise DuplicatedUser("user_id") + + email_resp = self.dynamo.query( + key_condition_expression=Key(GSI2_PK_ATTR).eq( + gsi2_partition_key(new_user.email) + ), + IndexName=GSI2_NAME, + Select="COUNT", + ) + if email_resp.get("Count", 0) > 0: + raise DuplicatedUser("email") + + self.dynamo.put_item( + item=UserDynamoDTO.from_entity_to_dynamo(new_user), + partition_key=self._pk(), + sort_key=self._sk(new_user.user_id), + ) + return new_user + + def delete_user(self, user_id: UUID) -> User: + resp = self.dynamo.delete_item( + partition_key=self._pk(), + sort_key=self._sk(user_id), + ) + + if "Attributes" not in resp: + raise NoItemsFound("user_id") + + return UserDynamoDTO.from_dynamo_to_entity(resp["Attributes"]) + + def update_user(self, user: User) -> User: + existing = self.dynamo.get_item( + partition_key=self._pk(), + sort_key=self._sk(user.user_id), + ) + if existing.get("Item") is None: + raise NoItemsFound("user_id") + + self.dynamo.put_item( + item=UserDynamoDTO.from_entity_to_dynamo(user), + partition_key=self._pk(), + sort_key=self._sk(user.user_id), + ) + return user diff --git a/src/shared/infra/repositories/user_repository_dynamo.py b/src/shared/infra/repositories/user_repository_dynamo.py deleted file mode 100644 index 17408c7..0000000 --- a/src/shared/infra/repositories/user_repository_dynamo.py +++ /dev/null @@ -1,95 +0,0 @@ -from decimal import Decimal -from typing import List - -from src.shared.domain.entities.user import User -from src.shared.domain.repositories.user_repository_interface import IUserRepository -from src.shared.environments import Environments -from src.shared.helpers.errors.usecase_errors import NoItemsFound -from src.shared.infra.dto.user_dynamo_dto import UserDynamoDTO -from src.shared.infra.external.dynamo.datasources.dynamo_datasource import DynamoDatasource - - -class UserRepositoryDynamo(IUserRepository): - - @staticmethod - def partition_key_format(user_id) -> str: - return f"user#{user_id}" - - @staticmethod - def sort_key_format(user_id: int) -> str: - return f"#{user_id}" - - def __init__(self): - envs = Environments.get_envs() - self.dynamo = DynamoDatasource( - endpoint_url=envs.dynamo_endpoint_url, - dynamo_table_name=envs.dynamo_table_name, - region=envs.region, - partition_key=envs.dynamo_partition_key, - sort_key=envs.dynamo_sort_key, - ) - def get_user(self, user_id: int) -> User: - resp = self.dynamo.get_item(partition_key=self.partition_key_format(user_id), sort_key=self.sort_key_format(user_id)) - - if resp.get('Item') is None: - raise NoItemsFound("user_id") - - user_dto = UserDynamoDTO.from_dynamo(resp["Item"]) - return user_dto.to_entity() - - def get_all_user(self) -> List[User]: - resp = self.dynamo.get_all_items() - users = [] - for item in resp['Items']: - if item.get("entity") == 'user': - users.append(UserDynamoDTO.from_dynamo(item).to_entity()) - - return users - - - def create_user(self, new_user: User) -> User: - print(f"repo entered.\n Repo:{self}") - print(self.dynamo.dynamo_table.__dict__) - new_user.user_id = self.get_user_counter() - print(f"nre user id: {new_user.user_id}") - user_dto = UserDynamoDTO.from_entity(user=new_user) - resp = self.dynamo.put_item(partition_key=self.partition_key_format(new_user.user_id), - sort_key=self.sort_key_format(user_id=new_user.user_id), item=user_dto.to_dynamo(), - is_decimal=True) - return new_user - - def delete_user(self, user_id: int) -> User: - resp = self.dynamo.delete_item(partition_key=self.partition_key_format(user_id), sort_key=self.sort_key_format(user_id)) - - if "Attributes" not in resp: - raise NoItemsFound("user_id") - - return UserDynamoDTO.from_dynamo(resp['Attributes']).to_entity() - - def update_user(self, user_id: int, new_name: str) -> User: - - user = self.get_user(user_id=user_id) - - item_to_update = {} - - if new_name: - item_to_update['name'] = new_name - else: - raise NoItemsFound("Nothing to update") - - resp = self.dynamo.update_item(partition_key=self.partition_key_format(user_id), sort_key=self.sort_key_format(user_id), update_dict=item_to_update) - - return UserDynamoDTO.from_dynamo(resp['Attributes']).to_entity() - - def get_user_counter(self) -> int: - - return self.update_counter() - - def update_counter(self) -> int: #TODO fix this - print("updating counter") - counter = int(self.dynamo.get_item(partition_key='COUNTER', sort_key='COUNTER')['Item']['COUNTER']) - print(f"counter: {counter}") - resp = self.dynamo.update_item(partition_key='COUNTER', sort_key='COUNTER', update_dict={'COUNTER': Decimal(counter+1)}) - print(f"resp: {resp}") - - return int(resp['Attributes']['COUNTER']) diff --git a/tests/shared/infra/dto/test_template_dynamo_dto.py b/tests/shared/infra/dto/test_template_dynamo_dto.py new file mode 100644 index 0000000..a7a5cc2 --- /dev/null +++ b/tests/shared/infra/dto/test_template_dynamo_dto.py @@ -0,0 +1,28 @@ +from src.shared.domain.enums.role_enum import ROLE +from src.shared.infra.dto.template_dynamo_dto import UserDynamoDTO +from src.shared.infra.repositories.user_repository_mock import UserRepositoryMock + + +class Test_UserDynamoDTO: + def test_from_entity_to_dynamo(self): + user = UserRepositoryMock().users[0] + data = UserDynamoDTO.from_entity_to_dynamo(user) + + assert data["pk"] == "USER" + assert data["sk"] == f"USER#{user.user_id}" + assert data["gsi2pk"] == f"EMAIL#{user.email}" + assert data["gsi2sk"] == f"USER#{user.user_id}" + assert data["email"] == user.email + assert data["role"] == ROLE.ADMIN.value + assert data["password_hash"] == user.password_hash + assert "user_id" in data + + def test_from_dynamo_to_entity_roundtrip(self): + user = UserRepositoryMock().users[1] + dynamo = UserDynamoDTO.from_entity_to_dynamo(user) + restored = UserDynamoDTO.from_dynamo_to_entity(dynamo) + + assert restored.user_id == user.user_id + assert restored.email == user.email + assert restored.role == ROLE.USER + assert restored.password_hash == user.password_hash diff --git a/tests/shared/infra/dto/test_user_dynamo_dto.py b/tests/shared/infra/dto/test_user_dynamo_dto.py deleted file mode 100644 index 8fa83b0..0000000 --- a/tests/shared/infra/dto/test_user_dynamo_dto.py +++ /dev/null @@ -1,133 +0,0 @@ -""" from decimal import Decimal - -from src.shared.domain.entities.user import User -from src.shared.domain.enums.state_enum import STATE -from src.shared.infra.dto.user_dynamo_dto import UserDynamoDTO -from src.shared.infra.repositories.user_repository_mock import UserRepositoryMock - - -class Test_UserDynamoDto: - def test_from_entity(self): - repo = UserRepositoryMock() - - user_dto = UserDynamoDTO.from_entity(user=repo.users[0]) - - expected_selfie_dto = UserDynamoDTO( - name=repo.users[0].name, - email=repo.users[0].email, - user_id=repo.users[0].user_id, - state=repo.users[0].state - ) - - assert user_dto == expected_selfie_dto - - def test_to_dynamo(self): - repo = UserRepositoryMock() - - user_dto = UserDynamoDTO( - name=repo.users[0].name, - email=repo.users[0].email, - user_id=repo.users[0].user_id, - state=repo.users[0].state - ) - - user_dynamo = user_dto.to_dynamo() - - expected_dict = { - "entity": "user", - "name": repo.users[0].name, - "email": repo.users[0].email, - "user_id": repo.users[0].user_id, - "state": repo.users[0].state.value - } - - assert user_dto.to_dynamo() == expected_dict - - def test_from_dynamo(self): - dynamo_dict = {'Item': {'user_id': Decimal('1'), - 'name': 'Bruno Soller', - 'SK': '#1', - 'state': 'APPROVED', - 'PK': 'user#1', - 'entity': 'user', - 'email': 'soller@soller.com'}, - 'ResponseMetadata': {'RequestId': 'aa6a5e5e-943f-4452-8c1f-4e5441ee6042', - 'HTTPStatusCode': 200, - 'HTTPHeaders': {'date': 'Fri, 16 Dec 2022 15:40:29 GMT', - 'content-type': 'application/x-amz-json-1.0', - 'x-amz-crc32': '3909675734', - 'x-amzn-requestid': 'aa6a5e5e-943f-4452-8c1f-4e5441ee6042', - 'content-length': '174', - 'server': 'Jetty(9.4.48.v20220622)'}, - 'RetryAttempts': 0}} - - user_dto = UserDynamoDTO.from_dynamo(user_data=dynamo_dict["Item"]) - - expected_user_dto = UserDynamoDTO( - name="Bruno Soller", - email='soller@soller.com', - user_id=1, - state=STATE.APPROVED - ) - - assert user_dto == expected_user_dto - - def test_to_entity(self): - repo = UserRepositoryMock() - - user_dto = UserDynamoDTO( - name=repo.users[0].name, - email=repo.users[0].email, - user_id=repo.users[0].user_id, - state=repo.users[0].state - ) - - user = user_dto.to_entity() - - assert user.name == repo.users[0].name - assert user.email == repo.users[0].email - assert user.user_id == repo.users[0].user_id - assert user.state == repo.users[0].state - - def test_from_dynamo_to_entity(self): - dynamo_item = {'Item': {'user_id': Decimal('1'), - 'name': 'Bruno Soller', - 'SK': '#1', - 'state': 'APPROVED', - 'PK': 'user#1', - 'entity': 'user', - 'email': 'soller@soller.com'}} - - user_dto = UserDynamoDTO.from_dynamo(user_data=dynamo_item["Item"]) - - user = user_dto.to_entity() - - expected_user = User( - name="Bruno Soller", - email="soller@soller.com", - user_id=1, - state=STATE.APPROVED - ) - - assert user.name == expected_user.name - assert user.email == expected_user.email - assert user.user_id == expected_user.user_id - assert user.state == expected_user.state - - def test_from_entity_to_dynamo(self): - repo = UserRepositoryMock() - - user_dto = UserDynamoDTO.from_entity(user=repo.users[0]) - - user_dynamo = user_dto.to_dynamo() - - expected_dict = { - "entity": "user", - "name": repo.users[0].name, - "email": repo.users[0].email, - "user_id": repo.users[0].user_id, - "state": repo.users[0].state.value - } - - assert user_dynamo == expected_dict - """ \ No newline at end of file diff --git a/tests/shared/infra/repositories/test_user_repository_dynamo.py b/tests/shared/infra/repositories/test_template_repository_dynamo.py similarity index 54% rename from tests/shared/infra/repositories/test_user_repository_dynamo.py rename to tests/shared/infra/repositories/test_template_repository_dynamo.py index e158c18..6ad721a 100644 --- a/tests/shared/infra/repositories/test_user_repository_dynamo.py +++ b/tests/shared/infra/repositories/test_template_repository_dynamo.py @@ -2,46 +2,46 @@ import pytest -from src.shared.infra.repositories.user_repository_dynamo import UserRepositoryDynamo +from src.shared.infra.repositories.template_repository_dynamo import TemplateRepositoryDynamo from src.shared.infra.repositories.user_repository_mock import UserRepositoryMock -class Test_UserRepositoryDynamo: +class Test_TemplateRepositoryDynamo: @pytest.mark.skip(reason="Needs dynamoDB") def test_create_user(self): os.environ["STAGE"] = "TEST" - user_repository = UserRepositoryDynamo() + user_repository = TemplateRepositoryDynamo() user_repository_mock = UserRepositoryMock() resp = user_repository.create_user(user_repository_mock.users[0]) - assert user_repository_mock.users[0].name == resp.name + assert user_repository_mock.users[0].email == resp.email @pytest.mark.skip(reason="Needs dynamoDB") def test_get_user(self): os.environ["STAGE"] = "TEST" - user_repository = UserRepositoryDynamo() + user_repository = TemplateRepositoryDynamo() user_repository_mock = UserRepositoryMock() - resp = user_repository.get_user(1) + resp = user_repository.get_user(user_repository_mock.users[0].user_id) - assert user_repository_mock.users[0].name == resp.name + assert user_repository_mock.users[0].email == resp.email @pytest.mark.skip(reason="Needs dynamoDB") def test_delete_user(self): os.environ["STAGE"] = "TEST" - user_repository = UserRepositoryDynamo() + user_repository = TemplateRepositoryDynamo() user_repository_mock = UserRepositoryMock() - resp = user_repository.delete_user(3) + resp = user_repository.delete_user(user_repository_mock.users[0].user_id) - assert user_repository_mock.users[2].name == resp.name + assert user_repository_mock.users[0].email == resp.email @pytest.mark.skip(reason="Needs dynamoDB") def test_get_all_user(self): os.environ["STAGE"] = "TEST" - user_repository = UserRepositoryDynamo() + user_repository = TemplateRepositoryDynamo() user_repository_mock = UserRepositoryMock() resp = user_repository.get_all_user() @@ -51,9 +51,10 @@ def test_get_all_user(self): def test_update_user(self): os.environ["STAGE"] = "TEST" - user_repository = UserRepositoryDynamo() + user_repository = TemplateRepositoryDynamo() user_repository_mock = UserRepositoryMock() - resp = user_repository.update_user(user_id=1, new_name="Vitor Soller Soller") + updated = user_repository_mock.users[0].model_copy(update={"password_hash": "novo_hash"}) + resp = user_repository.update_user(updated) - assert resp.name == "Vitor Soller Soller" - """ \ No newline at end of file + assert resp.password_hash == "novo_hash" + """ From d51089a07b8e1b0f3361d0f3916f4f61bf246fda Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Fri, 21 Aug 2026 10:24:45 -0300 Subject: [PATCH 27/50] =?UTF-8?q?refactor:=20valida=C3=A7=C3=A3o=20de=20us?= =?UTF-8?q?er=5Fid=20e=20atualiza=C3=A7=C3=A3o=20de=20testes=20para=20uso?= =?UTF-8?q?=20de=20UUID?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/get_user/app/get_user_usecase.py | 2 ++ .../get_user/app/get_user_viewmodel.py | 7 ++----- .../repositories/user_repository_mock.py | 4 ++-- .../get_user/app/test_get_user_presenter.py | 12 +++++------ .../get_user/app/test_get_user_usecase.py | 9 ++++----- .../get_user/app/test_get_user_viewmodel.py | 20 +++++++------------ 6 files changed, 22 insertions(+), 32 deletions(-) diff --git a/src/modules/get_user/app/get_user_usecase.py b/src/modules/get_user/app/get_user_usecase.py index bcb496f..92aeebe 100644 --- a/src/modules/get_user/app/get_user_usecase.py +++ b/src/modules/get_user/app/get_user_usecase.py @@ -12,6 +12,8 @@ def __init__(self, repo: IUserRepository, observability: ObservabilityAWS): def __call__(self, user_id: UUID) -> User: self.observability.log_usecase_in() + if not isinstance(user_id, UUID): + raise EntityError('user_id') user = self.repo.get_user(user_id) self.observability.log_usecase_out() return user diff --git a/src/modules/get_user/app/get_user_viewmodel.py b/src/modules/get_user/app/get_user_viewmodel.py index 7687fb6..9a6fd50 100644 --- a/src/modules/get_user/app/get_user_viewmodel.py +++ b/src/modules/get_user/app/get_user_viewmodel.py @@ -1,12 +1,9 @@ from src.shared.domain.entities.user import User -from src.shared.domain.enums.state_enum import STATE +from src.shared.domain.enums.role_enum import ROLE class GetUserViewmodel: - user_id: int - name: str - email: str - state: STATE + user: User def __init__(self, user: User): self.user = user diff --git a/src/shared/infra/repositories/user_repository_mock.py b/src/shared/infra/repositories/user_repository_mock.py index b113c95..2373225 100644 --- a/src/shared/infra/repositories/user_repository_mock.py +++ b/src/shared/infra/repositories/user_repository_mock.py @@ -12,8 +12,8 @@ class UserRepositoryMock(IUserRepository): def __init__(self): self.users = [ - User(email="admin@example.com", role=ROLE.ADMIN), - User(email="user@example.com", role=ROLE.USER), + User(user_id=uuid.UUID('5b20bcf8-f467-4569-83f2-1744534c162a'), email="admin@example.com", role=ROLE.ADMIN), + User(user_id=uuid.UUID('842faa44-caf7-43bd-8019-d5ae5d3942b2'),email="user@example.com", role=ROLE.USER), ] def get_user(self, user_id: uuid.UUID) -> User: diff --git a/tests/modules/get_user/app/test_get_user_presenter.py b/tests/modules/get_user/app/test_get_user_presenter.py index 5e656c8..0dec7d3 100644 --- a/tests/modules/get_user/app/test_get_user_presenter.py +++ b/tests/modules/get_user/app/test_get_user_presenter.py @@ -1,4 +1,4 @@ -""" import json +import json from dataclasses import dataclass import pytest @@ -22,7 +22,7 @@ def test_get_user(self): "header2": "value1,value2" }, "queryStringParameters": { - "user_id": "1" + "user_id": "5b20bcf8-f467-4569-83f2-1744534c162a" }, "requestContext": { "accountId": "123456789012", @@ -62,8 +62,6 @@ def test_get_user(self): response = lambda_handler(event, None) assert response["statusCode"] == 200 - assert json.loads(response["body"])["name"] == "Bruno Soller" - assert json.loads(response["body"])["email"] == "soller@soller.com" - assert json.loads(response["body"])["state"] == "APPROVED" - assert json.loads(response["body"])["user_id"] == 1 - """ \ No newline at end of file + assert json.loads(response["body"])["email"] == "admin@example.com" + assert json.loads(response["body"])["role"] == "admin" + assert json.loads(response["body"])["user_id"] == '5b20bcf8-f467-4569-83f2-1744534c162a' diff --git a/tests/modules/get_user/app/test_get_user_usecase.py b/tests/modules/get_user/app/test_get_user_usecase.py index 86c75fa..fa40b35 100644 --- a/tests/modules/get_user/app/test_get_user_usecase.py +++ b/tests/modules/get_user/app/test_get_user_usecase.py @@ -1,5 +1,5 @@ -""" import pytest - +import uuid +import pytest from src.modules.get_user.app.get_user_usecase import GetUserUsecase from src.shared.helpers.errors.domain_errors import EntityError from src.shared.helpers.errors.usecase_errors import NoItemsFound @@ -23,12 +23,11 @@ def test_get_user_not_found(self): usecase = GetUserUsecase(repo, observability=observability) with pytest.raises(NoItemsFound): - user = usecase(user_id=999) + user = usecase(user_id=uuid.uuid4()) def test_get_user_invalid_id(self): repo = UserRepositoryMock() usecase = GetUserUsecase(repo, observability=observability) with pytest.raises(EntityError): - user = usecase(user_id="invalid") - """ \ No newline at end of file + user = usecase(user_id=99) \ No newline at end of file diff --git a/tests/modules/get_user/app/test_get_user_viewmodel.py b/tests/modules/get_user/app/test_get_user_viewmodel.py index 848cda9..f655a47 100644 --- a/tests/modules/get_user/app/test_get_user_viewmodel.py +++ b/tests/modules/get_user/app/test_get_user_viewmodel.py @@ -1,23 +1,17 @@ -""" from src.modules.get_user.app.get_user_viewmodel import GetUserViewmodel +from src.modules.get_user.app.get_user_viewmodel import GetUserViewmodel from src.shared.domain.entities.user import User from src.shared.domain.enums.state_enum import STATE class Test_GetUserViewModel: def test_get_user_viewmodel(self): - user = User( - user_id=1, - name="Vitor Soller", - email="vitinho@hype.com", - state=STATE.APPROVED - ) + user = User(email="teste@teste.com", role="admin") + user_id = str(user.user_id) userViewmodel = GetUserViewmodel(user=user).to_dict() - expected = {'user_id': 1, - 'name': 'Vitor Soller', - 'email': 'vitinho@hype.com', - 'state': 'APPROVED', + expected = {'user_id': user_id, + 'email': 'teste@teste.com', + 'role': 'admin', 'message': 'the user was retrieved successfully'} - assert expected == userViewmodel - """ \ No newline at end of file + assert expected == userViewmodel \ No newline at end of file From 99c6542645ddd7c9420d5516761853202a5c4739 Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Mon, 24 Aug 2026 17:54:20 -0300 Subject: [PATCH 28/50] =?UTF-8?q?refactor:=20adiciona=20user=5Fid=20na=20i?= =?UTF-8?q?nicializa=C3=A7=C3=A3o=20dos=20usu=C3=A1rios=20no=20reposit?= =?UTF-8?q?=C3=B3rio=20mock?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/infra/repositories/user_repository_mock.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/shared/infra/repositories/user_repository_mock.py b/src/shared/infra/repositories/user_repository_mock.py index b113c95..e36055c 100644 --- a/src/shared/infra/repositories/user_repository_mock.py +++ b/src/shared/infra/repositories/user_repository_mock.py @@ -12,8 +12,8 @@ class UserRepositoryMock(IUserRepository): def __init__(self): self.users = [ - User(email="admin@example.com", role=ROLE.ADMIN), - User(email="user@example.com", role=ROLE.USER), + User(user_id=uuid.UUID('5b20bcf8-f467-4569-83f2-1744534c162a'), email="admin@example.com", role=ROLE.ADMIN), + User(user_id=uuid.UUID('842faa44-caf7-43bd-8019-d5ae5d3942b2'),email="user@example.com", role=ROLE.USER), ] def get_user(self, user_id: uuid.UUID) -> User: @@ -42,4 +42,4 @@ def update_user(self, user: User) -> User: self.users[idx] = user return self.users[idx] - raise NoItemsFound("user_id") + raise NoItemsFound("user_id") \ No newline at end of file From 731df3280b6472f1082cb002d9db200fae388971 Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Mon, 24 Aug 2026 18:11:52 -0300 Subject: [PATCH 29/50] =?UTF-8?q?refactor:=20altera=20a=20l=C3=B3gica=20de?= =?UTF-8?q?=20exclus=C3=A3o=20de=20usu=C3=A1rio=20para=20usar=20UUID=20e?= =?UTF-8?q?=20atualiza=20os=20testes=20correspondentes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../delete_user/app/delete_user_controller.py | 12 +++++++---- .../delete_user/app/delete_user_usecase.py | 8 ++------ .../delete_user/app/delete_user_viewmodel.py | 10 ++-------- .../app/test_delete_user_controller.py | 18 ++++++++--------- .../app/test_delete_user_presenter.py | 19 +++++++++--------- .../app/test_delete_user_usecase.py | 15 ++++---------- .../app/test_delete_user_viewmodel.py | 20 +++++++++---------- 7 files changed, 45 insertions(+), 57 deletions(-) diff --git a/src/modules/delete_user/app/delete_user_controller.py b/src/modules/delete_user/app/delete_user_controller.py index d69e104..ef3eef4 100644 --- a/src/modules/delete_user/app/delete_user_controller.py +++ b/src/modules/delete_user/app/delete_user_controller.py @@ -1,3 +1,4 @@ +from uuid import UUID from src.shared.helpers.errors.usecase_errors import NoItemsFound from src.shared.helpers.external_interfaces.external_interface import IRequest, IResponse from .delete_user_usecase import DeleteUserUsecase @@ -17,17 +18,20 @@ def __call__(self, request: IRequest) -> IResponse: if request.data.get('user_id') is None: raise MissingParameters('user_id') - if type(request.data.get('user_id')) != str: + if not isinstance(request.data.get('user_id'), str): raise WrongTypeParameter( fieldName="user_id", fieldTypeExpected="str", fieldTypeReceived=request.data.get('user_id').__class__.__name__ ) - if not request.data.get('user_id').isdecimal(): - raise EntityError("user_id") + + try: + user_id = UUID(request.data.get('user_id')) + except: + raise EntityError('user_id') user = self.DeleteUserUsecase( - user_id=int(request.data.get('user_id')) + user_id=user_id ) viewmodel = DeleteUserViewmodel(user=user) diff --git a/src/modules/delete_user/app/delete_user_usecase.py b/src/modules/delete_user/app/delete_user_usecase.py index 2321587..88091fc 100644 --- a/src/modules/delete_user/app/delete_user_usecase.py +++ b/src/modules/delete_user/app/delete_user_usecase.py @@ -1,3 +1,4 @@ +from uuid import UUID from src.shared.domain.entities.user import User from src.shared.domain.repositories.user_repository_interface import IUserRepository from src.shared.helpers.errors.domain_errors import EntityError @@ -7,12 +8,7 @@ class DeleteUserUsecase: def __init__(self, repo: IUserRepository): self.repo = repo - def __call__(self, user_id: int) -> User: - - - if type(user_id) != int: - raise EntityError("user_id") - + def __call__(self, user_id: UUID) -> User: user = self.repo.delete_user(user_id) return user diff --git a/src/modules/delete_user/app/delete_user_viewmodel.py b/src/modules/delete_user/app/delete_user_viewmodel.py index 83b6a6e..cc9879a 100644 --- a/src/modules/delete_user/app/delete_user_viewmodel.py +++ b/src/modules/delete_user/app/delete_user_viewmodel.py @@ -9,16 +9,10 @@ class DeleteUserViewmodel: state: STATE def __init__(self, user: User): - self.user_id = user.user_id - self.name = user.name - self.email = user.email - self.state = user.state + self.data = user.model_dump(mode='json') def to_dict(self): return { - 'user_id': self.user_id, - 'name': self.name, - 'email': self.email, - 'state': self.state.value, + **self.data, 'message': "the user was deleted successfully" } diff --git a/tests/modules/delete_user/app/test_delete_user_controller.py b/tests/modules/delete_user/app/test_delete_user_controller.py index 5e3978d..4c9f246 100644 --- a/tests/modules/delete_user/app/test_delete_user_controller.py +++ b/tests/modules/delete_user/app/test_delete_user_controller.py @@ -1,4 +1,5 @@ -""" from src.modules.delete_user.app.delete_user_controller import DeleteUserController +import uuid +from src.modules.delete_user.app.delete_user_controller import DeleteUserController from src.modules.delete_user.app.delete_user_usecase import DeleteUserUsecase from src.shared.helpers.external_interfaces.http_models import HttpRequest from src.shared.infra.repositories.user_repository_mock import UserRepositoryMock @@ -10,9 +11,11 @@ def test_delete_user_controller(self): usecase = DeleteUserUsecase(repo=repo) controller = DeleteUserController(usecase=usecase) - request = HttpRequest(body={ - 'user_id': '1' - }) + request = HttpRequest( + body={'user_id': str(repo.users[0].user_id)} + ) + + print(request.body) response = controller(request=request) @@ -67,13 +70,10 @@ def test_delete_user_controller_no_items_found(self): controller = DeleteUserController(usecase=usecase) request = HttpRequest(body={ - 'user_id': '69' + 'user_id': str(uuid.uuid4()) }) response = controller(request=request) assert response.status_code == 404 - assert response.body == 'No items found for user_id' - - - """ \ No newline at end of file + assert response.body == 'No items found for user_id' \ No newline at end of file diff --git a/tests/modules/delete_user/app/test_delete_user_presenter.py b/tests/modules/delete_user/app/test_delete_user_presenter.py index f7c38e0..007271b 100644 --- a/tests/modules/delete_user/app/test_delete_user_presenter.py +++ b/tests/modules/delete_user/app/test_delete_user_presenter.py @@ -1,11 +1,12 @@ -""" import json +import json from src.modules.delete_user.app.delete_user_presenter import lambda_handler - +from src.shared.infra.repositories.user_repository_mock import UserRepositoryMock class Test_DeleteUserPresenter: def test_delete_user(self): + repo = UserRepositoryMock() event = { "version": "2.0", "routeKey": "$default", @@ -52,7 +53,7 @@ def test_delete_user(self): "time": "12/Mar/2020:19:03:58 +0000", "timeEpoch": 1583348638390 }, - "body": '{"user_id": "1"}', + "body": '{"user_id": "5b20bcf8-f467-4569-83f2-1744534c162a"}', "pathParameters": None, "isBase64Encoded": None, "stageVariables": None @@ -60,12 +61,12 @@ def test_delete_user(self): response = lambda_handler(event, None) - expected = {'user_id': 1, - 'name': 'Bruno Soller', - 'email': 'soller@soller.com', - 'state': 'APPROVED', - 'message': 'the user was deleted successfully'} + expected = { + 'user_id': '5b20bcf8-f467-4569-83f2-1744534c162a', + 'email': 'admin@example.com', + 'role': 'admin', + 'message': 'the user was deleted successfully' + } assert json.loads(response["body"]) == expected assert response["statusCode"] == 200 - """ \ No newline at end of file diff --git a/tests/modules/delete_user/app/test_delete_user_usecase.py b/tests/modules/delete_user/app/test_delete_user_usecase.py index 32bdc8d..c21da51 100644 --- a/tests/modules/delete_user/app/test_delete_user_usecase.py +++ b/tests/modules/delete_user/app/test_delete_user_usecase.py @@ -1,4 +1,5 @@ -""" import pytest +import uuid +import pytest from src.modules.delete_user.app.delete_user_usecase import DeleteUserUsecase from src.shared.helpers.errors.domain_errors import EntityError @@ -13,7 +14,7 @@ def test_delete_user(self): lenBefore = len(repo.users) - user = usecase(1) + user = usecase(uuid.UUID('5b20bcf8-f467-4569-83f2-1744534c162a')) assert len(repo.users) == lenBefore - 1 @@ -22,12 +23,4 @@ def test_delete_user_not_found(self): usecase = DeleteUserUsecase(repo) with pytest.raises(NoItemsFound): - user = usecase(69) - - def test_delete_user_invalid_id(self): - repo = UserRepositoryMock() - usecase = DeleteUserUsecase(repo) - - with pytest.raises(EntityError): - user = usecase("invalid") - """ \ No newline at end of file + user = usecase(uuid.uuid4()) diff --git a/tests/modules/delete_user/app/test_delete_user_viewmodel.py b/tests/modules/delete_user/app/test_delete_user_viewmodel.py index e0bb5ac..a3c19f2 100644 --- a/tests/modules/delete_user/app/test_delete_user_viewmodel.py +++ b/tests/modules/delete_user/app/test_delete_user_viewmodel.py @@ -1,24 +1,24 @@ -""" from src.modules.delete_user.app.delete_user_viewmodel import DeleteUserViewmodel +import uuid +from src.modules.delete_user.app.delete_user_viewmodel import DeleteUserViewmodel from src.shared.domain.entities.user import User -from src.shared.domain.enums.state_enum import STATE +from src.shared.domain.enums.role_enum import ROLE class Test_DeleteUserViewmodel: def test_delete_user_viewmodel(self): + user_id = uuid.uuid4() user = User( - user_id=1, - name="Vitinho da Silva", + user_id = user_id, email="21.01444-2@maua.br", - state=STATE.APPROVED) + role=ROLE.ADMIN + ) delete_user_viewmodel = DeleteUserViewmodel(user) expected = { - 'user_id': 1, - 'name': 'Vitinho da Silva', + 'user_id': str(user_id), 'email': '21.01444-2@maua.br', - 'state': 'APPROVED', + 'role': 'admin', 'message': 'the user was deleted successfully'} - assert expected == delete_user_viewmodel.to_dict() - """ \ No newline at end of file + assert expected == delete_user_viewmodel.to_dict() \ No newline at end of file From 846688231802cdbdf6b8836d22da16fcce73ac52 Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Mon, 24 Aug 2026 18:33:10 -0300 Subject: [PATCH 30/50] refactor: atualiza a estrutura do UserViewmodel e ajusta testes para refletir novas propriedades --- .../get_all_users/app/get_all_users_viewmodel.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/modules/get_all_users/app/get_all_users_viewmodel.py b/src/modules/get_all_users/app/get_all_users_viewmodel.py index 32ff93c..192774d 100644 --- a/src/modules/get_all_users/app/get_all_users_viewmodel.py +++ b/src/modules/get_all_users/app/get_all_users_viewmodel.py @@ -5,18 +5,10 @@ class UserViewmodel: def __init__(self, user: User): - self.state = user.state - self.email = user.email - self.name = user.name - self.user_id = user.user_id + self.user = user def to_dict(self): - return { - 'user_id': self.user_id, - 'name': self.name, - 'email': self.email, - 'state': self.state.value - } + return self.user.model_dump(mode='json') class GetAllUsersViewmodel: From 20823cbd6bd566a1c7094a16351f6ddeb9a6b73d Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Mon, 24 Aug 2026 18:33:19 -0300 Subject: [PATCH 31/50] =?UTF-8?q?refactor:=20ajusta=20importa=C3=A7=C3=B5e?= =?UTF-8?q?s=20e=20atualiza=20dados=20de=20usu=C3=A1rios=20nos=20testes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/test_get_all_users_controller.py | 3 +- .../app/test_get_all_users_presenter.py | 23 +++------- .../app/test_get_all_users_usecase.py | 3 +- .../app/test_get_all_users_viewmodel.py | 43 ++++++++----------- 4 files changed, 26 insertions(+), 46 deletions(-) diff --git a/tests/modules/get_all_users/app/test_get_all_users_controller.py b/tests/modules/get_all_users/app/test_get_all_users_controller.py index c97d059..4c6f70d 100644 --- a/tests/modules/get_all_users/app/test_get_all_users_controller.py +++ b/tests/modules/get_all_users/app/test_get_all_users_controller.py @@ -1,4 +1,4 @@ -""" from src.modules.get_all_users.app.get_all_users_controller import GetAllUsersController +from src.modules.get_all_users.app.get_all_users_controller import GetAllUsersController from src.modules.get_all_users.app.get_all_users_usecase import GetAllUsersUsecase from src.shared.infra.repositories.user_repository_mock import UserRepositoryMock @@ -14,4 +14,3 @@ def test_get_all_users_controller(self): assert response.status_code == 200 - """ \ No newline at end of file diff --git a/tests/modules/get_all_users/app/test_get_all_users_presenter.py b/tests/modules/get_all_users/app/test_get_all_users_presenter.py index d48d60b..aae6275 100644 --- a/tests/modules/get_all_users/app/test_get_all_users_presenter.py +++ b/tests/modules/get_all_users/app/test_get_all_users_presenter.py @@ -1,4 +1,4 @@ -""" import json +import json from src.modules.get_all_users.app.get_all_users_presenter import lambda_handler @@ -60,23 +60,15 @@ def test_get_all_users_presenter(self): expected_body = { "all_users": [ { - 'user_id': 1, - 'name': "Bruno Soller", - 'email': "soller@soller.com", - 'state': 'APPROVED', + 'user_id': '5b20bcf8-f467-4569-83f2-1744534c162a', + 'email': "admin@example.com", + 'role': 'admin', }, { - 'user_id': 2, - 'name': "Vitor Brancas", - 'email': "brancas@brancas.com", - 'state': 'REJECTED', + 'user_id': '842faa44-caf7-43bd-8019-d5ae5d3942b2', + 'email': "user@example.com", + 'role': 'user', }, - { - 'user_id': 3, - 'name': "João Vilas", - 'email': "bruno@bruno.com", - 'state': 'PENDING', - } ], "message": "all users has been retrieved" } @@ -85,4 +77,3 @@ def test_get_all_users_presenter(self): assert response["statusCode"] == 200 assert json.loads(response["body"]) == expected_body - """ \ No newline at end of file diff --git a/tests/modules/get_all_users/app/test_get_all_users_usecase.py b/tests/modules/get_all_users/app/test_get_all_users_usecase.py index bee9f26..ea08022 100644 --- a/tests/modules/get_all_users/app/test_get_all_users_usecase.py +++ b/tests/modules/get_all_users/app/test_get_all_users_usecase.py @@ -1,4 +1,4 @@ -""" from src.modules.get_all_users.app.get_all_users_usecase import GetAllUsersUsecase +from src.modules.get_all_users.app.get_all_users_usecase import GetAllUsersUsecase from src.shared.infra.repositories.user_repository_mock import UserRepositoryMock @@ -13,4 +13,3 @@ def test_get_all_users_usecase(self): assert all_users_list_returned == repo_mock.users assert len(all_users_list_returned) == len(repo_mock.users) - """ \ No newline at end of file diff --git a/tests/modules/get_all_users/app/test_get_all_users_viewmodel.py b/tests/modules/get_all_users/app/test_get_all_users_viewmodel.py index 0e16eef..b8125e8 100644 --- a/tests/modules/get_all_users/app/test_get_all_users_viewmodel.py +++ b/tests/modules/get_all_users/app/test_get_all_users_viewmodel.py @@ -1,19 +1,16 @@ -""" from src.modules.get_all_users.app.get_all_users_viewmodel import GetAllUsersViewmodel, UserViewmodel +from src.modules.get_all_users.app.get_all_users_viewmodel import GetAllUsersViewmodel, UserViewmodel from src.shared.domain.entities.user import User -from src.shared.domain.enums.state_enum import STATE - +from src.shared.domain.enums.role_enum import ROLE class Test_GetAllUsersViewmodel: all_users_list = [ - User(user_id=1, - name="Lucas Duez", + User(user_id='842faa44-caf7-43bd-8019-d5ae5d3942b2', email="deuzexmachina@gmail.com", - state=STATE.APPROVED), + role=ROLE.ADMIN), - User(user_id=2, - name="Laura Blablachan", + User(user_id='5b20bcf8-f467-4569-83f2-1744534c162a', email="laurinha@gmail.com", - state=STATE.APPROVED), + role=ROLE.USER), ] def test_get_all_users_viewmodel(self): @@ -22,16 +19,14 @@ def test_get_all_users_viewmodel(self): expected = { "all_users": [ { - 'user_id': 1, - 'name': "Lucas Duez", + 'user_id': '842faa44-caf7-43bd-8019-d5ae5d3942b2', 'email': "deuzexmachina@gmail.com", - 'state': 'APPROVED', + 'role': 'admin', }, { - 'user_id': 2, - 'name': "Laura Blablachan", + 'user_id': '5b20bcf8-f467-4569-83f2-1744534c162a', 'email': "laurinha@gmail.com", - 'state': 'APPROVED', + 'role': 'user', } ], "message": "all users has been retrieved" @@ -43,23 +38,19 @@ def test_get_all_users_viewmodel(self): def test_user_viewmodel(self): viewmodel = UserViewmodel( - User(user_id=2, - name="Laura Blablachan", + User(user_id='5b20bcf8-f467-4569-83f2-1744534c162a', email="laurinha@gmail.com", - state=STATE.APPROVED), + role=ROLE.USER), ) response = viewmodel.to_dict() - expected = { - 'user_id': 2, - 'name': "Laura Blablachan", - 'email': "laurinha@gmail.com", - 'state': 'APPROVED', - } + expected ={ + 'user_id': '5b20bcf8-f467-4569-83f2-1744534c162a', + 'email': "laurinha@gmail.com", + 'role': 'user', + } assert response == expected - - """ \ No newline at end of file From b52246c3763cd7b8257e9984c692b79613b82366 Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Mon, 24 Aug 2026 19:57:32 -0300 Subject: [PATCH 32/50] =?UTF-8?q?refactor:=20atualiza=20l=C3=B3gica=20de?= =?UTF-8?q?=20atualiza=C3=A7=C3=A3o=20de=20usu=C3=A1rio=20e=20melhora=20o?= =?UTF-8?q?=20modelo=20de=20visualiza=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../update_user/app/update_user_controller.py | 14 +++++++++++--- .../update_user/app/update_user_usecase.py | 18 +++++++++--------- .../update_user/app/update_user_viewmodel.py | 16 ++++------------ 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/modules/update_user/app/update_user_controller.py b/src/modules/update_user/app/update_user_controller.py index 5a2e703..22d0c9e 100644 --- a/src/modules/update_user/app/update_user_controller.py +++ b/src/modules/update_user/app/update_user_controller.py @@ -1,3 +1,4 @@ +from uuid import UUID from src.shared.helpers.external_interfaces.external_interface import IResponse, IRequest from .update_user_usecase import UpdateUserUsecase from .update_user_viewmodel import UpdateUserViewmodel @@ -16,8 +17,6 @@ def __call__(self, request: IRequest) -> IResponse: try: if request.data.get('user_id') is None: raise MissingParameters('user_id') - if request.data.get('new_name') is None: - raise MissingParameters('new_name') if type(request.data.get('user_id')) != str: raise WrongTypeParameter( @@ -26,7 +25,16 @@ def __call__(self, request: IRequest) -> IResponse: fieldTypeReceived=request.data.get('user_id').__class__.__name__ ) - user = self.UpdateUserUsecase(user_id=int(request.data.get('user_id')), new_name=request.data.get('new_name')) + try: + user_id = UUID(request.data.get('user_id')) + except ValueError: + EntityError('user_id') + + user = self.UpdateUserUsecase( + user_id=user_id, + new_email=request.data.get('new_email'), + new_role=request.data.get('new_role') + ) viewmodel = UpdateUserViewmodel(user=user) diff --git a/src/modules/update_user/app/update_user_usecase.py b/src/modules/update_user/app/update_user_usecase.py index 1c5511b..c41041b 100644 --- a/src/modules/update_user/app/update_user_usecase.py +++ b/src/modules/update_user/app/update_user_usecase.py @@ -1,3 +1,4 @@ +from uuid import UUID from src.shared.domain.entities.user import User from src.shared.domain.repositories.user_repository_interface import IUserRepository from src.shared.helpers.errors.domain_errors import EntityError @@ -7,14 +8,13 @@ class UpdateUserUsecase: def __init__(self, repo: IUserRepository): self.repo = repo - def __call__(self, user_id: int, new_name: str) -> User: - - if type(user_id) != int: - raise EntityError("user_id") - - if type(new_name) != str: - raise EntityError("new_name") - - updated_user = self.repo.update_user(user_id=user_id, new_name=new_name) + def __call__(self, user_id: UUID, **user_atributes) -> User: + stored_user = self.repo.get_user(user_id=user_id) + user = User( + user_id=user_id, + email=user_atributes.get('new_email') or stored_user.email, + role=user_atributes.get('new_role') or stored_user.role + ) + updated_user = self.repo.update_user(user=user) return updated_user diff --git a/src/modules/update_user/app/update_user_viewmodel.py b/src/modules/update_user/app/update_user_viewmodel.py index 85bcc49..0717425 100644 --- a/src/modules/update_user/app/update_user_viewmodel.py +++ b/src/modules/update_user/app/update_user_viewmodel.py @@ -9,17 +9,9 @@ class UpdateUserViewmodel: state: STATE def __init__(self, user: User): - self.user_id = user.user_id - self.name = user.name - self.email = user.email - self.state = user.state + self.user = user def to_dict(self): - return { - 'user_id': self.user_id, - 'name': self.name, - 'email': self.email, - 'state': self.state.value, - 'message': "the user was updated successfully" - } - + data = self.user.model_dump(mode='json') + data.update({'message': "the user was updated successfully"}) + return data From f63a09aca655c4db55fb66fa0d4694fc15e4f3d7 Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Mon, 24 Aug 2026 19:59:53 -0300 Subject: [PATCH 33/50] =?UTF-8?q?refactor:=20atualiza=20testes=20de=20atua?= =?UTF-8?q?liza=C3=A7=C3=A3o=20de=20usu=C3=A1rio=20para=20incluir=20novos?= =?UTF-8?q?=20campos=20e=20valida=C3=A7=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/test_update_user_controller.py | 38 ++--- .../app/test_update_user_presenter.py | 132 +++++++++++++++++- .../app/test_update_user_usecase.py | 29 ++-- .../app/test_update_user_viewmodel.py | 17 ++- 4 files changed, 166 insertions(+), 50 deletions(-) diff --git a/tests/modules/update_user/app/test_update_user_controller.py b/tests/modules/update_user/app/test_update_user_controller.py index bfc3789..8c257ad 100644 --- a/tests/modules/update_user/app/test_update_user_controller.py +++ b/tests/modules/update_user/app/test_update_user_controller.py @@ -1,27 +1,29 @@ -""" from src.modules.update_user.app.update_user_controller import UpdateUserController +import uuid +from src.modules.update_user.app.update_user_controller import UpdateUserController from src.modules.update_user.app.update_user_usecase import UpdateUserUsecase from src.shared.helpers.external_interfaces.http_models import HttpRequest from src.shared.infra.repositories.user_repository_mock import UserRepositoryMock class Test_UpdateUserController: + def test_update_user_controller(self): repo = UserRepositoryMock() usecase = UpdateUserUsecase(repo=repo) controller = UpdateUserController(usecase=usecase) request = HttpRequest(body={ - 'user_id': "1", - 'new_name': 'Branco do Branco Branco da Silva' + 'user_id': '5b20bcf8-f467-4569-83f2-1744534c162a', + 'new_email': 'new_admin@epep.com', + 'new_role': 'user' }) response = controller(request=request) assert response.status_code == 200 - assert response.body['user_id'] == repo.users[0].user_id - assert response.body['name'] == 'Branco do Branco Branco da Silva' - assert response.body['email'] == repo.users[0].email - assert response.body['state'] == repo.users[0].state.value + assert response.body['user_id'] == str(repo.users[0].user_id) + assert response.body['email'] == 'new_admin@epep.com' + assert response.body['role'] == repo.users[0].role assert response.body['message'] == "the user was updated successfully" def test_update_user_controller_missing_user_id(self): @@ -30,7 +32,7 @@ def test_update_user_controller_missing_user_id(self): controller = UpdateUserController(usecase=usecase) request = HttpRequest(body={ - 'new_name': 'Branco do Branco Branco da Silva' + 'new_email': 'new_user@epep.com' }) response = controller(request=request) @@ -38,19 +40,6 @@ def test_update_user_controller_missing_user_id(self): assert response.status_code == 400 assert response.body == "Field user_id is missing" - def test_update_user_controller_missing_new_name(self): - repo = UserRepositoryMock() - usecase = UpdateUserUsecase(repo=repo) - controller = UpdateUserController(usecase=usecase) - - request = HttpRequest(body={ - 'user_id': "1" - }) - - response = controller(request=request) - - assert response.status_code == 400 - assert response.body == "Field new_name is missing" def test_update_user_controller_invalid_user_id(self): repo = UserRepositoryMock() @@ -59,7 +48,7 @@ def test_update_user_controller_invalid_user_id(self): request = HttpRequest(body={ 'user_id': 3, - 'new_name': 'Branco do Branco Branco da Silva' + 'new_email': 'user@epep.com' }) response = controller(request=request) @@ -73,12 +62,11 @@ def test_update_user_not_found(self): controller = UpdateUserController(usecase=usecase) request = HttpRequest(body={ - 'user_id': "69", - 'new_name': 'Branco do Branco Branco da Silva' + 'user_id': str(uuid.uuid4()), + 'new_role': 'user' }) response = controller(request=request) assert response.status_code == 404 assert response.body == 'No items found for user_id' - """ \ No newline at end of file diff --git a/tests/modules/update_user/app/test_update_user_presenter.py b/tests/modules/update_user/app/test_update_user_presenter.py index baad578..a22341e 100644 --- a/tests/modules/update_user/app/test_update_user_presenter.py +++ b/tests/modules/update_user/app/test_update_user_presenter.py @@ -1,11 +1,11 @@ -""" import json +import json from src.modules.update_user.app.update_user_presenter import lambda_handler class Test_UpdateUserPresenter: - def test_update_user(self): + def test_update_user_email(self): event = { "version": "2.0", "routeKey": "$default", @@ -52,7 +52,7 @@ def test_update_user(self): "time": "12/Mar/2020:19:03:58 +0000", "timeEpoch": 1583348638390 }, - "body": '{"user_id": "1", "new_name": "João Soller"}', + "body": '{"user_id": "5b20bcf8-f467-4569-83f2-1744534c162a", "new_email": "admin@epep.com"}', "pathParameters": None, "isBase64Encoded": None, "stageVariables": None @@ -62,5 +62,127 @@ def test_update_user(self): assert response["statusCode"] == 200 - assert json.loads(response["body"])['name'] == 'João Soller' - """ \ No newline at end of file + assert json.loads(response["body"])['email'] == 'admin@epep.com' + + + def test_update_user_role(self): + event = { + "version": "2.0", + "routeKey": "$default", + "rawPath": "/my/path", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "header1": "value1", + "header2": "value1,value2" + }, + "queryStringParameters": { + "parameter1": "1" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "", + "authentication": None, + "authorizer": { + "iam": { + "accessKey": "AKIA...", + "accountId": "111122223333", + "callerId": "AIDA...", + "cognitoIdentity": None, + "principalOrgId": None, + "userArn": "arn:aws:iam::111122223333:user/example-user", + "userId": "AIDA..." + } + }, + "domainName": ".lambda-url.us-west-2.on.aws", + "domainPrefix": "", + "external_interfaces": { + "method": "POST", + "path": "/my/path", + "protocol": "HTTP/1.1", + "sourceIp": "123.123.123.123", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "body": '{"user_id": "5b20bcf8-f467-4569-83f2-1744534c162a", "new_role": "user"}', + "pathParameters": None, + "isBase64Encoded": None, + "stageVariables": None + } + + response = lambda_handler(event, None) + + + assert response["statusCode"] == 200 + assert json.loads(response["body"])['role'] == 'user' + + + def test_update_user_role_and_email(self): + event = { + "version": "2.0", + "routeKey": "$default", + "rawPath": "/my/path", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "header1": "value1", + "header2": "value1,value2" + }, + "queryStringParameters": { + "parameter1": "1" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "", + "authentication": None, + "authorizer": { + "iam": { + "accessKey": "AKIA...", + "accountId": "111122223333", + "callerId": "AIDA...", + "cognitoIdentity": None, + "principalOrgId": None, + "userArn": "arn:aws:iam::111122223333:user/example-user", + "userId": "AIDA..." + } + }, + "domainName": ".lambda-url.us-west-2.on.aws", + "domainPrefix": "", + "external_interfaces": { + "method": "POST", + "path": "/my/path", + "protocol": "HTTP/1.1", + "sourceIp": "123.123.123.123", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "body": '{"user_id": "5b20bcf8-f467-4569-83f2-1744534c162a", "new_email": "admin@epep.com", "new_role": "user"}', + "pathParameters": None, + "isBase64Encoded": None, + "stageVariables": None + } + + response = lambda_handler(event, None) + + + assert response["statusCode"] == 200 + assert json.loads(response["body"])['email'] == 'admin@epep.com' + assert json.loads(response["body"])['role'] == 'user' + + diff --git a/tests/modules/update_user/app/test_update_user_usecase.py b/tests/modules/update_user/app/test_update_user_usecase.py index df58e56..67f627a 100644 --- a/tests/modules/update_user/app/test_update_user_usecase.py +++ b/tests/modules/update_user/app/test_update_user_usecase.py @@ -1,30 +1,37 @@ -""" import pytest - +import pytest +from uuid import UUID from src.modules.update_user.app.update_user_usecase import UpdateUserUsecase from src.shared.helpers.errors.domain_errors import EntityError from src.shared.infra.repositories.user_repository_mock import UserRepositoryMock class Test_UpdateUserUsecase: - def test_update_user_usecase(selfs): + def test_update_user_usecase(self): repo = UserRepositoryMock() usecase = UpdateUserUsecase(repo=repo) - updated_user = usecase(user_id=1, new_name="Bruno Guirão MPNTM") + updated_user = usecase( + user_id=UUID('5b20bcf8-f467-4569-83f2-1744534c162a'), + new_email="admin@epep.com", + new_role='user' + ) + + assert updated_user.email == "admin@epep.com" + assert updated_user.role == "user" - assert updated_user.name == "Bruno Guirão MPNTM" - def test_update_user_usecase_wrong_user_id(selfs): + def test_update_user_usecase_wrong_new_email(self): repo = UserRepositoryMock() usecase = UpdateUserUsecase(repo=repo) with pytest.raises(EntityError): - usecase(user_id="1", new_name="Bruno Guirão MPNTM") + usecase( + user_id=UUID('5b20bcf8-f467-4569-83f2-1744534c162a'), + new_email=1 + ) - def test_update_user_usecase_wrong_new_name(selfs): + def test_update_user_usecase_wrong_new_role(self): repo = UserRepositoryMock() usecase = UpdateUserUsecase(repo=repo) with pytest.raises(EntityError): - usecase(user_id=1, new_name=1) - - """ \ No newline at end of file + usecase(user_id=UUID('5b20bcf8-f467-4569-83f2-1744534c162a'), new_role='a') diff --git a/tests/modules/update_user/app/test_update_user_viewmodel.py b/tests/modules/update_user/app/test_update_user_viewmodel.py index 167edb0..777a85a 100644 --- a/tests/modules/update_user/app/test_update_user_viewmodel.py +++ b/tests/modules/update_user/app/test_update_user_viewmodel.py @@ -1,21 +1,20 @@ -""" from src.modules.update_user.app.update_user_viewmodel import UpdateUserViewmodel +from src.modules.update_user.app.update_user_viewmodel import UpdateUserViewmodel from src.shared.domain.entities.user import User -from src.shared.domain.enums.state_enum import STATE +from src.shared.domain.enums.role_enum import ROLE class Test_UpadateUserViewmodel: def test_update_user_viewmodel(self): - user = User(user_id=1, name="Test", email="teste@test.com", state=STATE.APPROVED) + + user = User(email="teste@test.com", role=ROLE.ADMIN) - updated_useer_viewmodel = UpdateUserViewmodel(user) + updated_user_viewmodel = UpdateUserViewmodel(user) expected = { - 'user_id': 1, - 'name': "Test", + 'user_id': str(user.user_id), 'email': "teste@test.com", - 'state': "APPROVED", + 'role': "admin", 'message': "the user was updated successfully" } - assert expected == updated_useer_viewmodel.to_dict() - """ \ No newline at end of file + assert expected == updated_user_viewmodel.to_dict() From 2f4bd318ee335d5dc5f05d825c4a912344febe06 Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Tue, 25 Aug 2026 21:15:17 -0300 Subject: [PATCH 34/50] =?UTF-8?q?feat:=20implementa=20UserRepositoryDynamo?= =?UTF-8?q?=20com=20m=C3=A9todos=20para=20obter=20e=20criar=20usu=C3=A1rio?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repositories/user_repository_dynamo.py | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 src/shared/infra/repositories/user_repository_dynamo.py diff --git a/src/shared/infra/repositories/user_repository_dynamo.py b/src/shared/infra/repositories/user_repository_dynamo.py new file mode 100644 index 0000000..ccc8165 --- /dev/null +++ b/src/shared/infra/repositories/user_repository_dynamo.py @@ -0,0 +1,78 @@ +from typing import List +from uuid import UUID + +from src.shared.domain.entities.user import User +from src.shared.domain.repositories.user_repository_interface import IUserRepository +from src.shared.helpers.errors.usecase_errors import DuplicatedUser, NoItemsFound + +from boto3.dynamodb.conditions import Key + +from src.shared.environments import Environments +from ..dto.template_dynamo_dto import UserDynamoDTO +from src.shared.infra.external.dynamo.datasources.dynamo_datasource import DynamoDatasource +from src.shared.infra.external.dynamo.dynamo_keys import ( + EntityKind, + GSI2_NAME, + GSI2_PK_ATTR, + PK_ATTR, + gsi2_partition_key, + partition_key, + sort_key, +) + + +class UserRepositoryDynamo(IUserRepository): + + def __init__(self) -> None: + envs = Environments() + self.dynamo = DynamoDatasource( + dynamo_table_name=envs.dynamo_table_name, + region=envs.region, + partition_key=envs.dynamo_partition_key, + sort_key=envs.dynamo_sort_key, + endpoint_url=envs.dynamo_endpoint_url, + ) + + def _pk(self) -> str: + return partition_key(kind=EntityKind.USER) + + def _sk(self, user_id: UUID) -> str: + return sort_key(id=user_id, kind=EntityKind.USER) + + def get_user(self, user_id: UUID) -> User: + resp = self.dynamo.get_item( + partition_key=self._pk(), + sort_key=self._sk(user_id), + ) + + if resp.get("Item") is None: + raise NoItemsFound("user_id") + + return UserDynamoDTO.from_dynamo_to_entity(resp["Item"]) + + def create_user(self, new_user: User) -> User: + existing = self.dynamo.get_item( + partition_key=self._pk(), + sort_key=self._sk(new_user.user_id), + ) + + if existing.get("Item") is not None: + DuplicatedUser("user_id") + + email_resp = self.dynamo.query( + key_condition_expression=Key(GSI2_PK_ATTR).eq( + gsi2_partition_key(new_user.email) + ), + IndexName=GSI2_NAME, + Select="COUNT", + ) + if email_resp.get("Item", 0 ) > 0: + DuplicatedUser("email") + + self.dynamo.put_item( + item=UserDynamoDTO.from_entity_to_dynamo(new_user), + partition_key=self._pk(), + sort_key=self._sk(new_user.user_id), + ) + + return new_user \ No newline at end of file From ca6b529ce029e87137072a9cbaa48f6381ba3a25 Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Wed, 26 Aug 2026 11:10:37 -0300 Subject: [PATCH 35/50] =?UTF-8?q?feat:=20adiciona=20m=C3=A9todos=20para=20?= =?UTF-8?q?obter=20todos=20os=20usu=C3=A1rios,=20deletar=20e=20atualizar?= =?UTF-8?q?=20usu=C3=A1rios=20no=20UserRepositoryDynamo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repositories/user_repository_dynamo.py | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/shared/infra/repositories/user_repository_dynamo.py b/src/shared/infra/repositories/user_repository_dynamo.py index ccc8165..f924b3d 100644 --- a/src/shared/infra/repositories/user_repository_dynamo.py +++ b/src/shared/infra/repositories/user_repository_dynamo.py @@ -49,6 +49,17 @@ def get_user(self, user_id: UUID) -> User: raise NoItemsFound("user_id") return UserDynamoDTO.from_dynamo_to_entity(resp["Item"]) + + def get_all_user(self) -> List[User]: + resp = self.dynamo.query( + key_condition_expression=Key(PK_ATTR).eq(self._pk()) + ) + + return [ + UserDynamoDTO.from_dynamo_to_entity(user) + for user in resp.get("Items", []) + ] + def create_user(self, new_user: User) -> User: existing = self.dynamo.get_item( @@ -75,4 +86,31 @@ def create_user(self, new_user: User) -> User: sort_key=self._sk(new_user.user_id), ) - return new_user \ No newline at end of file + return new_user + + def delete_user(self, user_id: UUID) -> User: + resp = self.dynamo.delete_item( + partition_key=self._pk(), + sort_key=self._sk(user_id) + ) + + if "Atributes" not in resp: + raise NoItemsFound('user_id') + + return UserDynamoDTO.from_dynamo_to_entity(resp["Atributes"]) + + + def update_user(self, user: User) -> User: + existing = self.dynamo.get_item( + partition_key=self._pk(), + sort_key=self._sk(user.user_id), + ) + if existing.get("Item") is None: + raise NoItemsFound("user_id") + + self.dynamo.put_item( + item=UserDynamoDTO.from_entity_to_dynamo(user), + partition_key=self._pk(), + sort_key=self._sk(user.user_id), + ) + return user \ No newline at end of file From 37cf65d91940fe0fe3158d7eade0188c8067b6fc Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 26 Aug 2026 14:54:52 -0300 Subject: [PATCH 36/50] feat: member_dynamo_dto and test --- src/shared/infra/dto/member_dynamo_dto.py | 48 ++++++ .../infra/dto/test_member_dynamo_dto.py | 148 ++++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 src/shared/infra/dto/member_dynamo_dto.py create mode 100644 tests/shared/infra/dto/test_member_dynamo_dto.py diff --git a/src/shared/infra/dto/member_dynamo_dto.py b/src/shared/infra/dto/member_dynamo_dto.py new file mode 100644 index 0000000..c31fc8d --- /dev/null +++ b/src/shared/infra/dto/member_dynamo_dto.py @@ -0,0 +1,48 @@ +from pydantic import BaseModel, ConfigDict, HttpUrl +import uuid + +from src.shared.domain.entities.member import Member +from src.shared.domain.enums.member_function_enum import MemberFunctionEnum + +class MemberDynamoDTO(BaseModel): + model_config = ConfigDict(use_enum_values=True) + + entity: str = "member" + member_id: uuid.UUID + name: str + member_function: MemberFunctionEnum + linkedin: HttpUrl + member_photo: HttpUrl + description: str + + @staticmethod + def from_entity(member: Member) -> "MemberDynamoDTO": + """ + Parse data from Member to MemberDynamoDTO + """ + return MemberDynamoDTO.model_validate(member, from_attributes=True) + + def to_dynamo(self) -> dict: + """ + Parse data from MemberDynamoDTO to dict + """ + data = self.model_dump() + data["member_id"] = str(data["member_id"]) + data["linkedin"] = str(data["linkedin"]) + data["member_photo"] = str(data["member_photo"]) + + return data + + @staticmethod + def from_dynamo(member_data: dict) -> "MemberDynamoDTO": + """ + Parse data from DynamoDB to MemberDynamoDTO + @param user_data: dict from DynamoDB + """ + return MemberDynamoDTO.model_validate(member_data) + + def to_entity(self) -> Member: + """ + Parse data from MemberDynamoDTO to Member + """ + return Member.model_validate(self, from_attributes=True) \ No newline at end of file diff --git a/tests/shared/infra/dto/test_member_dynamo_dto.py b/tests/shared/infra/dto/test_member_dynamo_dto.py new file mode 100644 index 0000000..61449f0 --- /dev/null +++ b/tests/shared/infra/dto/test_member_dynamo_dto.py @@ -0,0 +1,148 @@ +import uuid + +from src.shared.domain.entities.member import Member +from src.shared.domain.enums.member_function_enum import MemberFunctionEnum +from src.shared.infra.dto.member_dynamo_dto import MemberDynamoDTO +from src.shared.infra.repositories.member_repository_mock import MemberRepositoryMock + +class Test_MemberDynamoDTO: + def test_from_entity(self): + repo = MemberRepositoryMock() + + member_dto = MemberDynamoDTO.from_entity(member=repo.members[0]) + + expected_member_dto = MemberDynamoDTO( + member_id=repo.members[0].member_id, + name=repo.members[0].name, + member_function=repo.members[0].member_function, + linkedin=repo.members[0].linkedin, + member_photo=repo.members[0].member_photo, + description=repo.members[0].description + ) + + assert member_dto == expected_member_dto + + + def test_to_dynamo(self): + repo = MemberRepositoryMock() + + member_dto = MemberDynamoDTO( + member_id=repo.members[0].member_id, + name=repo.members[0].name, + member_function=repo.members[0].member_function, + linkedin=repo.members[0].linkedin, + member_photo=repo.members[0].member_photo, + description=repo.members[0].description + ) + + expected_dict = { + "entity": "member", + "member_id": str(repo.members[0].member_id), + "name": repo.members[0].name, + "member_function": "Marketing", + "linkedin": str(repo.members[0].linkedin), + "member_photo": str(repo.members[0].member_photo), + "description": repo.members[0].description + } + + assert member_dto.to_dynamo() == expected_dict + + + def test_from_dynamo(self): + member_id = uuid.uuid4() + dynamo_dict = { + "entity": "member", + "member_id": str(member_id), + "name": "Nome do Membro Um", + "member_function": "Marketing", + "linkedin": "https://www.linkedin.com/1", + "member_photo": "https://portalinterno.devmaua.com/assets/logo_fake1.png", + "description": "Exemplo de descrição do membro 1" + } + + member_dto = MemberDynamoDTO.from_dynamo(member_data=dynamo_dict) + + expected_member_dto = MemberDynamoDTO( + member_id=member_id, + name="Nome do Membro Um", + member_function="Marketing", + linkedin="https://www.linkedin.com/1", + member_photo="https://portalinterno.devmaua.com/assets/logo_fake1.png", + description="Exemplo de descrição do membro 1" + ) + + assert member_dto == expected_member_dto + + + def test_to_entity(self): + repo = MemberRepositoryMock() + + member_dto = MemberDynamoDTO( + member_id=repo.members[0].member_id, + name=repo.members[0].name, + member_function=repo.members[0].member_function, + linkedin=repo.members[0].linkedin, + member_photo=repo.members[0].member_photo, + description=repo.members[0].description + ) + member = member_dto.to_entity() + + assert member.member_id == repo.members[0].member_id + assert member.name == repo.members[0].name + assert member.member_function == repo.members[0].member_function + assert str(member.linkedin) == str(repo.members[0].linkedin) + assert str(member.member_photo) == str(repo.members[0].member_photo) + assert member.description == repo.members[0].description + + + def test_from_dynamo_to_entity(self): + member_id = uuid.uuid4() + dynamo_item = { + "entity": "member", + "member_id": str(member_id), + "name": "Nome do Membro Um", + "member_function": "Marketing", + "linkedin": "https://www.linkedin.com/1", + "member_photo": "https://portalinterno.devmaua.com/assets/logo_fake1.png", + "description": "Exemplo de descrição do membro 1" + } + + member_dto = MemberDynamoDTO.from_dynamo(member_data=dynamo_item) + + member = member_dto.to_entity() + + expected_member = Member( + member_id=member_id, + name="Nome do Membro Um", + member_function="Marketing", + linkedin="https://www.linkedin.com/1", + member_photo="https://portalinterno.devmaua.com/assets/logo_fake1.png", + description="Exemplo de descrição do membro 1" + ) + + assert member.member_id == expected_member.member_id + assert member.name == expected_member.name + assert member.member_function == expected_member.member_function + assert str(member.linkedin) == str(expected_member.linkedin) + assert str(member.member_photo) == str(expected_member.member_photo) + assert member.description == expected_member.description + + + def test_from_entity_to_dynamo(self): + repo = MemberRepositoryMock() + + member_dto = MemberDynamoDTO.from_entity(member=repo.members[0]) + + member_dynamo = member_dto.to_dynamo() + + expected_dict = { + "entity": "member", + "member_id": str(repo.members[0].member_id), + "name": repo.members[0].name, + "member_function": "Marketing", + "linkedin": str(repo.members[0].linkedin), + "member_photo": str(repo.members[0].member_photo), + "description": repo.members[0].description + } + + assert member_dynamo == expected_dict From 7bb359187850a7603ce5bce7f5372a236debcfe6 Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Thu, 27 Aug 2026 20:31:19 -0300 Subject: [PATCH 37/50] =?UTF-8?q?feat:=20atualiza=20UserRepositoryDynamo?= =?UTF-8?q?=20para=20usar=20m=C3=A9todo=20get=5Fenvs=20e=20corrige=20verif?= =?UTF-8?q?ica=C3=A7=C3=A3o=20de=20atributos;=20adiciona=20testes=20para?= =?UTF-8?q?=20UserDynamoDTO=20e=20UserRepositoryDynamo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repositories/user_repository_dynamo.py | 10 ++-- .../infra/dto/test_template_dynamo_dto.py | 5 +- .../shared/infra/dto/test_user_dynamo_dto.py | 26 +++++++++ .../test_user_repository_dynamo.py | 58 +++++++++++++++++++ 4 files changed, 91 insertions(+), 8 deletions(-) create mode 100644 tests/shared/infra/dto/test_user_dynamo_dto.py create mode 100644 tests/shared/infra/repositories/test_user_repository_dynamo.py diff --git a/src/shared/infra/repositories/user_repository_dynamo.py b/src/shared/infra/repositories/user_repository_dynamo.py index f924b3d..d00545f 100644 --- a/src/shared/infra/repositories/user_repository_dynamo.py +++ b/src/shared/infra/repositories/user_repository_dynamo.py @@ -24,7 +24,7 @@ class UserRepositoryDynamo(IUserRepository): def __init__(self) -> None: - envs = Environments() + envs = Environments.get_envs() self.dynamo = DynamoDatasource( dynamo_table_name=envs.dynamo_table_name, region=envs.region, @@ -91,13 +91,13 @@ def create_user(self, new_user: User) -> User: def delete_user(self, user_id: UUID) -> User: resp = self.dynamo.delete_item( partition_key=self._pk(), - sort_key=self._sk(user_id) + sort_key=self._sk(user_id), ) - if "Atributes" not in resp: - raise NoItemsFound('user_id') + if "Attributes" not in resp: + raise NoItemsFound("user_id") - return UserDynamoDTO.from_dynamo_to_entity(resp["Atributes"]) + return UserDynamoDTO.from_dynamo_to_entity(resp["Attributes"]) def update_user(self, user: User) -> User: diff --git a/tests/shared/infra/dto/test_template_dynamo_dto.py b/tests/shared/infra/dto/test_template_dynamo_dto.py index a7a5cc2..78904ed 100644 --- a/tests/shared/infra/dto/test_template_dynamo_dto.py +++ b/tests/shared/infra/dto/test_template_dynamo_dto.py @@ -1,4 +1,4 @@ -from src.shared.domain.enums.role_enum import ROLE +""" from src.shared.domain.enums.role_enum import ROLE from src.shared.infra.dto.template_dynamo_dto import UserDynamoDTO from src.shared.infra.repositories.user_repository_mock import UserRepositoryMock @@ -14,7 +14,6 @@ def test_from_entity_to_dynamo(self): assert data["gsi2sk"] == f"USER#{user.user_id}" assert data["email"] == user.email assert data["role"] == ROLE.ADMIN.value - assert data["password_hash"] == user.password_hash assert "user_id" in data def test_from_dynamo_to_entity_roundtrip(self): @@ -25,4 +24,4 @@ def test_from_dynamo_to_entity_roundtrip(self): assert restored.user_id == user.user_id assert restored.email == user.email assert restored.role == ROLE.USER - assert restored.password_hash == user.password_hash + """ \ No newline at end of file diff --git a/tests/shared/infra/dto/test_user_dynamo_dto.py b/tests/shared/infra/dto/test_user_dynamo_dto.py new file mode 100644 index 0000000..3ccf1be --- /dev/null +++ b/tests/shared/infra/dto/test_user_dynamo_dto.py @@ -0,0 +1,26 @@ +from src.shared.domain.enums.role_enum import ROLE +from src.shared.infra.dto.template_dynamo_dto import UserDynamoDTO +from src.shared.infra.repositories.user_repository_mock import UserRepositoryMock + + +class Test_UserDynamoDTO: + def test_from_entity_to_dynamo(self): + user = UserRepositoryMock().users[0] + data = UserDynamoDTO.from_entity_to_dynamo(user) + + assert data["pk"] == "USER" + assert data["sk"] == f"USER#{user.user_id}" + assert data["gsi2pk"] == f"EMAIL#{user.email}" + assert data["gsi2sk"] == f"USER#{user.user_id}" + assert data["email"] == user.email + assert data["role"] == ROLE.ADMIN.value + assert "user_id" in data + + def test_from_dynamo_to_entity_roundtrip(self): + user = UserRepositoryMock().users[1] + dynamo = UserDynamoDTO.from_entity_to_dynamo(user) + restored = UserDynamoDTO.from_dynamo_to_entity(dynamo) + + assert restored.user_id == user.user_id + assert restored.email == user.email + assert restored.role == ROLE.USER diff --git a/tests/shared/infra/repositories/test_user_repository_dynamo.py b/tests/shared/infra/repositories/test_user_repository_dynamo.py new file mode 100644 index 0000000..6d06c8b --- /dev/null +++ b/tests/shared/infra/repositories/test_user_repository_dynamo.py @@ -0,0 +1,58 @@ +import os + +import pytest + +from src.shared.infra.repositories.user_repository_dynamo import UserRepositoryDynamo +from src.shared.infra.repositories.user_repository_mock import UserRepositoryMock + + +class Test_UserRepositoryDynamo: + # @pytest.mark.skip(reason="Needs dynamoDB") + def test_create_user(self): + os.environ["STAGE"] = "TEST" + + user_repository = UserRepositoryDynamo() + user_repository_mock = UserRepositoryMock() + resp = user_repository.create_user(user_repository_mock.users[0]) + + assert user_repository_mock.users[0].email == resp.email + + # @pytest.mark.skip(reason="Needs dynamoDB") + def test_get_user(self): + os.environ["STAGE"] = "TEST" + + user_repository = UserRepositoryDynamo() + user_repository_mock = UserRepositoryMock() + resp = user_repository.get_user(user_repository_mock.users[0].user_id) + + assert user_repository_mock.users[0].email == resp.email + + # @pytest.mark.skip(reason="Needs dynamoDB") + def test_get_all_user(self): + os.environ["STAGE"] = "TEST" + + user_repository = UserRepositoryDynamo() + user_repository_mock = UserRepositoryMock() + resp = user_repository.get_all_user() + assert len(user_repository_mock.users) == len(resp) + + # @pytest.mark.skip(reason="Needs dynamoDB") + def test_update_user(self): + os.environ["STAGE"] = "TEST" + + user_repository = UserRepositoryDynamo() + user_repository_mock = UserRepositoryMock() + updated = user_repository_mock.users[0].model_copy(update={"email": "novo_email@epep.com"}) + resp = user_repository.update_user(updated) + + assert resp.email == "novo_email@epep.com" + + # @pytest.mark.skip(reason="Needs dynamoDB") + def test_delete_user(self): + os.environ["STAGE"] = "TEST" + + user_repository = UserRepositoryDynamo() + user_repository_mock = UserRepositoryMock() + resp = user_repository.delete_user(user_repository_mock.users[0].user_id) + + assert "novo_email@epep.com" == resp.email \ No newline at end of file From c92352953fad1bd66073e6626f619626fd25b539 Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Thu, 27 Aug 2026 20:33:06 -0300 Subject: [PATCH 38/50] =?UTF-8?q?refat:=20corrige=20a=20formata=C3=A7?= =?UTF-8?q?=C3=A3o=20dos=20decorators=20de=20teste=20no=20Test=5FUserRepos?= =?UTF-8?q?itoryDynamo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infra/repositories/test_user_repository_dynamo.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/shared/infra/repositories/test_user_repository_dynamo.py b/tests/shared/infra/repositories/test_user_repository_dynamo.py index 6d06c8b..edb556f 100644 --- a/tests/shared/infra/repositories/test_user_repository_dynamo.py +++ b/tests/shared/infra/repositories/test_user_repository_dynamo.py @@ -7,7 +7,7 @@ class Test_UserRepositoryDynamo: - # @pytest.mark.skip(reason="Needs dynamoDB") + @pytest.mark.skip(reason="Needs dynamoDB") def test_create_user(self): os.environ["STAGE"] = "TEST" @@ -17,7 +17,7 @@ def test_create_user(self): assert user_repository_mock.users[0].email == resp.email - # @pytest.mark.skip(reason="Needs dynamoDB") + @pytest.mark.skip(reason="Needs dynamoDB") def test_get_user(self): os.environ["STAGE"] = "TEST" @@ -27,7 +27,7 @@ def test_get_user(self): assert user_repository_mock.users[0].email == resp.email - # @pytest.mark.skip(reason="Needs dynamoDB") + @pytest.mark.skip(reason="Needs dynamoDB") def test_get_all_user(self): os.environ["STAGE"] = "TEST" @@ -36,7 +36,7 @@ def test_get_all_user(self): resp = user_repository.get_all_user() assert len(user_repository_mock.users) == len(resp) - # @pytest.mark.skip(reason="Needs dynamoDB") + @pytest.mark.skip(reason="Needs dynamoDB") def test_update_user(self): os.environ["STAGE"] = "TEST" @@ -47,7 +47,7 @@ def test_update_user(self): assert resp.email == "novo_email@epep.com" - # @pytest.mark.skip(reason="Needs dynamoDB") + @pytest.mark.skip(reason="Needs dynamoDB") def test_delete_user(self): os.environ["STAGE"] = "TEST" From 0139522ebe21a88bafef477d7dc56bef1c90dc41 Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Mon, 31 Aug 2026 19:10:44 -0300 Subject: [PATCH 39/50] =?UTF-8?q?refat:=20remove=20verifica=C3=A7=C3=A3o?= =?UTF-8?q?=20de=20duplica=C3=A7=C3=A3o=20de=20email=20na=20cria=C3=A7?= =?UTF-8?q?=C3=A3o=20de=20usu=C3=A1rio=20no=20UserRepositoryDynamo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infra/repositories/user_repository_dynamo.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/shared/infra/repositories/user_repository_dynamo.py b/src/shared/infra/repositories/user_repository_dynamo.py index d00545f..995485e 100644 --- a/src/shared/infra/repositories/user_repository_dynamo.py +++ b/src/shared/infra/repositories/user_repository_dynamo.py @@ -70,16 +70,6 @@ def create_user(self, new_user: User) -> User: if existing.get("Item") is not None: DuplicatedUser("user_id") - email_resp = self.dynamo.query( - key_condition_expression=Key(GSI2_PK_ATTR).eq( - gsi2_partition_key(new_user.email) - ), - IndexName=GSI2_NAME, - Select="COUNT", - ) - if email_resp.get("Item", 0 ) > 0: - DuplicatedUser("email") - self.dynamo.put_item( item=UserDynamoDTO.from_entity_to_dynamo(new_user), partition_key=self._pk(), From 49b243c0d8086384e5237fc47f852925906450a8 Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Mon, 31 Aug 2026 19:25:39 -0300 Subject: [PATCH 40/50] =?UTF-8?q?refactor:=20adiciona=20valida=C3=A7=C3=B5?= =?UTF-8?q?es=20de=20tipo=20para=20campos=20de=20email=20e=20papel=20na=20?= =?UTF-8?q?atualiza=C3=A7=C3=A3o=20de=20usu=C3=A1rio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../update_user/app/update_user_controller.py | 16 ++++++++- .../app/test_update_user_controller.py | 34 ++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/modules/update_user/app/update_user_controller.py b/src/modules/update_user/app/update_user_controller.py index 22d0c9e..692beda 100644 --- a/src/modules/update_user/app/update_user_controller.py +++ b/src/modules/update_user/app/update_user_controller.py @@ -18,13 +18,27 @@ def __call__(self, request: IRequest) -> IResponse: if request.data.get('user_id') is None: raise MissingParameters('user_id') - if type(request.data.get('user_id')) != str: + if not isinstance(request.data.get('user_id'), str): raise WrongTypeParameter( fieldName="user_id", fieldTypeExpected="str", fieldTypeReceived=request.data.get('user_id').__class__.__name__ ) + if request.data.get('new_email') is not None and not isinstance(request.data.get('new_email'), str): + raise WrongTypeParameter( + fieldName="new_email", + fieldTypeExpected="str", + fieldTypeReceived=request.data.get('new_email').__class__.__name__ + ) + + if request.data.get('new_role') is not None and not isinstance(request.data.get('new_role'), str): + raise WrongTypeParameter( + fieldName="new_role", + fieldTypeExpected="str", + fieldTypeReceived=request.data.get('new_role').__class__.__name__ + ) + try: user_id = UUID(request.data.get('user_id')) except ValueError: diff --git a/tests/modules/update_user/app/test_update_user_controller.py b/tests/modules/update_user/app/test_update_user_controller.py index 8c257ad..6e0a09d 100644 --- a/tests/modules/update_user/app/test_update_user_controller.py +++ b/tests/modules/update_user/app/test_update_user_controller.py @@ -54,7 +54,37 @@ def test_update_user_controller_invalid_user_id(self): response = controller(request=request) assert response.status_code == 400 - assert response.body == "Field user_id isn't in the right type.\n Received: int.\n Expected: str" + assert response.body == "Field user_id isn't in the right type.\n Received: int.\n Expected: str"\ + + def test_update_user_controller_invalid_email_type(self): + repo = UserRepositoryMock() + usecase = UpdateUserUsecase(repo=repo) + controller = UpdateUserController(usecase=usecase) + + request = HttpRequest(body={ + 'user_id': "5b20bcf8-f467-4569-83f2-1744534c162a", + 'new_email': 1 + }) + + response = controller(request=request) + + assert response.status_code == 400 + assert response.body == "Field new_email isn't in the right type.\n Received: int.\n Expected: str" + + def test_update_user_controller_invalid_role(self): + repo = UserRepositoryMock() + usecase = UpdateUserUsecase(repo=repo) + controller = UpdateUserController(usecase=usecase) + + request = HttpRequest(body={ + 'user_id': "5b20bcf8-f467-4569-83f2-1744534c162a", + 'new_role': 2 + }) + + response = controller(request=request) + + assert response.status_code == 400 + assert response.body == "Field new_role isn't in the right type.\n Received: int.\n Expected: str" def test_update_user_not_found(self): repo = UserRepositoryMock() @@ -70,3 +100,5 @@ def test_update_user_not_found(self): assert response.status_code == 404 assert response.body == 'No items found for user_id' + + \ No newline at end of file From b12547d6b4ba75ccc8821a76396fdcb1c1d7a8e7 Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Mon, 31 Aug 2026 20:11:41 -0300 Subject: [PATCH 41/50] =?UTF-8?q?refat:=20corrige=20mensagem=20de=20erro?= =?UTF-8?q?=20para=20tipo=20de=20usu=C3=A1rio=20inv=C3=A1lido=20nos=20test?= =?UTF-8?q?es=20de=20controle=20de=20exclus=C3=A3o=20e=20obten=C3=A7=C3=A3?= =?UTF-8?q?o=20de=20usu=C3=A1rio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/modules/delete_user/app/test_delete_user_controller.py | 2 +- tests/modules/get_user/app/test_get_user_controller.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/modules/delete_user/app/test_delete_user_controller.py b/tests/modules/delete_user/app/test_delete_user_controller.py index 4c9f246..4ccaf79 100644 --- a/tests/modules/delete_user/app/test_delete_user_controller.py +++ b/tests/modules/delete_user/app/test_delete_user_controller.py @@ -62,7 +62,7 @@ def test_delete_user_controller_invalid_user_id(self): response = controller(request=request) assert response.status_code == 400 - assert response.body == "Field user_id isn't in the right type.\n Received: int.\n Expected: str" + assert response.body == "The field 'user_id' has the wrong type. Received: 'int'. Expected: 'str'." def test_delete_user_controller_no_items_found(self): repo = UserRepositoryMock() diff --git a/tests/modules/get_user/app/test_get_user_controller.py b/tests/modules/get_user/app/test_get_user_controller.py index 509fe2c..a99f18f 100644 --- a/tests/modules/get_user/app/test_get_user_controller.py +++ b/tests/modules/get_user/app/test_get_user_controller.py @@ -49,7 +49,7 @@ def test_get_user_contoller_wrong_type_parameter(self): response = controller(request=request) assert response.status_code == 400 - assert response.body == "Field user_id isn't in the right type.\n Received: int.\n Expected: str" + assert response.body == "The field 'user_id' has the wrong type. Received: 'int'. Expected: 'str'." def test_get_user_controller_entity_error(self): repo = UserRepositoryMock() From 536b8c2150819b67b3901e030c9c486be50d317a Mon Sep 17 00:00:00 2001 From: Leo Iorio Date: Tue, 1 Sep 2026 19:44:26 -0300 Subject: [PATCH 42/50] feat(infra): migra IAC para template v2 com REST API e workflows Substitui o stack legado por constructs (APIGW, Dynamo, Lambda, SSM), layer build e CI/CD com cdk synth. --- .github/scripts/assert_project_configured.sh | 45 +++++++ .github/workflows/CD.yml | 82 +++++++++++- .github/workflows/CI.yml | 10 ++ .github/workflows/cdk_synth_ci.yml | 37 ++++++ iac/.gitignore | 1 + iac/adjust_layer_directory.py | 89 ++++++++----- iac/app.py | 65 ++++++---- iac/cdk.json | 2 +- iac/components/apigw_construct.py | 80 ++++++++++++ iac/components/dynamo_construct.py | 64 ++++++++++ iac/components/lambda_construct.py | 124 +++++++++++++++++++ iac/components/ssm_construct.py | 49 ++++++++ iac/iac/lambda_stack.py | 70 ----------- iac/iac/template_dynamo_table.py | 30 ----- iac/iac/template_stack.py | 55 -------- iac/stack/iac_stack.py | 70 +++++++++++ 16 files changed, 660 insertions(+), 213 deletions(-) create mode 100755 .github/scripts/assert_project_configured.sh create mode 100644 .github/workflows/cdk_synth_ci.yml create mode 100644 iac/components/apigw_construct.py create mode 100644 iac/components/dynamo_construct.py create mode 100644 iac/components/lambda_construct.py create mode 100644 iac/components/ssm_construct.py delete mode 100644 iac/iac/lambda_stack.py delete mode 100644 iac/iac/template_dynamo_table.py delete mode 100644 iac/iac/template_stack.py create mode 100644 iac/stack/iac_stack.py diff --git a/.github/scripts/assert_project_configured.sh b/.github/scripts/assert_project_configured.sh new file mode 100755 index 0000000..e927e1c --- /dev/null +++ b/.github/scripts/assert_project_configured.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Blocks CD deploy while this repo still looks like the clean_mss_template starter. +set -euo pipefail + +REPO_NAME="${GITHUB_REPOSITORY##*/}" + +FORBIDDEN_REPO_NAMES=( + "clean_mss_template" +) + +FORBIDDEN_STACK_PREFIX="CleanMssTemplate" +FORBIDDEN_PROJECT_TAG="Template" + +failed=0 + +for name in "${FORBIDDEN_REPO_NAMES[@]}"; do + if [[ "$REPO_NAME" == "$name" ]]; then + echo "::error::Repository is still named '${name}'. Rename it before enabling CD deploy." + failed=1 + fi +done + +if [[ "${GITHUB_REPOSITORY}" == "Maua-Dev/clean_mss_template" ]]; then + echo "::error::Refusing to deploy the upstream template repository itself." + failed=1 +fi + +if grep -qE "STACK_NAME=${FORBIDDEN_STACK_PREFIX}" .github/workflows/CD.yml; then + echo "::error::Change STACK_NAME prefix in .github/workflows/CD.yml (still '${FORBIDDEN_STACK_PREFIX}')." + failed=1 +fi + +if grep -qE "['\"]project['\"]: ['\"]${FORBIDDEN_PROJECT_TAG}['\"]" iac/app.py; then + echo "::error::Change the project tag in iac/app.py (still '${FORBIDDEN_PROJECT_TAG}')." + failed=1 +fi + +if [[ "$failed" -ne 0 ]]; then + echo "" + echo "CD deploy is blocked until the template identity is customized." + echo "No AWS resources will be created." + exit 1 +fi + +echo "Template identity looks customized. Deploy may proceed." diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index dcac48a..414d0d1 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -1,9 +1,87 @@ - name: CD on: push: branches: - - main + - dev + - homolog + - prod workflow_dispatch: + +jobs: + deployGuard: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Assert project is no longer the unmodified template + run: bash .github/scripts/assert_project_configured.sh + env: + GITHUB_REPOSITORY: ${{ github.repository }} + + testApp: + needs: [deployGuard] + uses: maua-dev/ci_workflows_reusable/.github/workflows/pytest_ci_313.yml@V1.3 + + cdkSynth: + needs: [deployGuard] + uses: ./.github/workflows/cdk_synth_ci.yml + + DeployToAWS: + needs: [deployGuard, testApp, cdkSynth] + environment: + name: ${{ github.ref_name }} + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Set AWS Account ID + run: | + if [[ "${{ github.ref_name }}" == "dev" ]]; then + echo "AWS_ACCOUNT_ID=${{ secrets.AWS_ACCOUNT_ID_DEV }}" >> $GITHUB_ENV + elif [[ "${{ github.ref_name }}" == "homolog" ]]; then + echo "AWS_ACCOUNT_ID=${{ secrets.AWS_ACCOUNT_ID_HOML }}" >> $GITHUB_ENV + elif [[ "${{ github.ref_name }}" == "prod" ]]; then + echo "AWS_ACCOUNT_ID=${{ secrets.AWS_ACCOUNT_ID_PROD }}" >> $GITHUB_ENV + else + echo "Invalid branch name: ${{ github.ref_name }}" && exit 1 + fi + + - name: Setup AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: ${{ vars.AWS_REGION }} + role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/GithubActionsRole + role-session-name: github-action + + - name: Setting stage and stack name + run: | + echo "STACK_NAME=EpepStackBack${{ github.ref_name }}" >> $GITHUB_ENV + + - name: Installing Dependencies + run: | + npm install -g aws-cdk + pip install -r requirements-app.txt + pip install -r iac/requirements-infra.txt + + - name: DeployWithCDK + working-directory: iac + run: | + cdk synth "$STACK_NAME" + cdk deploy "$STACK_NAME" --require-approval never + env: + AWS_REGION: ${{ vars.AWS_REGION }} + AWS_ACCOUNT_ID: ${{ env.AWS_ACCOUNT_ID }} + STACK_NAME: ${{ env.STACK_NAME }} + STAGE: ${{ github.ref_name }} + GITHUB_REF_NAME: ${{ github.ref_name }} + MSS_NAME: ${{ github.event.repository.name }} + GRAPH_MICROSOFT_ENDPOINT: ${{ vars.GRAPH_MICROSOFT_ENDPOINT }} diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 0a63b60..9c52037 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -8,3 +8,13 @@ on: jobs: testApp: uses: maua-dev/ci_workflows_reusable/.github/workflows/pytest_ci_313.yml@V1.3 + + cdkSynth: + if: > + github.event_name != 'pull_request' && + (github.ref_name == 'dev' || + github.ref_name == 'homolog' || + github.ref_name == 'prod' || + startsWith(github.ref_name, 'feature/infra') || + startsWith(github.ref_name, 'infra/')) + uses: ./.github/workflows/cdk_synth_ci.yml diff --git a/.github/workflows/cdk_synth_ci.yml b/.github/workflows/cdk_synth_ci.yml new file mode 100644 index 0000000..9b44e39 --- /dev/null +++ b/.github/workflows/cdk_synth_ci.yml @@ -0,0 +1,37 @@ +name: CDK Synth + +on: + workflow_call: + +jobs: + cdkSynth: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-app.txt + pip install -r iac/requirements-infra.txt + npm install -g aws-cdk + + - name: CDK synth + working-directory: iac + run: cdk synth + env: + AWS_REGION: sa-east-1 + AWS_ACCOUNT_ID: "000000000000" + STACK_NAME: EpepStackBackCi + STAGE: ci + GITHUB_REF_NAME: ci + MSS_NAME: epep_api + GRAPH_MICROSOFT_ENDPOINT: https://graph.microsoft.com/v1.0/me diff --git a/iac/.gitignore b/iac/.gitignore index 53b7da0..1f71dff 100644 --- a/iac/.gitignore +++ b/iac/.gitignore @@ -5,6 +5,7 @@ __pycache__ .venv *.egg-info lambda_layer_out_temp/ +build/ # CDK asset staging directory .cdk.staging diff --git a/iac/adjust_layer_directory.py b/iac/adjust_layer_directory.py index ff845c4..0d817fe 100644 --- a/iac/adjust_layer_directory.py +++ b/iac/adjust_layer_directory.py @@ -1,37 +1,60 @@ import os import shutil +import subprocess from pathlib import Path -import sys -IAC_DIRECTORY_NAME = "iac" -SOURCE_DIRECTORY_NAME = "src" -LAMBDA_LAYER_PREFIX = os.path.join("python", "src") - - -def adjust_layer_directory(shared_dir_name: str, destination: str): - # Get the root directory of the source directory - root_directory = Path(__file__).parent.parent - iac_directory = os.path.join(root_directory, IAC_DIRECTORY_NAME) - - print(f"Root directory: {root_directory}") - print(f"Root direcotry files: {os.listdir(root_directory)}") - print(f"IaC directory: {iac_directory}") - print(f"IaC directory files: {os.listdir(iac_directory)}") - - - # Get the destination and source directory - destination_directory = os.path.join(root_directory, IAC_DIRECTORY_NAME, destination) - source_directory = os.path.join(root_directory, SOURCE_DIRECTORY_NAME, shared_dir_name) - - # Delete the destination directory if it exists - if os.path.exists(destination_directory): - shutil.rmtree(destination_directory) - - # Copy the source directory to the destination directory - shutil.copytree(source_directory, os.path.join(destination_directory, LAMBDA_LAYER_PREFIX, shared_dir_name)) - print( - f"Copying files from {source_directory} to {os.path.join(destination_directory, LAMBDA_LAYER_PREFIX, shared_dir_name)}") - - -if __name__ == '__main__': - adjust_layer_directory(shared_dir_name="shared", destination="lambda_layer_out_temp") \ No newline at end of file +BUILD_DIRECTORY = "build" +PYTHON_TOP_LEVEL_DIR = os.path.join(BUILD_DIRECTORY, "python") +REQUIREMENTS_FILE = "requirements-app.txt" + +PROJECT_ROOT = Path(__file__).parent.parent +SHARED_CODE_SOURCE = os.path.join(PROJECT_ROOT, "src", "shared") + + +def adjust_layer_directory(): + """ + Prepara um diretório 'build' para uma Lambda Layer do AWS CDK. + + A função junta o código local compartilhado e as dependências externas (pip) + na estrutura de pastas que a Lambda espera (/python). + """ + if os.path.exists(BUILD_DIRECTORY): + shutil.rmtree(BUILD_DIRECTORY) + + shared_code_intermediate_dir = os.path.join(PYTHON_TOP_LEVEL_DIR, "src") + os.makedirs(shared_code_intermediate_dir) + + print(f"Copiando código de: {SHARED_CODE_SOURCE}") + shared_code_dest = os.path.join( + shared_code_intermediate_dir, + os.path.basename(SHARED_CODE_SOURCE), + ) + shutil.copytree(SHARED_CODE_SOURCE, shared_code_dest) + + requirements_path = os.path.join(PROJECT_ROOT, REQUIREMENTS_FILE) + if os.path.exists(requirements_path): + subprocess.check_call( + [ + "pip", + "install", + "-r", + requirements_path, + "-t", + PYTHON_TOP_LEVEL_DIR, + "--no-cache-dir", + "--platform", + "manylinux2014_x86_64", + "--python-version", + "3.13", + "--only-binary=:all:", + ] + ) + else: + print( + f"Aviso: Arquivo '{requirements_path}' não encontrado. " + "Nenhuma dependência externa será instalada." + ) + + +if __name__ == "__main__": + adjust_layer_directory() diff --git a/iac/app.py b/iac/app.py index 4b09603..41c741e 100644 --- a/iac/app.py +++ b/iac/app.py @@ -1,17 +1,40 @@ #!/usr/bin/env python3 import os +import re import aws_cdk as cdk -from adjust_layer_directory import adjust_layer_directory - -from iac.template_stack import TemplateStack +from adjust_layer_directory import adjust_layer_directory +from stack.iac_stack import IacStack + +_ALLOWED_STAGES = frozenset({"dev", "homolog", "prod", "ci", "test"}) + + +def resolve_stage() -> str: + """ + STAGE preferencial; GITHUB_REF_NAME só como fallback. + Normaliza para dev|homolog|prod|ci|test sem barras (evita refs de PR tipo N/merge). + """ + raw = (os.environ.get("STAGE") or os.environ.get("GITHUB_REF_NAME") or "dev").strip().lower() + if "/" in raw: + head, _, tail = raw.partition("/") + if head in _ALLOWED_STAGES: + return head + if head == "infra" or head == "feature": + return "ci" + if tail in _ALLOWED_STAGES: + return tail + return "ci" + if raw in _ALLOWED_STAGES: + return raw + sanitized = re.sub(r"[^a-z0-9-]", "-", raw).strip("-") or "ci" + return sanitized if sanitized in _ALLOWED_STAGES else "ci" print("Starting the CDK") print("Adjusting the layer directory") -adjust_layer_directory(shared_dir_name="shared", destination="lambda_layer_out_temp") +adjust_layer_directory() print("Finished adjusting the layer directory") @@ -20,27 +43,25 @@ aws_region = os.environ.get("AWS_REGION") aws_account_id = os.environ.get("AWS_ACCOUNT_ID") stack_name = os.environ.get("STACK_NAME") - -if 'prod' in stack_name: - stage = 'PROD' - -elif 'homolog' in stack_name: - stage = 'HOMOLOG' - -elif 'dev' in stack_name: - stage = 'DEV' - -else: - stage = 'TEST' +stage = resolve_stage() tags = { - 'project': 'Template', - 'stage': stage, - 'stack': 'BACK', - 'owner': 'DevCommunity' + "project": "EpepApi", + "stage": stage, + "stack": stack_name, + "owner": "DevCommunity", } -TemplateStack(app, stack_name=stack_name, env=cdk.Environment(account=aws_account_id, region=aws_region), tags=tags) - +IacStack( + app, + stack_id=stack_name, + stack_name=stack_name, + stage=stage, + env=cdk.Environment( + account=aws_account_id, + region=aws_region, + ), + tags=tags, +) app.synth() diff --git a/iac/cdk.json b/iac/cdk.json index 99841c9..def2d49 100644 --- a/iac/cdk.json +++ b/iac/cdk.json @@ -1,5 +1,5 @@ { - "app": "python app.py", + "app": "python -m app", "watch": { "include": [ "**" diff --git a/iac/components/apigw_construct.py b/iac/components/apigw_construct.py new file mode 100644 index 0000000..623dbca --- /dev/null +++ b/iac/components/apigw_construct.py @@ -0,0 +1,80 @@ +from aws_cdk import aws_apigateway as apigateway +from constructs import Construct +from aws_cdk.aws_apigateway import ( + Cors, + CorsOptions, + GatewayResponse, + Resource, + ResponseType, + RestApi, +) + + +class ApigwConstruct(Construct): + + rest_api: RestApi + api_gateway_resource: Resource + + def __init__( + self, + scope: Construct, + construct_id: str, + stack_name: str, + stage: str, + **kwargs, + ): + super().__init__(scope, construct_id, **kwargs) + + stage_lower = stage.lower() + + cors_options = CorsOptions( + allow_origins=Cors.ALL_ORIGINS, + allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], + allow_headers=Cors.DEFAULT_HEADERS, + ) + + self.rest_api = RestApi( + self, + id="RestApi", + rest_api_name=f"{stack_name}-RestApi-{stage_lower}", + description=f"{stack_name} RestApi for {stage_lower}", + deploy_options=apigateway.StageOptions( + stage_name=stage_lower, + logging_level=apigateway.MethodLoggingLevel.OFF, + data_trace_enabled=False, + metrics_enabled=True, + tracing_enabled=True, + ), + default_cors_preflight_options=cors_options, + ) + + self.api_gateway_resource = self.rest_api.root.add_resource( + path_part="epep-api", + default_cors_preflight_options=cors_options, + ) + + GatewayResponse( + self, + "AuthorizerDenyResponse", + rest_api=self.rest_api, + type=ResponseType.ACCESS_DENIED, + response_headers={ + "Access-Control-Allow-Origin": "'*'", + "Access-Control-Allow-Headers": "'*'", + "Access-Control-Allow-Methods": "'*'", + }, + status_code="403", + ) + + GatewayResponse( + self, + "AuthorizerUnauthorizedResponse", + rest_api=self.rest_api, + type=ResponseType.UNAUTHORIZED, + response_headers={ + "Access-Control-Allow-Origin": "'*'", + "Access-Control-Allow-Headers": "'*'", + "Access-Control-Allow-Methods": "'*'", + }, + status_code="401", + ) diff --git a/iac/components/dynamo_construct.py b/iac/components/dynamo_construct.py new file mode 100644 index 0000000..3040032 --- /dev/null +++ b/iac/components/dynamo_construct.py @@ -0,0 +1,64 @@ +from aws_cdk import ( + RemovalPolicy, + aws_dynamodb as dynamodb, +) +from constructs import Construct + +# Mantenha alinhado com src.shared.infra.external.dynamo.dynamo_keys / Environments +_EPEP_TABLE_PREFIX = "EpepTable" +_USER_EMAIL_INDEX_NAME = "UserEmailIndex" + +RETAINED_STAGES = {"prod", "homolog"} + + +class DynamoConstruct(Construct): + + epep_table: dynamodb.Table + + def __init__( + self, + scope: Construct, + construct_id: str, + stack_name: str, + stage: str, + **kwargs, + ) -> None: + super().__init__(scope, construct_id, **kwargs) + + stage_lower = stage.lower() + + removal_policy = ( + RemovalPolicy.RETAIN if stage_lower in RETAINED_STAGES else RemovalPolicy.DESTROY + ) + + self.epep_table = dynamodb.Table( + self, + id="EpepTable", + partition_key=dynamodb.Attribute( + name="pk", + type=dynamodb.AttributeType.STRING, + ), + sort_key=dynamodb.Attribute( + name="sk", + type=dynamodb.AttributeType.STRING, + ), + billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST, + removal_policy=removal_policy, + table_name=f"{_EPEP_TABLE_PREFIX}-{stage_lower}", + point_in_time_recovery_specification=dynamodb.PointInTimeRecoverySpecification( + point_in_time_recovery_enabled=(stage_lower == "prod") + ), + ) + + self.epep_table.add_global_secondary_index( + index_name=_USER_EMAIL_INDEX_NAME, + partition_key=dynamodb.Attribute( + name="gsi2pk", + type=dynamodb.AttributeType.STRING, + ), + sort_key=dynamodb.Attribute( + name="gsi2sk", + type=dynamodb.AttributeType.STRING, + ), + projection_type=dynamodb.ProjectionType.ALL, + ) diff --git a/iac/components/lambda_construct.py b/iac/components/lambda_construct.py new file mode 100644 index 0000000..bdc79c9 --- /dev/null +++ b/iac/components/lambda_construct.py @@ -0,0 +1,124 @@ +from typing import Optional + +from aws_cdk import ( + aws_apigateway as apigw, + aws_lambda as lambda_, + Duration, +) +from constructs import Construct +from aws_cdk.aws_apigateway import Resource, LambdaIntegration + + +class LambdaConstruct(Construct): + + stage: str + stack_name: str + functions_that_need_dynamo_db_access: list[lambda_.Function] + lambda_layer: lambda_.LayerVersion + + def create_lambda_function( + self, + module_name: str, + environment_variables: dict, + subfolder: str = "", + ) -> lambda_.Function: + code = ( + lambda_.Code.from_asset(f"../src/modules/{subfolder}/{module_name}") + if subfolder + else lambda_.Code.from_asset(f"../src/modules/{module_name}") + ) + return lambda_.Function( + self, + module_name.title().replace("_", ""), + code=code, + handler=f"app.{module_name}_presenter.lambda_handler", + function_name=f"{module_name}-{self.stack_name}-{self.stage}"[:63], + runtime=lambda_.Runtime.PYTHON_3_13, + layers=[self.lambda_layer], + environment=environment_variables, + timeout=Duration.seconds(60), + memory_size=512, + tracing=lambda_.Tracing.ACTIVE, + ) + + def add_method_to_resource( + self, + resource: Resource, + method: str, + function: lambda_.Function, + authorizer: Optional[apigw.IAuthorizer] = None, + api_key_required: bool = False, + ) -> None: + method_options = { + "integration": LambdaIntegration(function), + "api_key_required": api_key_required, + } + if authorizer is not None: + method_options["authorization_type"] = apigw.AuthorizationType.CUSTOM + method_options["authorizer"] = authorizer + else: + method_options["authorization_type"] = apigw.AuthorizationType.NONE + + resource.add_method(method, **method_options) + + def __init__( + self, + scope: Construct, + construct_id: str, + stage: str, + stack_name: str, + api_gateway_resource: Resource, + environment_variables: dict, + **kwargs, + ) -> None: + super().__init__(scope, construct_id, **kwargs) + + self.stage = stage + self.stack_name = stack_name + self.functions_that_need_dynamo_db_access = [] + + self.lambda_layer = lambda_.LayerVersion( + self, + id=f"{stack_name}_LambdaLayer_{stage}", + layer_version_name=f"{stack_name}-LambdaLayer-{self.stage}", + code=lambda_.Code.from_asset("./build"), + compatible_runtimes=[lambda_.Runtime.PYTHON_3_13], + ) + + users_resource = api_gateway_resource.add_resource("users") + user_id_resource = users_resource.add_resource("{user_id}") + + self.create_user = self.create_lambda_function( + module_name="create_user", + environment_variables=environment_variables, + ) + self.add_method_to_resource(users_resource, "POST", self.create_user) + self.functions_that_need_dynamo_db_access.append(self.create_user) + + self.get_all_users = self.create_lambda_function( + module_name="get_all_users", + environment_variables=environment_variables, + ) + self.add_method_to_resource(users_resource, "GET", self.get_all_users) + self.functions_that_need_dynamo_db_access.append(self.get_all_users) + + self.get_user = self.create_lambda_function( + module_name="get_user", + environment_variables=environment_variables, + ) + self.add_method_to_resource(user_id_resource, "GET", self.get_user) + self.functions_that_need_dynamo_db_access.append(self.get_user) + + self.update_user = self.create_lambda_function( + module_name="update_user", + environment_variables=environment_variables, + ) + self.add_method_to_resource(user_id_resource, "PUT", self.update_user) + self.functions_that_need_dynamo_db_access.append(self.update_user) + + self.delete_user = self.create_lambda_function( + module_name="delete_user", + environment_variables=environment_variables, + ) + self.add_method_to_resource(user_id_resource, "DELETE", self.delete_user) + self.functions_that_need_dynamo_db_access.append(self.delete_user) diff --git a/iac/components/ssm_construct.py b/iac/components/ssm_construct.py new file mode 100644 index 0000000..da60bec --- /dev/null +++ b/iac/components/ssm_construct.py @@ -0,0 +1,49 @@ +from constructs import Construct +from aws_cdk import Resource, aws_ssm as ssm +from aws_cdk.aws_apigateway import RestApi +from aws_cdk import aws_s3 as s3 + + +class SsmConstruct(Construct): + + def __init__( + self, + scope: Construct, + construct_id: str, + stage: str, + stack_name: str, + api: RestApi, + api_gateway_resource: Resource, + buckets: dict[str, s3.Bucket] = None, + extra_params: dict[str, str] = None, + **kwargs, + ): + super().__init__(scope, construct_id, **kwargs) + + stage = stage.lower() + path_prefix = stack_name.lower().replace("-", "_") + + if api: + ssm.StringParameter( + self, + id=f"ApiUrl_{stage}", + parameter_name=f"/{path_prefix}/{stage}/api/url", + string_value=f"{api.url}{api_gateway_resource.path.lstrip('/')}/", + ) + + for logical_name, bucket in (buckets or {}).items(): + ssm.StringParameter( + self, + id=f"Bucket_{logical_name}_{stage}", + parameter_name=f"/{path_prefix}/{stage}/buckets/{logical_name}", + string_value=bucket.bucket_name, + ) + + for key, value in (extra_params or {}).items(): + safe_id = key.replace("/", "_") + ssm.StringParameter( + self, + id=f"Extra_{safe_id}_{stage}", + parameter_name=f"/{path_prefix}/{stage}/{key}", + string_value=value, + ) diff --git a/iac/iac/lambda_stack.py b/iac/iac/lambda_stack.py deleted file mode 100644 index 8187d82..0000000 --- a/iac/iac/lambda_stack.py +++ /dev/null @@ -1,70 +0,0 @@ -import os -from aws_cdk import ( - aws_lambda as lambda_, - NestedStack, Duration -) -from constructs import Construct -from aws_cdk.aws_apigateway import Resource, LambdaIntegration - - -class LambdaStack(Construct): - functions_that_need_dynamo_permissions = [] - - def create_lambda_api_gateway_integration(self, module_name: str, method: str, mss_student_api_resource: Resource, - environment_variables: dict = {"STAGE": "TEST"}): - function = lambda_.Function( - self, module_name.title(), - code=lambda_.Code.from_asset(f"../src/modules/{module_name}"), - handler=f"app.{module_name}_presenter.lambda_handler", - runtime=lambda_.Runtime.PYTHON_3_9, - layers=[self.lambda_layer, self.lambda_power_tools], - environment=environment_variables, - timeout=Duration.seconds(15) - ) - - mss_student_api_resource.add_resource(module_name.replace("_", "-")).add_method(method, - integration=LambdaIntegration( - function)) - - return function - - def __init__(self, scope: Construct, api_gateway_resource: Resource, environment_variables: dict) -> None: - super().__init__(scope, "Template_Lambdas") - - self.lambda_layer = lambda_.LayerVersion(self, "Template_Layer", - code=lambda_.Code.from_asset("./lambda_layer_out_temp"), - compatible_runtimes=[lambda_.Runtime.PYTHON_3_9] - ) - - self.lambda_power_tools = lambda_.LayerVersion.from_layer_version_arn(self, "Lambda_Power_Tools", layer_version_arn="arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:22") - - self.get_user_function = self.create_lambda_api_gateway_integration( - module_name="get_user", - method="GET", - mss_student_api_resource=api_gateway_resource, - environment_variables=environment_variables - ) - - self.create_user_function = self.create_lambda_api_gateway_integration( - module_name="create_user", - method="POST", - mss_student_api_resource=api_gateway_resource, - environment_variables=environment_variables - ) - - self.delete_user_function = self.create_lambda_api_gateway_integration( - module_name="delete_user", - method="POST", - mss_student_api_resource=api_gateway_resource, - environment_variables=environment_variables - ) - - self.update_user_function = self.create_lambda_api_gateway_integration( - module_name="update_user", - method="POST", - mss_student_api_resource=api_gateway_resource, - environment_variables=environment_variables - ) - - self.functions_that_need_dynamo_permissions = [self.get_user_function, self.create_user_function, - self.delete_user_function, self.update_user_function] diff --git a/iac/iac/template_dynamo_table.py b/iac/iac/template_dynamo_table.py deleted file mode 100644 index 9bc82dc..0000000 --- a/iac/iac/template_dynamo_table.py +++ /dev/null @@ -1,30 +0,0 @@ -from decimal import Decimal -from aws_cdk import ( - aws_dynamodb as dynamodb, RemovalPolicy, -) -from constructs import Construct - - -class TemplateDynamoTable(Construct): - table: dynamodb.Table - - def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: - super().__init__(scope, construct_id, **kwargs) - - self.table = dynamodb.Table( - self, "TemplateDynamoTable", - partition_key=dynamodb.Attribute( - name="PK", - type=dynamodb.AttributeType.STRING - ), - sort_key=dynamodb.Attribute( - name="SK", - type=dynamodb.AttributeType.STRING - ), - billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST, - removal_policy=RemovalPolicy.DESTROY - ) - - - - diff --git a/iac/iac/template_stack.py b/iac/iac/template_stack.py deleted file mode 100644 index dbaf9b4..0000000 --- a/iac/iac/template_stack.py +++ /dev/null @@ -1,55 +0,0 @@ -from aws_cdk import ( - # Duration, - Stack, - # aws_sqs as sqs, -) -from constructs import Construct -from aws_cdk.aws_apigateway import RestApi, Cors - -from .lambda_stack import LambdaStack -from .template_dynamo_table import TemplateDynamoTable - - -class TemplateStack(Stack): - - def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: - super().__init__(scope, construct_id, **kwargs) - - self.rest_api = RestApi(self, "Template_RestApi", - rest_api_name="Template_RestApi", - description="This is the Template RestApi", - default_cors_preflight_options= - { - "allow_origins": Cors.ALL_ORIGINS, - "allow_methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"], - "allow_headers": ["*"] - }, - ) - - api_gateway_resource = self.rest_api.root.add_resource("mss-template", default_cors_preflight_options= - { - "allow_origins": Cors.ALL_ORIGINS, - "allow_methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"], - "allow_headers": Cors.DEFAULT_HEADERS - } - ) - - self.dynamo_table = TemplateDynamoTable(self, "TemplateDynamoTable") - - ENVIRONMENT_VARIABLES = { - "STAGE": "DEV", - "DYNAMO_TABLE_NAME": self.dynamo_table.table.table_name, - "DYNAMO_PARTITION_KEY": "PK", - "DYNAMO_SORT_KEY": "SK", - "REGION": self.region, - } - - - - self.lambda_stack = LambdaStack(self, api_gateway_resource=api_gateway_resource, - environment_variables=ENVIRONMENT_VARIABLES) - - for function in self.lambda_stack.functions_that_need_dynamo_permissions: - self.dynamo_table.table.grant_read_write_data(function) - - \ No newline at end of file diff --git a/iac/stack/iac_stack.py b/iac/stack/iac_stack.py new file mode 100644 index 0000000..db79b33 --- /dev/null +++ b/iac/stack/iac_stack.py @@ -0,0 +1,70 @@ +import os + +from aws_cdk import Stack +from constructs import Construct + +from components.apigw_construct import ApigwConstruct +from components.dynamo_construct import DynamoConstruct +from components.lambda_construct import LambdaConstruct +from components.ssm_construct import SsmConstruct + + +class IacStack(Stack): + + def __init__( + self, + scope: Construct, + stack_id: str, + stack_name: str, + stage: str, + **kwargs, + ) -> None: + super().__init__(scope, stack_id, **kwargs) + + self.apigw_construct = ApigwConstruct( + self, + construct_id="Apigw", + stack_name=stack_name, + stage=stage, + ) + + self.dynamo_construct = DynamoConstruct( + self, + construct_id="Dynamo", + stack_name=stack_name, + stage=stage, + ) + + ENVIRONMENT_VARIABLES = { + "STAGE": stage.upper(), + "REGION": self.region, + "DYNAMO_TABLE_NAME": self.dynamo_construct.epep_table.table_name, + "DYNAMO_PARTITION_KEY": "pk", + "DYNAMO_SORT_KEY": "sk", + "MSS_NAME": stack_name, + "GRAPH_MICROSOFT_ENDPOINT": os.environ.get( + "GRAPH_MICROSOFT_ENDPOINT", + "https://graph.microsoft.com/v1.0/me", + ), + } + + self.lambda_construct = LambdaConstruct( + self, + construct_id="Lambda", + api_gateway_resource=self.apigw_construct.api_gateway_resource, + stage=stage, + stack_name=stack_name, + environment_variables=ENVIRONMENT_VARIABLES, + ) + + self.ssm_construct = SsmConstruct( + self, + construct_id=f"{stack_name}SystemsManager", + stack_name=stack_name, + stage=stage, + api=self.apigw_construct.rest_api, + api_gateway_resource=self.apigw_construct.api_gateway_resource, + ) + + for function in self.lambda_construct.functions_that_need_dynamo_db_access: + self.dynamo_construct.epep_table.grant_read_write_data(function) From a9bd507ad814bec5cccf9c6f87f1170b434710b4 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 4 Sep 2026 16:55:25 -0300 Subject: [PATCH 43/50] fix: DTO fix and fetch dynamo keys from dev --- src/shared/infra/dto/member_dynamo_dto.py | 57 +++--- .../infra/external/dynamo/dynamo_keys.py | 96 ++++++++++ .../infra/dto/test_member_dynamo_dto.py | 164 +++--------------- 3 files changed, 142 insertions(+), 175 deletions(-) create mode 100644 src/shared/infra/external/dynamo/dynamo_keys.py diff --git a/src/shared/infra/dto/member_dynamo_dto.py b/src/shared/infra/dto/member_dynamo_dto.py index c31fc8d..73f1a6f 100644 --- a/src/shared/infra/dto/member_dynamo_dto.py +++ b/src/shared/infra/dto/member_dynamo_dto.py @@ -1,48 +1,35 @@ -from pydantic import BaseModel, ConfigDict, HttpUrl -import uuid - from src.shared.domain.entities.member import Member -from src.shared.domain.enums.member_function_enum import MemberFunctionEnum +from src.shared.infra.external.dynamo.dynamo_keys import ( + EntityKind, + partition_key, + sort_key, + strip_keys, +) -class MemberDynamoDTO(BaseModel): - model_config = ConfigDict(use_enum_values=True) - entity: str = "member" - member_id: uuid.UUID - name: str - member_function: MemberFunctionEnum - linkedin: HttpUrl - member_photo: HttpUrl - description: str +class MemberDynamoDTO: + """ + DTO Dynamo: serializa entidade via model_dump + keys da single-table. + """ @staticmethod - def from_entity(member: Member) -> "MemberDynamoDTO": - """ - Parse data from Member to MemberDynamoDTO + def from_entity_to_dynamo(member: Member) -> dict: """ - return MemberDynamoDTO.model_validate(member, from_attributes=True) + Converts a Member entity to a dictionary compatible with DynamoDB. - def to_dynamo(self) -> dict: - """ - Parse data from MemberDynamoDTO to dict + Includes base keys (pk/sk). """ - data = self.model_dump() - data["member_id"] = str(data["member_id"]) - data["linkedin"] = str(data["linkedin"]) - data["member_photo"] = str(data["member_photo"]) - - return data + return { + **member.model_dump(mode="json"), + "pk": partition_key(kind=EntityKind.MEMBER), + "sk": sort_key(id=member.member_id, kind=EntityKind.MEMBER), + } @staticmethod - def from_dynamo(member_data: dict) -> "MemberDynamoDTO": - """ - Parse data from DynamoDB to MemberDynamoDTO - @param user_data: dict from DynamoDB + def from_dynamo_to_entity(member_data: dict) -> Member: """ - return MemberDynamoDTO.model_validate(member_data) + Converts a DynamoDB item dict into a Member entity. - def to_entity(self) -> Member: - """ - Parse data from MemberDynamoDTO to Member + Storage keys (pk/sk) are stripped before model_validate. """ - return Member.model_validate(self, from_attributes=True) \ No newline at end of file + return Member.model_validate(obj=strip_keys(member_data)) \ No newline at end of file diff --git a/src/shared/infra/external/dynamo/dynamo_keys.py b/src/shared/infra/external/dynamo/dynamo_keys.py new file mode 100644 index 0000000..c4f3d25 --- /dev/null +++ b/src/shared/infra/external/dynamo/dynamo_keys.py @@ -0,0 +1,96 @@ +"""Convenções de chaves Dynamo (tabela base + GSI UserEmailIndex). + +Tabela base (single-table): + pk = USER | MEMBER | SUBSCRIBER + sk = USER# | MEMBER# | SUBSCRIBER# + +GSI2 (UserEmailIndex) — access pattern "user por email": + gsi2pk = EMAIL# + gsi2sk = USER# + +Uso no repository Dynamo (exemplo):: + + from boto3.dynamodb.conditions import Key + from src.shared.infra.external.dynamo.dynamo_keys import ( + GSI2_NAME, GSI2_PK_ATTR, gsi2_partition_key, + ) + + resp = self.dynamo.query( + KeyConditionExpression=Key(GSI2_PK_ATTR).eq(gsi2_partition_key(user_email)), + IndexName=GSI2_NAME, + ) +""" + +from enum import Enum +from typing import Any +from uuid import UUID + +from pydantic import EmailStr + +# Nomes dos atributos da tabela base (alinhados ao CDK: pk/sk) +PK_ATTR = "pk" +SK_ATTR = "sk" + +# GSI2 — access pattern: buscar user por email (denso: email sempre presente) +# Alinhado a iac/iac/template_dynamo_table.py (UserEmailIndex) +GSI2_NAME = "UserEmailIndex" +GSI2_PK_ATTR = "gsi2pk" +GSI2_SK_ATTR = "gsi2sk" + +STORAGE_KEY_ATTRS = ( + PK_ATTR, SK_ATTR, + GSI2_PK_ATTR, GSI2_SK_ATTR, +) + + +class EntityKind(str, Enum): + USER = "USER" + MEMBER = "MEMBER" + SUBSCRIBER = "SUBSCRIBER" + + +def partition_key(kind: EntityKind) -> str: + """PK da tabela base — coleção (se repete para todos os items do kind).""" + return kind.value + + +def sort_key(id: UUID, kind: EntityKind) -> str: + """SK da tabela base — identidade única dentro da coleção.""" + return f"{kind.value}#{id}" + + +def gsi2_partition_key(user_email: EmailStr) -> str: + """ + PK do GSI2 — agrupa por email. + + Ex.: EMAIL#user@example.com + """ + return f"EMAIL#{user_email}" + + +def gsi2_sort_key(user_id: UUID) -> str: + """ + SK do GSI2 — identidade do user no índice de email. + + Ex.: USER# + """ + return sort_key(id=user_id, kind=EntityKind.USER) + + +def build_gsi2_attributes( + user_email: EmailStr, + user_id: UUID, +) -> dict[str, str]: + """ + GSI denso: email é obrigatório na entidade User, então todo user + recebe gsi2pk/gsi2sk e entra no UserEmailIndex. + """ + return { + GSI2_PK_ATTR: gsi2_partition_key(user_email=user_email), + GSI2_SK_ATTR: gsi2_sort_key(user_id=user_id), + } + + +def strip_keys(item: dict[str, Any]) -> dict[str, Any]: + """Remove atributos de storage (pk/sk/gsi) antes do model_validate.""" + return {k: v for k, v in item.items() if k not in STORAGE_KEY_ATTRS} diff --git a/tests/shared/infra/dto/test_member_dynamo_dto.py b/tests/shared/infra/dto/test_member_dynamo_dto.py index 61449f0..9e06481 100644 --- a/tests/shared/infra/dto/test_member_dynamo_dto.py +++ b/tests/shared/infra/dto/test_member_dynamo_dto.py @@ -1,148 +1,32 @@ import uuid -from src.shared.domain.entities.member import Member from src.shared.domain.enums.member_function_enum import MemberFunctionEnum from src.shared.infra.dto.member_dynamo_dto import MemberDynamoDTO from src.shared.infra.repositories.member_repository_mock import MemberRepositoryMock -class Test_MemberDynamoDTO: - def test_from_entity(self): - repo = MemberRepositoryMock() - - member_dto = MemberDynamoDTO.from_entity(member=repo.members[0]) - - expected_member_dto = MemberDynamoDTO( - member_id=repo.members[0].member_id, - name=repo.members[0].name, - member_function=repo.members[0].member_function, - linkedin=repo.members[0].linkedin, - member_photo=repo.members[0].member_photo, - description=repo.members[0].description - ) - - assert member_dto == expected_member_dto - - - def test_to_dynamo(self): - repo = MemberRepositoryMock() - - member_dto = MemberDynamoDTO( - member_id=repo.members[0].member_id, - name=repo.members[0].name, - member_function=repo.members[0].member_function, - linkedin=repo.members[0].linkedin, - member_photo=repo.members[0].member_photo, - description=repo.members[0].description - ) - - expected_dict = { - "entity": "member", - "member_id": str(repo.members[0].member_id), - "name": repo.members[0].name, - "member_function": "Marketing", - "linkedin": str(repo.members[0].linkedin), - "member_photo": str(repo.members[0].member_photo), - "description": repo.members[0].description - } - - assert member_dto.to_dynamo() == expected_dict - - - def test_from_dynamo(self): - member_id = uuid.uuid4() - dynamo_dict = { - "entity": "member", - "member_id": str(member_id), - "name": "Nome do Membro Um", - "member_function": "Marketing", - "linkedin": "https://www.linkedin.com/1", - "member_photo": "https://portalinterno.devmaua.com/assets/logo_fake1.png", - "description": "Exemplo de descrição do membro 1" - } - - member_dto = MemberDynamoDTO.from_dynamo(member_data=dynamo_dict) - - expected_member_dto = MemberDynamoDTO( - member_id=member_id, - name="Nome do Membro Um", - member_function="Marketing", - linkedin="https://www.linkedin.com/1", - member_photo="https://portalinterno.devmaua.com/assets/logo_fake1.png", - description="Exemplo de descrição do membro 1" - ) - - assert member_dto == expected_member_dto - - - def test_to_entity(self): - repo = MemberRepositoryMock() - - member_dto = MemberDynamoDTO( - member_id=repo.members[0].member_id, - name=repo.members[0].name, - member_function=repo.members[0].member_function, - linkedin=repo.members[0].linkedin, - member_photo=repo.members[0].member_photo, - description=repo.members[0].description - ) - member = member_dto.to_entity() - - assert member.member_id == repo.members[0].member_id - assert member.name == repo.members[0].name - assert member.member_function == repo.members[0].member_function - assert str(member.linkedin) == str(repo.members[0].linkedin) - assert str(member.member_photo) == str(repo.members[0].member_photo) - assert member.description == repo.members[0].description - - - def test_from_dynamo_to_entity(self): - member_id = uuid.uuid4() - dynamo_item = { - "entity": "member", - "member_id": str(member_id), - "name": "Nome do Membro Um", - "member_function": "Marketing", - "linkedin": "https://www.linkedin.com/1", - "member_photo": "https://portalinterno.devmaua.com/assets/logo_fake1.png", - "description": "Exemplo de descrição do membro 1" - } - - member_dto = MemberDynamoDTO.from_dynamo(member_data=dynamo_item) - - member = member_dto.to_entity() - - expected_member = Member( - member_id=member_id, - name="Nome do Membro Um", - member_function="Marketing", - linkedin="https://www.linkedin.com/1", - member_photo="https://portalinterno.devmaua.com/assets/logo_fake1.png", - description="Exemplo de descrição do membro 1" - ) - - assert member.member_id == expected_member.member_id - assert member.name == expected_member.name - assert member.member_function == expected_member.member_function - assert str(member.linkedin) == str(expected_member.linkedin) - assert str(member.member_photo) == str(expected_member.member_photo) - assert member.description == expected_member.description - +class Test_MemberDynamoDTO: def test_from_entity_to_dynamo(self): - repo = MemberRepositoryMock() - - member_dto = MemberDynamoDTO.from_entity(member=repo.members[0]) - - member_dynamo = member_dto.to_dynamo() - - expected_dict = { - "entity": "member", - "member_id": str(repo.members[0].member_id), - "name": repo.members[0].name, - "member_function": "Marketing", - "linkedin": str(repo.members[0].linkedin), - "member_photo": str(repo.members[0].member_photo), - "description": repo.members[0].description - } - - assert member_dynamo == expected_dict + member = MemberRepositoryMock().members[0] + data = MemberDynamoDTO.from_entity_to_dynamo(member) + + assert data["pk"] == "MEMBER" + assert data["sk"] == f"MEMBER#{member.member_id}" + assert data["name"] == member.name + assert data["member_function"] == MemberFunctionEnum.MARKETING.value + assert data["linkedin"] == str(member.linkedin) + assert data["member_photo"] == str(member.member_photo) + assert data["description"] == member.description + assert "member_id" in data + + def test_from_dynamo_to_entity_roundtrip(self): + member = MemberRepositoryMock().members[1] + dynamo = MemberDynamoDTO.from_entity_to_dynamo(member) + restored = MemberDynamoDTO.from_dynamo_to_entity(dynamo) + + assert restored.member_id == member.member_id + assert restored.name == member.name + assert restored.member_function == MemberFunctionEnum.REDACAO.value + assert str(restored.linkedin) == str(member.linkedin) + assert str(restored.member_photo) == str(member.member_photo) + assert restored.description == member.description \ No newline at end of file From 48dee5842d1182fb8160bba0041dcbdb749ebc59 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 4 Sep 2026 18:48:46 -0300 Subject: [PATCH 44/50] feat: member repository dynamo. --- src/shared/environments.py | 16 +-- src/shared/helpers/errors/usecase_errors.py | 4 + .../load_member_mock_to_dynamo.py | 122 ++++++++++++++++++ .../repositories/member_repository_dynamo.py | 99 ++++++++++++++ .../test_member_repository_dynamo.py | 80 ++++++++++++ 5 files changed, 313 insertions(+), 8 deletions(-) create mode 100644 src/shared/infra/repositories/load_member_mock_to_dynamo.py create mode 100644 src/shared/infra/repositories/member_repository_dynamo.py create mode 100644 tests/shared/infra/repositories/test_member_repository_dynamo.py diff --git a/src/shared/environments.py b/src/shared/environments.py index ce8c825..77e81be 100644 --- a/src/shared/environments.py +++ b/src/shared/environments.py @@ -24,7 +24,7 @@ class Environments: stage: STAGE s3_bucket_name: str region: str - endpoint_url: str = None + dynamo_endpoint_url: str = None # DynamoDB Local (ex: http://localhost:8000); None na AWS dynamo_table_name: str dynamo_partition_key: str dynamo_sort_key: str @@ -46,16 +46,16 @@ def load_envs(self): if self.stage == STAGE.TEST: self.s3_bucket_name = "bucket-test" self.region = "sa-east-1" - self.endpoint_url = "http://localhost:8000" + self.dynamo_endpoint_url = "http://localhost:8000" self.dynamo_table_name = "user_mss_template-table" - self.dynamo_partition_key = "PK" - self.dynamo_sort_key = "SK" + self.dynamo_partition_key = "pk" + self.dynamo_sort_key = "sk" self.cloud_front_distribution_domain = "https://d3q9q9q9q9q9q9.cloudfront.net" else: self.s3_bucket_name = os.environ.get("S3_BUCKET_NAME") self.region = os.environ.get("REGION") - self.endpoint_url = os.environ.get("ENDPOINT_URL") + self.dynamo_endpoint_url = os.environ.get("DYNAMO_ENDPOINT_URL") self.dynamo_table_name = os.environ.get("DYNAMO_TABLE_NAME") self.dynamo_partition_key = os.environ.get("DYNAMO_PARTITION_KEY") self.dynamo_sort_key = os.environ.get("DYNAMO_SORT_KEY") @@ -67,8 +67,8 @@ def get_user_repo() -> IUserRepository: from src.shared.infra.repositories.user_repository_mock import UserRepositoryMock return UserRepositoryMock elif Environments.get_envs().stage in [STAGE.DEV, STAGE.HOMOLOG, STAGE.PROD]: - from src.shared.infra.repositories.user_repository_dynamo import UserRepositoryDynamo - return UserRepositoryDynamo + from src.shared.infra.repositories.template_repository_dynamo import TemplateRepositoryDynamo + return TemplateRepositoryDynamo else: raise Exception("No repository found for this stage") @@ -86,7 +86,7 @@ def get_observability() -> IObservability: def get_envs() -> "Environments": """ Returns the Environments object. This method should be used to get the Environments object instead of instantiating it directly. - :return: Environments (stage={self.stage}, s3_bucket_name={self.s3_bucket_name}, region={self.region}, endpoint_url={self.endpoint_url}) + :return: Environments (stage={self.stage}, region={self.region}, dynamo_table_name={self.dynamo_table_name}, dynamo_endpoint_url={self.dynamo_endpoint_url}) """ envs = Environments() diff --git a/src/shared/helpers/errors/usecase_errors.py b/src/shared/helpers/errors/usecase_errors.py index 6782d14..867e0df 100644 --- a/src/shared/helpers/errors/usecase_errors.py +++ b/src/shared/helpers/errors/usecase_errors.py @@ -16,6 +16,10 @@ class DuplicatedUser(BaseError): def __init__(self, message: str): super().__init__(f'The user already exists for this {message}') +class DuplicatedMember(BaseError): + def __init__(self, message: str): + super().__init__(f'The member already exists for this {message}') + class ForbiddenAction(BaseError): def __init__(self, message: str): super().__init__(f'That action is forbidden for this {message}') diff --git a/src/shared/infra/repositories/load_member_mock_to_dynamo.py b/src/shared/infra/repositories/load_member_mock_to_dynamo.py new file mode 100644 index 0000000..ab2d03f --- /dev/null +++ b/src/shared/infra/repositories/load_member_mock_to_dynamo.py @@ -0,0 +1,122 @@ +import argparse + +import boto3 +import dotenv + +from src.shared.environments import Environments, STAGE +from src.shared.helpers.errors.usecase_errors import DuplicatedMember +from src.shared.infra.external.dynamo.dynamo_keys import ( + GSI2_NAME, + GSI2_PK_ATTR, + GSI2_SK_ATTR, +) +from src.shared.infra.repositories.member_repository_dynamo import MemberRepositoryDynamo +from src.shared.infra.repositories.member_repository_mock import MemberRepositoryMock + + +def setup_dynamo_table(): + envs = Environments.get_envs() + table_name = envs.dynamo_table_name + endpoint_url = envs.dynamo_endpoint_url + pk = envs.dynamo_partition_key + sk = envs.dynamo_sort_key + + print("Setting up DynamoDB table...") + dynamo_client = boto3.client("dynamodb", endpoint_url=endpoint_url) + tables = dynamo_client.list_tables()["TableNames"] + + if table_name in tables: + print("Table already exists!") + return + + print("Creating table...") + dynamo_client.create_table( + TableName=table_name, + KeySchema=[ + {"AttributeName": pk, "KeyType": "HASH"}, + {"AttributeName": sk, "KeyType": "RANGE"}, + ], + AttributeDefinitions=[ + {"AttributeName": pk, "AttributeType": "S"}, + {"AttributeName": sk, "AttributeType": "S"}, + {"AttributeName": GSI2_PK_ATTR, "AttributeType": "S"}, + {"AttributeName": GSI2_SK_ATTR, "AttributeType": "S"}, + ], + GlobalSecondaryIndexes=[ + { + "IndexName": GSI2_NAME, + "KeySchema": [ + {"AttributeName": GSI2_PK_ATTR, "KeyType": "HASH"}, + {"AttributeName": GSI2_SK_ATTR, "KeyType": "RANGE"}, + ], + "Projection": {"ProjectionType": "ALL"}, + }, + ], + BillingMode="PAY_PER_REQUEST", + ) + + print("Waiting for table to be created...") + dynamo_client.get_waiter("table_exists").wait(TableName=table_name) + print(f'Table "{table_name}" created!') + + +def _load_members(dynamo_repo: MemberRepositoryDynamo) -> int: + mock_repo = MemberRepositoryMock() + count = 0 + + print("Loading mock data to dynamo...") + for member in mock_repo.members: + print(f"Loading member {member.member_id} | {member.name} to dynamo") + try: + dynamo_repo.create_member(member) + count += 1 + except DuplicatedMember: + print(f" member {member.member_id} already exists, skipping") + + print(f"{count} members loaded to dynamo!") + return count + + +def load_mock_to_local_dynamo(): + """Create local table (if needed) and seed DynamoDB Local.""" + setup_dynamo_table() + _load_members(MemberRepositoryDynamo()) + + +def load_mock_to_real_dynamo(): + """ + Seed an already-deployed AWS table. + + Run manually after `cdk deploy` on DEV/HOMOLOG. + Does not create the table — CDK owns that. + Blocks PROD by default. + """ + envs = Environments.get_envs() + if envs.stage == STAGE.PROD: + raise RuntimeError( + "Refusing to seed PROD. Use DEV or HOMOLOG (or override intentionally)." + ) + + print( + f"Seeding AWS DynamoDB " + f"(stage={envs.stage.value}, table={envs.dynamo_table_name}, region={envs.region})" + ) + _load_members(MemberRepositoryDynamo()) + + +if __name__ == "__main__": + dotenv.load_dotenv() + + parser = argparse.ArgumentParser(description="Seed Member mock data into DynamoDB") + parser.add_argument( + "--target", + choices=["local", "aws"], + default="local", + help="local = DynamoDB Local (creates table). aws = deployed table (DEV/HOMOLOG only).", + ) + args = parser.parse_args() + + if args.target == "aws": + load_mock_to_real_dynamo() + else: + load_mock_to_local_dynamo() \ No newline at end of file diff --git a/src/shared/infra/repositories/member_repository_dynamo.py b/src/shared/infra/repositories/member_repository_dynamo.py new file mode 100644 index 0000000..9508492 --- /dev/null +++ b/src/shared/infra/repositories/member_repository_dynamo.py @@ -0,0 +1,99 @@ +from typing import List +from uuid import UUID + +from boto3.dynamodb.conditions import Key + +from src.shared.domain.entities.member import Member +from src.shared.domain.repositories.member_repository_interface import IMemberRepository +from src.shared.environments import Environments +from src.shared.helpers.errors.usecase_errors import DuplicatedMember, NoItemsFound +from src.shared.infra.dto.member_dynamo_dto import MemberDynamoDTO +from src.shared.infra.external.dynamo.datasources.dynamo_datasource import DynamoDatasource +from src.shared.infra.external.dynamo.dynamo_keys import ( + EntityKind, + PK_ATTR, + partition_key, + sort_key, +) + + +class MemberRepositoryDynamo(IMemberRepository): + """Repositório Dynamo (single-table) para a entidade Member.""" + + def __init__(self): + envs = Environments.get_envs() + self.dynamo = DynamoDatasource( + dynamo_table_name=envs.dynamo_table_name, + region=envs.region, + partition_key=envs.dynamo_partition_key, + sort_key=envs.dynamo_sort_key, + endpoint_url=envs.dynamo_endpoint_url, + ) + + def _pk(self) -> str: + return partition_key(kind=EntityKind.MEMBER) + + def _sk(self, member_id: UUID) -> str: + return sort_key(id=member_id, kind=EntityKind.MEMBER) + + def get_member(self, member_id: UUID) -> Member: + resp = self.dynamo.get_item( + partition_key=self._pk(), + sort_key=self._sk(member_id), + ) + + if resp.get("Item") is None: + raise NoItemsFound("member_id") + + return MemberDynamoDTO.from_dynamo_to_entity(resp["Item"]) + + def get_all_member(self) -> List[Member]: + resp = self.dynamo.query( + key_condition_expression=Key(PK_ATTR).eq(self._pk()), + ) + + return [ + MemberDynamoDTO.from_dynamo_to_entity(member) + for member in resp.get("Items", []) + ] + + def create_member(self, new_member: Member) -> Member: + existing = self.dynamo.get_item( + partition_key=self._pk(), + sort_key=self._sk(new_member.member_id), + ) + if existing.get("Item") is not None: + raise DuplicatedMember("member_id") + + self.dynamo.put_item( + item=MemberDynamoDTO.from_entity_to_dynamo(new_member), + partition_key=self._pk(), + sort_key=self._sk(new_member.member_id), + ) + return new_member + + def delete_member(self, member_id: UUID) -> Member: + resp = self.dynamo.delete_item( + partition_key=self._pk(), + sort_key=self._sk(member_id), + ) + + if "Attributes" not in resp: + raise NoItemsFound("member_id") + + return MemberDynamoDTO.from_dynamo_to_entity(resp["Attributes"]) + + def update_member(self, member: Member) -> Member: + existing = self.dynamo.get_item( + partition_key=self._pk(), + sort_key=self._sk(member.member_id), + ) + if existing.get("Item") is None: + raise NoItemsFound("member_id") + + self.dynamo.put_item( + item=MemberDynamoDTO.from_entity_to_dynamo(member), + partition_key=self._pk(), + sort_key=self._sk(member.member_id), + ) + return member \ No newline at end of file diff --git a/tests/shared/infra/repositories/test_member_repository_dynamo.py b/tests/shared/infra/repositories/test_member_repository_dynamo.py new file mode 100644 index 0000000..7dce796 --- /dev/null +++ b/tests/shared/infra/repositories/test_member_repository_dynamo.py @@ -0,0 +1,80 @@ +import os +import pytest + +from src.shared.domain.entities.member import Member +from src.shared.infra.repositories.member_repository_dynamo import MemberRepositoryDynamo +from src.shared.infra.repositories.member_repository_mock import MemberRepositoryMock + + +class Test_MemberRepositoryDynamo: + + @pytest.mark.skip(reason="Needs dynamoDB") + def test_create_member(self): + os.environ["STAGE"] = "TEST" + + member_repository = MemberRepositoryDynamo() + member_repository_mock = MemberRepositoryMock() + resp = member_repository.create_member(member_repository_mock.members[0]) + + assert member_repository_mock.members[0].name == resp.name + + + @pytest.mark.skip(reason="Needs dynamoDB") + def test_get_member(self): + os.environ["STAGE"] = "TEST" + + member_repository = MemberRepositoryDynamo() + member_repository_mock = MemberRepositoryMock() + + created = member_repository.create_member(member_repository_mock.members[0]) + resp = member_repository.get_member(created.member_id) + + assert created.name == resp.name + + + @pytest.mark.skip(reason="Needs dynamoDB") + def test_delete_member(self): + os.environ["STAGE"] = "TEST" + + member_repository = MemberRepositoryDynamo() + member_repository_mock = MemberRepositoryMock() + + created = member_repository.create_member(member_repository_mock.members[0]) + resp = member_repository.delete_member(created.member_id) + + assert created.name == resp.name + + + @pytest.mark.skip(reason="Needs dynamoDB") + def test_get_all_member(self): + os.environ["STAGE"] = "TEST" + + member_repository = MemberRepositoryDynamo() + member_repository_mock = MemberRepositoryMock() + + created = member_repository.create_member(member_repository_mock.members[0]) + resp = member_repository.get_all_member() + + assert any(m.member_id == created.member_id for m in resp) + + + @pytest.mark.skip(reason="Needs dynamoDB") + def test_update_member(self): + os.environ["STAGE"] = "TEST" + + member_repository = MemberRepositoryDynamo() + member_repository_mock = MemberRepositoryMock() + + created = member_repository.create_member(member_repository_mock.members[0]) + + updated_member = Member( + member_id=created.member_id, + name="Nome do Membro Atualizado", + member_function=created.member_function, + linkedin=created.linkedin, + member_photo=created.member_photo, + description=created.description + ) + resp = member_repository.update_member(updated_member) + + assert resp.name == "Nome do Membro Atualizado" From 6c312b9e695a434750320b32977c87d094222ee3 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 9 Sep 2026 18:52:42 -0300 Subject: [PATCH 45/50] refat: added status attribute (enum), added every member_function --- src/shared/domain/entities/member.py | 15 +- .../domain/enums/member_function_enum.py | 9 +- src/shared/domain/enums/member_status_enum.py | 7 + .../repositories/member_repository_mock.py | 10 +- tests/shared/domain/entities/test_member.py | 238 ++++++++++++------ .../test_member_repository_mock.py | 83 ++++-- 6 files changed, 246 insertions(+), 116 deletions(-) create mode 100644 src/shared/domain/enums/member_status_enum.py diff --git a/src/shared/domain/entities/member.py b/src/shared/domain/entities/member.py index 7deab90..7d208b1 100644 --- a/src/shared/domain/entities/member.py +++ b/src/shared/domain/entities/member.py @@ -3,6 +3,7 @@ from src.shared.helpers.errors.domain_errors import EntityError from src.shared.domain.enums.member_function_enum import MemberFunctionEnum +from src.shared.domain.enums.member_status_enum import MemberStatusEnum class Member(BaseModel): def __init__(self, **data): @@ -26,28 +27,34 @@ def __init__(self, **data): ) - member_function: MemberFunctionEnum = Field( + function: MemberFunctionEnum = Field( ..., description="Cargo do membro" ) + status: MemberStatusEnum = Field( + ..., + description="Status do membro" + ) + + linkedin: HttpUrl = Field( ..., description="Perfil do LinkedIn do membro" ) - member_photo: HttpUrl = Field( + photo: HttpUrl = Field( ..., description="Foto do membro" ) - @field_validator("member_photo") + @field_validator("photo") @classmethod def validate_photo_extension(cls, url: HttpUrl) -> HttpUrl: allowed_extensions = (".png", ".jpg", ".jpeg", ".webp") if not str(url).lower().endswith(allowed_extensions): - raise ValueError("member_photo must end with .png, .jpg, .jpeg, or .webp") + raise ValueError("photo must end with .png, .jpg, .jpeg, or .webp") return url # Checks the URL's extension format, but not its content. It can still be a non-existent URL, for example diff --git a/src/shared/domain/enums/member_function_enum.py b/src/shared/domain/enums/member_function_enum.py index ca28658..f6d39cc 100644 --- a/src/shared/domain/enums/member_function_enum.py +++ b/src/shared/domain/enums/member_function_enum.py @@ -1,5 +1,12 @@ from enum import Enum class MemberFunctionEnum(Enum): + PRESIDENCIA = "Presidencia" + VICE_PRESIDENCIA = "VicePresidencia" + RH = "RH" + MARKETING = "Marketing" REDACAO = "Redacao" - MARKETING = "Marketing" \ No newline at end of file + EVENTOS = "Eventos" + FINANCEIRO = "Financeiro" + PROJETOS = "Projetos" + \ No newline at end of file diff --git a/src/shared/domain/enums/member_status_enum.py b/src/shared/domain/enums/member_status_enum.py new file mode 100644 index 0000000..6bf060c --- /dev/null +++ b/src/shared/domain/enums/member_status_enum.py @@ -0,0 +1,7 @@ +from enum import Enum + +class MemberStatusEnum(Enum): + ATIVO = "Ativo" + DESATIVADO = "Desativado" + CONGELADO = "Congelado" + \ No newline at end of file diff --git a/src/shared/infra/repositories/member_repository_mock.py b/src/shared/infra/repositories/member_repository_mock.py index 4fbac8c..6447fc6 100644 --- a/src/shared/infra/repositories/member_repository_mock.py +++ b/src/shared/infra/repositories/member_repository_mock.py @@ -13,16 +13,18 @@ def __init__(self): self.members = [ Member( name="Nome do Membro Um", - member_function="Marketing", + function="Marketing", + status="Ativo", linkedin="https://www.linkedin.com/1", - member_photo="https://portalinterno.devmaua.com/assets/logo_fake1.png", + photo="https://portalinterno.devmaua.com/assets/logo_fake1.png", description="Exemplo de descrição do membro 1" ), Member( name="Nome do Membro Dois", - member_function="Redacao", + function="Redacao", + status="Congelado", linkedin="https://www.linkedin.com/2", - member_photo="https://portalinterno.devmaua.com/assets/logo_fake2.png", + photo="https://portalinterno.devmaua.com/assets/logo_fake2.png", description="Exemplo de descrição do membro 2" ) ] diff --git a/tests/shared/domain/entities/test_member.py b/tests/shared/domain/entities/test_member.py index 372a089..0ac30e5 100644 --- a/tests/shared/domain/entities/test_member.py +++ b/tests/shared/domain/entities/test_member.py @@ -3,6 +3,7 @@ from src.shared.domain.entities.member import Member from src.shared.domain.enums.member_function_enum import MemberFunctionEnum +from src.shared.domain.enums.member_status_enum import MemberStatusEnum from src.shared.helpers.errors.domain_errors import EntityError @@ -11,16 +12,18 @@ class Test_Member: def test_member(self): member = Member( name="Nome Completo do Membro", - member_function="Marketing", + function="Marketing", + status="Ativo", linkedin="https://www.linkedin.com/", - member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", description="Exemplo de descrição do membro" ) assert isinstance(member.member_id, uuid.UUID) assert member.name == "Nome Completo do Membro" - assert member.member_function == "Marketing" + assert member.function == "Marketing" + assert member.status == "Ativo" assert str(member.linkedin) == "https://www.linkedin.com/" - assert str(member.member_photo) == "https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png" + assert str(member.photo) == "https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png" assert member.description == "Exemplo de descrição do membro" @@ -31,9 +34,10 @@ def test_member_extra_field(self): with pytest.raises(EntityError): member = Member( name="Nome Completo do Membro", - member_function="Marketing", + function="Marketing", + status="Ativo", linkedin="https://www.linkedin.com/", - member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", description="Exemplo de descrição do membro", extra_field="Campo extra" ) @@ -46,9 +50,10 @@ def test_member_name_with_numbers(self): with pytest.raises(EntityError): member = Member( name="N0me 1nválido", - member_function="Marketing", + function="Marketing", + status="Ativo", linkedin="https://www.linkedin.com/", - member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", description="Exemplo de descrição do membro" ) @@ -57,9 +62,10 @@ def test_member_name_with_invalid_symbols(self): with pytest.raises(EntityError): member = Member( name="[Nome Inválido!]", - member_function="Marketing", + function="Marketing", + status="Ativo", linkedin="https://www.linkedin.com/", - member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", description="Exemplo de descrição do membro" ) @@ -68,9 +74,10 @@ def test_member_name_too_short(self): with pytest.raises(EntityError): member = Member( name="N", - member_function="Marketing", + function="Marketing", + status="Ativo", linkedin="https://www.linkedin.com/", - member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", description="Exemplo de descrição do membro" ) @@ -78,9 +85,10 @@ def test_member_name_too_short(self): def test_member_name_min_length(self): member = Member( name="Ju", - member_function="Marketing", + function="Marketing", + status="Ativo", linkedin="https://www.linkedin.com/", - member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", description="Exemplo de descrição do membro" ) assert member.name == "Ju" @@ -90,9 +98,10 @@ def test_member_name_empty(self): with pytest.raises(EntityError): member = Member( name="", - member_function="Marketing", + function="VicePresidencia", + status="Ativo", linkedin="https://www.linkedin.com/", - member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", description="Exemplo de descrição do membro" ) @@ -100,68 +109,112 @@ def test_member_name_empty(self): def test_member_has_no_name(self): with pytest.raises(EntityError): member = Member( - member_function="Marketing", + function="Marketing", + status="Ativo", linkedin="https://www.linkedin.com/", - member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", description="Exemplo de descrição do membro" ) - # member_function + # function def test_member_function_invalid(self): with pytest.raises(EntityError): member = Member( name="Nome Completo do Membro", - member_function="Cargo Inválido", + function="Cargo Inválido", + status="Ativo", linkedin="https://www.linkedin.com/", - member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", description="Exemplo de descrição do membro" ) - def test_member_function_redacao_enum(self): + def test_member_function_valid_enum(self): member = Member( name="Nome Completo do Membro", - member_function=MemberFunctionEnum.REDACAO, + function=MemberFunctionEnum.RH, + status="Ativo", linkedin="https://www.linkedin.com/", - member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", description="Exemplo de descrição do membro" ) - assert member.member_function == "Redacao" + assert member.function == "RH" - def test_member_function_marketing_enum(self): + def test_member_function_empty(self): + with pytest.raises(EntityError): member = Member( name="Nome Completo do Membro", - member_function=MemberFunctionEnum.MARKETING, + function="", + status="Ativo", linkedin="https://www.linkedin.com/", - member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", description="Exemplo de descrição do membro" ) - assert member.member_function == "Marketing" - def test_member_function_empty(self): + def test_member_has_no_function(self): with pytest.raises(EntityError): member = Member( name="Nome Completo do Membro", - member_function="", + status="Ativo", linkedin="https://www.linkedin.com/", - member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", description="Exemplo de descrição do membro" ) - def test_member_has_no_function(self): - with pytest.raises(EntityError): + + # status + + def test_member_status_invalid(self): + with pytest.raises(EntityError): + member = Member( + name="Nome Completo do Membro", + function="Presidencia", + status="Status Inválido", + linkedin="https://www.linkedin.com/", + photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + description="Exemplo de descrição do membro" + ) + + + def test_member_status_valid_enum(self): member = Member( name="Nome Completo do Membro", + function="Eventos", + status=MemberStatusEnum.CONGELADO, linkedin="https://www.linkedin.com/", - member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", description="Exemplo de descrição do membro" ) + assert member.status == "Congelado" + + + def test_member_status_empty(self): + with pytest.raises(EntityError): + member = Member( + name="Nome Completo do Membro", + function="Financeiro", + status="", + linkedin="https://www.linkedin.com/", + photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + description="Exemplo de descrição do membro" + ) + + + def test_member_has_no_status(self): + with pytest.raises(EntityError): + member = Member( + name="Nome Completo do Membro", + function="RH", + linkedin="https://www.linkedin.com/", + photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + description="Exemplo de descrição do membro" + ) @@ -171,9 +224,10 @@ def test_member_linkedin_invalid_url(self): with pytest.raises(EntityError): member = Member( name="Nome Completo do Membro", - member_function="Marketing", + function="Marketing", + status="Desativado", linkedin="Não é uma url", - member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", description="Exemplo de descrição do membro" ) @@ -182,9 +236,10 @@ def test_member_linkedin_no_scheme(self): with pytest.raises(EntityError): member = Member( name="Nome Completo do Membro", - member_function="Marketing", + function="Marketing", + status="Desativado", linkedin="www.linkedin.com", - member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", description="Exemplo de descrição do membro" ) @@ -193,9 +248,10 @@ def test_member_linkedin_empty(self): with pytest.raises(EntityError): member = Member( name="Nome Completo do Membro", - member_function="Marketing", + function="Marketing", + status="Desativado", linkedin="", - member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", description="Exemplo de descrição do membro" ) @@ -203,9 +259,10 @@ def test_member_linkedin_empty(self): def test_member_linkedin_normalizes_slash(self): member = Member( name="Nome Completo do Membro", - member_function="Marketing", + function="Marketing", + status="Desativado", linkedin="https://www.linkedin.com", - member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", description="Exemplo de descrição do membro" ) assert str(member.linkedin) == "https://www.linkedin.com/" @@ -215,22 +272,24 @@ def test_member_has_no_linkedin(self): with pytest.raises(EntityError): member = Member( name="Nome Completo do Membro", - member_function="Marketing", - member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + function="Marketing", + status="Desativado", + photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", description="Exemplo de descrição do membro" ) - # member_photo + # photo def test_member_photo_invalid_url(self): with pytest.raises(EntityError): member = Member( name="Nome Completo do Membro", - member_function="Marketing", + function="Marketing", + status="Desativado", linkedin="https://www.linkedin.com/", - member_photo="Não é URL", + photo="Não é URL", description="Exemplo de descrição do membro" ) @@ -239,9 +298,10 @@ def test_member_photo_no_scheme(self): with pytest.raises(EntityError): member = Member( name="Nome Completo do Membro", - member_function="Marketing", + function="Marketing", + status="Desativado", linkedin="https://www.linkedin.com/", - member_photo="portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + photo="portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", description="Exemplo de descrição do membro" ) @@ -250,9 +310,10 @@ def test_member_photo_url_no_file_extension(self): with pytest.raises(EntityError): member = Member( name="Nome Completo do Membro", - member_function="Marketing", + function="Marketing", + status="Desativado", linkedin="https://www.linkedin.com/", - member_photo="https://portalinterno.devmaua.com/", + photo="https://portalinterno.devmaua.com/", description="Exemplo de descrição do membro" ) @@ -260,54 +321,59 @@ def test_member_photo_url_no_file_extension(self): def test_member_photo_accepts_png(self): member = Member( name="Nome Completo do Membro", - member_function="Marketing", + function="Marketing", + status="Desativado", linkedin="https://www.linkedin.com/", - member_photo="https://www.exemplo.com/imagem.png", + photo="https://www.exemplo.com/imagem.png", description="Exemplo de descrição do membro" ) - assert str(member.member_photo).endswith(".png") + assert str(member.photo).endswith(".png") def test_member_photo_accepts_jpg(self): member = Member( name="Nome Completo do Membro", - member_function="Marketing", + function="Marketing", + status="Desativado", linkedin="https://www.linkedin.com/", - member_photo="https://www.exemplo.com/imagem.jpg", + photo="https://www.exemplo.com/imagem.jpg", description="Exemplo de descrição do membro" ) - assert str(member.member_photo).endswith(".jpg") + assert str(member.photo).endswith(".jpg") def test_member_photo_accepts_jpeg(self): member = Member( name="Nome Completo do Membro", - member_function="Marketing", + function="Marketing", + status="Desativado", linkedin="https://www.linkedin.com/", - member_photo="https://www.exemplo.com/imagem.jpeg", + photo="https://www.exemplo.com/imagem.jpeg", description="Exemplo de descrição do membro" ) - assert str(member.member_photo).endswith(".jpeg") + assert str(member.photo).endswith(".jpeg") def test_member_photo_accepts_webp(self): member = Member( name="Nome Completo do Membro", - member_function="Marketing", + function="Marketing", + status="Desativado", linkedin="https://www.linkedin.com/", - member_photo="https://www.exemplo.com/imagem.webp", + photo="https://www.exemplo.com/imagem.webp", description="Exemplo de descrição do membro" ) - assert str(member.member_photo).endswith(".webp") + assert str(member.photo).endswith(".webp") def test_member_photo_rejects_other_extensions(self): with pytest.raises(EntityError): member = Member( name="Nome Completo do Membro", - member_function="Marketing", + function="Marketing", + status="Desativado", linkedin="https://www.linkedin.com/", - member_photo="https://www.exemplo.com/imagem.pdf", + photo="https://www.exemplo.com/imagem.pdf", description="Exemplo de descrição do membro" ) @@ -315,21 +381,23 @@ def test_member_photo_rejects_other_extensions(self): def test_member_photo_case_insensitive(self): member = Member( name="Nome Completo do Membro", - member_function="Marketing", + function="Marketing", + status="Desativado", linkedin="https://www.linkedin.com/", - member_photo="https://www.exemplo.com/imagem.PNG", + photo="https://www.exemplo.com/imagem.PNG", description="Exemplo de descrição do membro" ) - assert str(member.member_photo).endswith(".PNG") + assert str(member.photo).endswith(".PNG") def test_member_photo_empty(self): with pytest.raises(EntityError): member = Member( name="Nome Completo do Membro", - member_function="Marketing", + function="Marketing", + status="Desativado", linkedin="https://www.linkedin.com/", - member_photo="", + photo="", description="Exemplo de descrição do membro" ) @@ -338,7 +406,8 @@ def test_member_has_no_photo(self): with pytest.raises(EntityError): member = Member( name="Nome Completo do Membro", - member_function="Marketing", + function="Marketing", + status="Desativado", linkedin="https://www.linkedin.com/", description="Exemplo de descrição do membro" ) @@ -350,9 +419,10 @@ def test_member_has_no_photo(self): def test_member_description_with_numbers(self): member = Member( name="Nome Completo do Membro", - member_function="Marketing", + function="Marketing", + status="Desativado", linkedin="https://www.linkedin.com/", - member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", description="Descrição: membro melhorou em 10%. (com parênteses)!" ) assert member.description == "Descrição: membro melhorou em 10%. (com parênteses)!" @@ -362,9 +432,10 @@ def test_member_description_too_short(self): with pytest.raises(EntityError): member = Member( name="Nome Completo do Membro", - member_function="Marketing", + function="Marketing", + status="Desativado", linkedin="https://www.linkedin.com/", - member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", description="Abcd" ) @@ -372,9 +443,10 @@ def test_member_description_too_short(self): def test_member_description_min_length(self): member = Member( name="Nome Completo do Membro", - member_function="Marketing", + function="Marketing", + status="Desativado", linkedin="https://www.linkedin.com/", - member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", description="Curto" ) assert member.description == "Curto" @@ -384,9 +456,10 @@ def test_member_description_empty(self): with pytest.raises(EntityError): member = Member( name="Nome Completo do Membro", - member_function="Marketing", + function="Marketing", + status="Desativado", linkedin="https://www.linkedin.com/", - member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", + photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png", description="" ) @@ -395,7 +468,8 @@ def test_member_has_no_description(self): with pytest.raises(EntityError): member = Member( name="Nome Completo do Membro", - member_function="Marketing", + function="Marketing", + status="Desativado", linkedin="https://www.linkedin.com/", - member_photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png" + photo="https://portalinterno.devmaua.com/assets/logo_dev-ec58e665.png" ) diff --git a/tests/shared/infra/repositories/test_member_repository_mock.py b/tests/shared/infra/repositories/test_member_repository_mock.py index 8e266ab..7de1fca 100644 --- a/tests/shared/infra/repositories/test_member_repository_mock.py +++ b/tests/shared/infra/repositories/test_member_repository_mock.py @@ -13,16 +13,18 @@ def test_create_member(self): repo = MemberRepositoryMock() member = Member( name="Membro Três", - member_function="Marketing", + function="Marketing", + status="Desativado", linkedin="https://www.linkedin.com/3", - member_photo="https://portalinterno.devmaua.com/assets/logo_fake3.png", + photo="https://portalinterno.devmaua.com/assets/logo_fake3.png", description="Descrição do terceiro membro do repo" ) repo.create_member(member) assert repo.members[-1].name == "Membro Três" - assert repo.members[-1].member_function == "Marketing" + assert repo.members[-1].function == "Marketing" + assert repo.members[-1].status == "Desativado" assert str(repo.members[-1].linkedin) =="https://www.linkedin.com/3" - assert str(repo.members[-1].member_photo) =="https://portalinterno.devmaua.com/assets/logo_fake3.png" + assert str(repo.members[-1].photo) =="https://portalinterno.devmaua.com/assets/logo_fake3.png" assert repo.members[-1].description =="Descrição do terceiro membro do repo" assert len(repo.members) == 3 @@ -33,9 +35,10 @@ def test_get_member(self): member = repo.get_member(member_id) assert member.name == "Nome do Membro Dois" - assert member.member_function == "Redacao" + assert member.function == "Redacao" + assert member.status == "Congelado" assert str(member.linkedin) == "https://www.linkedin.com/2" - assert str(member.member_photo) == "https://portalinterno.devmaua.com/assets/logo_fake2.png" + assert str(member.photo) == "https://portalinterno.devmaua.com/assets/logo_fake2.png" assert member.description == "Exemplo de descrição do membro 2" @@ -56,24 +59,50 @@ def test_update_member_function(self): repo = MemberRepositoryMock() member = repo.get_all_member()[0] - member_old_function = member.member_function + member_old_function = member.function member_name = member.name member = Member( member_id=member.member_id, name=member.name, - member_function="Redacao", + function="Projetos", + status=member.status, linkedin=member.linkedin, - member_photo=member.member_photo, + photo=member.photo, description=member.description ) updated_member = repo.update_member(member) assert updated_member is not None assert updated_member.name == member_name - assert updated_member.member_function != member_old_function - assert updated_member.member_function == "Redacao" - assert repo.members[0].member_function == "Redacao" + assert updated_member.function != member_old_function + assert updated_member.function == "Projetos" + assert repo.members[0].function == "Projetos" + + + def test_update_member_status(self): + repo = MemberRepositoryMock() + member = repo.get_all_member()[0] + + member_old_status = member.status + member_name = member.name + + member = Member( + member_id=member.member_id, + name=member.name, + function=member.function, + status="Desativado", + linkedin=member.linkedin, + photo=member.photo, + description=member.description + ) + updated_member = repo.update_member(member) + + assert updated_member is not None + assert updated_member.name == member_name + assert updated_member.status != member_old_status + assert updated_member.status == "Desativado" + assert repo.members[0].status == "Desativado" def test_update_member_linkedin(self): @@ -86,9 +115,10 @@ def test_update_member_linkedin(self): member = Member( member_id=member.member_id, name=member.name, - member_function=member.member_function, + function=member.function, + status=member.status, linkedin="https://www.linkedin.com/novo", - member_photo=member.member_photo, + photo=member.photo, description=member.description ) updated_member = repo.update_member(member) @@ -104,24 +134,25 @@ def test_update_member_photo(self): repo = MemberRepositoryMock() member = repo.get_all_member()[0] - member_old_photo = member.member_photo + member_old_photo = member.photo member_name = member.name member = Member( member_id=member.member_id, name=member.name, - member_function=member.member_function, + function=member.function, + status=member.status, linkedin=member.linkedin, - member_photo="https://www.photo.com/photo1.jpg", + photo="https://www.photo.com/photo1.jpg", description=member.description ) updated_member = repo.update_member(member) assert updated_member is not None assert updated_member.name == member_name - assert updated_member.member_photo != member_old_photo - assert str(updated_member.member_photo) == "https://www.photo.com/photo1.jpg" - assert str(repo.members[0].member_photo) == "https://www.photo.com/photo1.jpg" + assert updated_member.photo != member_old_photo + assert str(updated_member.photo) == "https://www.photo.com/photo1.jpg" + assert str(repo.members[0].photo) == "https://www.photo.com/photo1.jpg" def test_update_member_description(self): @@ -134,9 +165,10 @@ def test_update_member_description(self): member = Member( member_id=member.member_id, name=member.name, - member_function=member.member_function, + function=member.function, + status=member.status, linkedin=member.linkedin, - member_photo=member.member_photo, + photo=member.photo, description="Nova descrição do membro" ) updated_member = repo.update_member(member) @@ -155,10 +187,11 @@ def test_update_member_not_found(self): member = Member( # new member_id name=member.name, - member_function=member.member_function, + function=member.function, + status=member.status, linkedin=member.linkedin, - member_photo=member.member_photo, - description="Nova descrição do membro" + photo=member.photo, + description=member.description ) with pytest.raises(NoItemsFound): From aa288088eb6398fc731ef63691b5ee1525da80ff Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 14 Sep 2026 17:38:40 -0300 Subject: [PATCH 46/50] fix: dto test fix --- tests/shared/infra/dto/test_member_dynamo_dto.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/shared/infra/dto/test_member_dynamo_dto.py b/tests/shared/infra/dto/test_member_dynamo_dto.py index 9e06481..521e168 100644 --- a/tests/shared/infra/dto/test_member_dynamo_dto.py +++ b/tests/shared/infra/dto/test_member_dynamo_dto.py @@ -1,6 +1,7 @@ import uuid from src.shared.domain.enums.member_function_enum import MemberFunctionEnum +from src.shared.domain.enums.member_status_enum import MemberStatusEnum from src.shared.infra.dto.member_dynamo_dto import MemberDynamoDTO from src.shared.infra.repositories.member_repository_mock import MemberRepositoryMock @@ -13,9 +14,10 @@ def test_from_entity_to_dynamo(self): assert data["pk"] == "MEMBER" assert data["sk"] == f"MEMBER#{member.member_id}" assert data["name"] == member.name - assert data["member_function"] == MemberFunctionEnum.MARKETING.value + assert data["function"] == MemberFunctionEnum.MARKETING.value + assert data["status"] == MemberStatusEnum.ATIVO.value assert data["linkedin"] == str(member.linkedin) - assert data["member_photo"] == str(member.member_photo) + assert data["photo"] == str(member.photo) assert data["description"] == member.description assert "member_id" in data @@ -26,7 +28,8 @@ def test_from_dynamo_to_entity_roundtrip(self): assert restored.member_id == member.member_id assert restored.name == member.name - assert restored.member_function == MemberFunctionEnum.REDACAO.value + assert restored.function == MemberFunctionEnum.REDACAO.value + assert restored.status == MemberStatusEnum.CONGELADO.value assert str(restored.linkedin) == str(member.linkedin) - assert str(restored.member_photo) == str(member.member_photo) + assert str(restored.photo) == str(member.photo) assert restored.description == member.description \ No newline at end of file From 46dd7cc9897067dc3faa6d6759af4dac99c83d8c Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 14 Sep 2026 21:21:10 -0300 Subject: [PATCH 47/50] fix: member repo dynamo test fix --- .../repositories/test_member_repository_dynamo.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/shared/infra/repositories/test_member_repository_dynamo.py b/tests/shared/infra/repositories/test_member_repository_dynamo.py index 7dce796..0593222 100644 --- a/tests/shared/infra/repositories/test_member_repository_dynamo.py +++ b/tests/shared/infra/repositories/test_member_repository_dynamo.py @@ -8,7 +8,7 @@ class Test_MemberRepositoryDynamo: - @pytest.mark.skip(reason="Needs dynamoDB") + #@pytest.mark.skip(reason="Needs dynamoDB") def test_create_member(self): os.environ["STAGE"] = "TEST" @@ -19,7 +19,7 @@ def test_create_member(self): assert member_repository_mock.members[0].name == resp.name - @pytest.mark.skip(reason="Needs dynamoDB") + #@pytest.mark.skip(reason="Needs dynamoDB") def test_get_member(self): os.environ["STAGE"] = "TEST" @@ -32,7 +32,7 @@ def test_get_member(self): assert created.name == resp.name - @pytest.mark.skip(reason="Needs dynamoDB") + #@pytest.mark.skip(reason="Needs dynamoDB") def test_delete_member(self): os.environ["STAGE"] = "TEST" @@ -45,7 +45,7 @@ def test_delete_member(self): assert created.name == resp.name - @pytest.mark.skip(reason="Needs dynamoDB") + #@pytest.mark.skip(reason="Needs dynamoDB") def test_get_all_member(self): os.environ["STAGE"] = "TEST" @@ -58,7 +58,7 @@ def test_get_all_member(self): assert any(m.member_id == created.member_id for m in resp) - @pytest.mark.skip(reason="Needs dynamoDB") + #@pytest.mark.skip(reason="Needs dynamoDB") def test_update_member(self): os.environ["STAGE"] = "TEST" @@ -70,9 +70,10 @@ def test_update_member(self): updated_member = Member( member_id=created.member_id, name="Nome do Membro Atualizado", - member_function=created.member_function, + function=created.function, + status=created.status, linkedin=created.linkedin, - member_photo=created.member_photo, + photo=created.photo, description=created.description ) resp = member_repository.update_member(updated_member) From 93ee104918750ed8e219f462df96944d77f06906 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 14 Sep 2026 21:23:25 -0300 Subject: [PATCH 48/50] fix: skipping dynamoDB tests --- .../repositories/test_member_repository_dynamo.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/shared/infra/repositories/test_member_repository_dynamo.py b/tests/shared/infra/repositories/test_member_repository_dynamo.py index 0593222..81e9a96 100644 --- a/tests/shared/infra/repositories/test_member_repository_dynamo.py +++ b/tests/shared/infra/repositories/test_member_repository_dynamo.py @@ -8,7 +8,7 @@ class Test_MemberRepositoryDynamo: - #@pytest.mark.skip(reason="Needs dynamoDB") + @pytest.mark.skip(reason="Needs dynamoDB") def test_create_member(self): os.environ["STAGE"] = "TEST" @@ -19,7 +19,7 @@ def test_create_member(self): assert member_repository_mock.members[0].name == resp.name - #@pytest.mark.skip(reason="Needs dynamoDB") + @pytest.mark.skip(reason="Needs dynamoDB") def test_get_member(self): os.environ["STAGE"] = "TEST" @@ -32,7 +32,7 @@ def test_get_member(self): assert created.name == resp.name - #@pytest.mark.skip(reason="Needs dynamoDB") + @pytest.mark.skip(reason="Needs dynamoDB") def test_delete_member(self): os.environ["STAGE"] = "TEST" @@ -45,7 +45,7 @@ def test_delete_member(self): assert created.name == resp.name - #@pytest.mark.skip(reason="Needs dynamoDB") + @pytest.mark.skip(reason="Needs dynamoDB") def test_get_all_member(self): os.environ["STAGE"] = "TEST" @@ -58,7 +58,7 @@ def test_get_all_member(self): assert any(m.member_id == created.member_id for m in resp) - #@pytest.mark.skip(reason="Needs dynamoDB") + @pytest.mark.skip(reason="Needs dynamoDB") def test_update_member(self): os.environ["STAGE"] = "TEST" From 6c208df46fc2ff02a4043e04d05ab8d52ec24ad4 Mon Sep 17 00:00:00 2001 From: PedroOSanches Date: Wed, 16 Sep 2026 10:54:36 -0300 Subject: [PATCH 49/50] =?UTF-8?q?refactor:=20melhora=20a=20valida=C3=A7?= =?UTF-8?q?=C3=A3o=20do=20user=5Fid=20e=20atualiza=20a=20l=C3=B3gica=20de?= =?UTF-8?q?=20atualiza=C3=A7=C3=A3o=20de=20usu=C3=A1rio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../update_user/app/update_user_controller.py | 2 +- .../update_user/app/update_user_usecase.py | 11 ++++++++--- .../update_user/app/update_user_viewmodel.py | 5 +---- .../app/test_update_user_controller.py | 19 +++++++++++++++++-- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/modules/update_user/app/update_user_controller.py b/src/modules/update_user/app/update_user_controller.py index 692beda..5ee6716 100644 --- a/src/modules/update_user/app/update_user_controller.py +++ b/src/modules/update_user/app/update_user_controller.py @@ -42,7 +42,7 @@ def __call__(self, request: IRequest) -> IResponse: try: user_id = UUID(request.data.get('user_id')) except ValueError: - EntityError('user_id') + raise EntityError('user_id') user = self.UpdateUserUsecase( user_id=user_id, diff --git a/src/modules/update_user/app/update_user_usecase.py b/src/modules/update_user/app/update_user_usecase.py index c41041b..5898e0f 100644 --- a/src/modules/update_user/app/update_user_usecase.py +++ b/src/modules/update_user/app/update_user_usecase.py @@ -8,12 +8,17 @@ class UpdateUserUsecase: def __init__(self, repo: IUserRepository): self.repo = repo - def __call__(self, user_id: UUID, **user_atributes) -> User: + def __call__( + self, + user_id: UUID, + new_email: str | None = None, + new_role: str | None = None + ) -> User: stored_user = self.repo.get_user(user_id=user_id) user = User( user_id=user_id, - email=user_atributes.get('new_email') or stored_user.email, - role=user_atributes.get('new_role') or stored_user.role + email=new_email or stored_user.email, + role=new_role or stored_user.role ) updated_user = self.repo.update_user(user=user) diff --git a/src/modules/update_user/app/update_user_viewmodel.py b/src/modules/update_user/app/update_user_viewmodel.py index 0717425..bec54d5 100644 --- a/src/modules/update_user/app/update_user_viewmodel.py +++ b/src/modules/update_user/app/update_user_viewmodel.py @@ -3,10 +3,7 @@ class UpdateUserViewmodel: - user_id: int - name: str - email: str - state: STATE + user: User def __init__(self, user: User): self.user = user diff --git a/tests/modules/update_user/app/test_update_user_controller.py b/tests/modules/update_user/app/test_update_user_controller.py index 6e0a09d..26719ea 100644 --- a/tests/modules/update_user/app/test_update_user_controller.py +++ b/tests/modules/update_user/app/test_update_user_controller.py @@ -41,7 +41,7 @@ def test_update_user_controller_missing_user_id(self): assert response.body == "Field user_id is missing" - def test_update_user_controller_invalid_user_id(self): + def test_update_user_controller_invalid_type_user_id(self): repo = UserRepositoryMock() usecase = UpdateUserUsecase(repo=repo) controller = UpdateUserController(usecase=usecase) @@ -54,7 +54,22 @@ def test_update_user_controller_invalid_user_id(self): response = controller(request=request) assert response.status_code == 400 - assert response.body == "Field user_id isn't in the right type.\n Received: int.\n Expected: str"\ + assert response.body == "Field user_id isn't in the right type.\n Received: int.\n Expected: str" + + def test_update_user_controller_invalid_user_id(self): + repo = UserRepositoryMock() + usecase = UpdateUserUsecase(repo=repo) + controller = UpdateUserController(usecase=usecase) + + request = HttpRequest(body={ + 'user_id': 'a', + 'new_email': 'user@epep.com' + }) + + response = controller(request=request) + + assert response.status_code == 400 + assert response.body == 'Field user_id is not valid' def test_update_user_controller_invalid_email_type(self): repo = UserRepositoryMock() From 2f75dc6b4ce8cbbb92dd82981bf1ce6d15520af7 Mon Sep 17 00:00:00 2001 From: Leo Iorio Date: Sat, 3 Oct 2026 11:09:12 -0300 Subject: [PATCH 50/50] =?UTF-8?q?fix:=20alinha=20update=5Fuser=20com=20men?= =?UTF-8?q?sagens=20e=20limpeza=20p=C3=B3s-merge=20de=20dev?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/update_user/app/update_user_usecase.py | 1 - src/modules/update_user/app/update_user_viewmodel.py | 1 - .../modules/update_user/app/test_update_user_controller.py | 6 +++--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/modules/update_user/app/update_user_usecase.py b/src/modules/update_user/app/update_user_usecase.py index 5898e0f..5875a18 100644 --- a/src/modules/update_user/app/update_user_usecase.py +++ b/src/modules/update_user/app/update_user_usecase.py @@ -1,7 +1,6 @@ from uuid import UUID from src.shared.domain.entities.user import User from src.shared.domain.repositories.user_repository_interface import IUserRepository -from src.shared.helpers.errors.domain_errors import EntityError class UpdateUserUsecase: diff --git a/src/modules/update_user/app/update_user_viewmodel.py b/src/modules/update_user/app/update_user_viewmodel.py index bec54d5..6169d47 100644 --- a/src/modules/update_user/app/update_user_viewmodel.py +++ b/src/modules/update_user/app/update_user_viewmodel.py @@ -1,5 +1,4 @@ from src.shared.domain.entities.user import User -from src.shared.domain.enums.state_enum import STATE class UpdateUserViewmodel: diff --git a/tests/modules/update_user/app/test_update_user_controller.py b/tests/modules/update_user/app/test_update_user_controller.py index 26719ea..c7f50a0 100644 --- a/tests/modules/update_user/app/test_update_user_controller.py +++ b/tests/modules/update_user/app/test_update_user_controller.py @@ -54,7 +54,7 @@ def test_update_user_controller_invalid_type_user_id(self): response = controller(request=request) assert response.status_code == 400 - assert response.body == "Field user_id isn't in the right type.\n Received: int.\n Expected: str" + assert response.body == "The field 'user_id' has the wrong type. Received: 'int'. Expected: 'str'." def test_update_user_controller_invalid_user_id(self): repo = UserRepositoryMock() @@ -84,7 +84,7 @@ def test_update_user_controller_invalid_email_type(self): response = controller(request=request) assert response.status_code == 400 - assert response.body == "Field new_email isn't in the right type.\n Received: int.\n Expected: str" + assert response.body == "The field 'new_email' has the wrong type. Received: 'int'. Expected: 'str'." def test_update_user_controller_invalid_role(self): repo = UserRepositoryMock() @@ -99,7 +99,7 @@ def test_update_user_controller_invalid_role(self): response = controller(request=request) assert response.status_code == 400 - assert response.body == "Field new_role isn't in the right type.\n Received: int.\n Expected: str" + assert response.body == "The field 'new_role' has the wrong type. Received: 'int'. Expected: 'str'." def test_update_user_not_found(self): repo = UserRepositoryMock()