From 8c27afeb43c36f0708782b709bb87a3e98facf40 Mon Sep 17 00:00:00 2001 From: veloy25 Date: Thu, 23 Jul 2026 22:05:31 -0300 Subject: [PATCH 01/18] fix(get_bookings): add requester role handling and user owner details retrieval --- .../get_bookings/app/get_bookings_usecase.py | 25 +++++++++++++++++-- .../app/test_get_bookings_usecase.py | 18 +++++++------ 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/modules/get_bookings/app/get_bookings_usecase.py b/src/modules/get_bookings/app/get_bookings_usecase.py index 1290fda..b7421f5 100644 --- a/src/modules/get_bookings/app/get_bookings_usecase.py +++ b/src/modules/get_bookings/app/get_bookings_usecase.py @@ -4,6 +4,7 @@ from src.shared.domain.enums.sport import SPORT from src.shared.domain.enums.type import BOOKING_TYPE from src.shared.domain.repositories.booking_repository_interface import IBookingRepository +from src.shared.clients.user_api_client import UserAPIClient from src.shared.helpers.errors.domain_errors import EntityError from src.shared.helpers.errors.usecase_errors import NoItemsFound, DependantFilter @@ -20,7 +21,8 @@ def __call__(self, court_number: Optional[int] = None, end_date: Optional[int] = None, start_date: Optional[int] = None, - booking_type: Optional[str] = None): + booking_type: Optional[str] = None, + requester_role: str = None): if booking_id: if not Booking.validate_booking_id(booking_id): @@ -56,4 +58,23 @@ def __call__(self, if bookings is None or bookings == []: raise NoItemsFound('booking filters passed') - return bookings + owner_list = [] + + for booking in bookings: + if requester_role == 'ADMIN': + client = self.user_client or UserAPIClient() + try: + owner = { + 'name': client.get_user_name(booking.user_id), + 'network_id': client.get_user_network_id(booking.user_id), + } + owner_list.append(owner) + except Exception as e: + + print(f"ERRO NA API DE USER: {e}") + owner = { + 'name': 'Erro de integração', + 'network_id': 'Erro de integração', + } + + return {'bookings': bookings, 'owner': owner_list} diff --git a/tests/modules/get_bookings/app/test_get_bookings_usecase.py b/tests/modules/get_bookings/app/test_get_bookings_usecase.py index de767b0..54bff0f 100644 --- a/tests/modules/get_bookings/app/test_get_bookings_usecase.py +++ b/tests/modules/get_bookings/app/test_get_bookings_usecase.py @@ -9,18 +9,20 @@ class TestGetBookingsUseCase: def test_get_bookings_usecase(self): repo = BookingRepositoryMock() usecase = GetBookingsUseCase(repo=repo) - booking = repo.bookings[0].booking_id - response = usecase(booking) - assert response[0] == repo.bookings[0] + booking_id = repo.bookings[0].booking_id + response = usecase(booking_id=booking_id) + + assert response['bookings'][0] == repo.bookings[0] + assert response['owner'] == [] def test_get_bookings_usecase_invalid_id(self): with pytest.raises(EntityError): repo = BookingRepositoryMock() - usecase = GetBookingsUseCase(repo = repo) - usecase('invalid_id') + usecase = GetBookingsUseCase(repo=repo) + usecase(booking_id='invalid_id') def test_get_bookings_usecase_no_items_found(self): - with pytest.raises(NoItemsFound): + with pytest.raises(NoItemsFound): repo = BookingRepositoryMock() - usecase = GetBookingsUseCase(repo = repo) - usecase('b3d3b3b3-dc0d-4fc1-861c-506a40cc2925') \ No newline at end of file + usecase = GetBookingsUseCase(repo=repo) + usecase(booking_id='b3d3b3b3-dc0d-4fc1-861c-506a40cc2925') \ No newline at end of file From 14bec38c7a503e587d908e49cd6f78d6b926a48d Mon Sep 17 00:00:00 2001 From: veloy25 Date: Thu, 23 Jul 2026 22:05:57 -0300 Subject: [PATCH 02/18] fix(get_bookings_viewmodel): enhance initialization to handle owner list and update to_dict method for owner details --- .../app/get_bookings_viewmodel.py | 29 +++++++++++++++---- .../app/test_get_bookings_viewmodel.py | 4 +-- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/modules/get_bookings/app/get_bookings_viewmodel.py b/src/modules/get_bookings/app/get_bookings_viewmodel.py index fefc788..c8d9e86 100644 --- a/src/modules/get_bookings/app/get_bookings_viewmodel.py +++ b/src/modules/get_bookings/app/get_bookings_viewmodel.py @@ -5,14 +5,31 @@ class GetBookingsViewmodel: bookings: List[Booking] - def __init__(self, bookings: list): - self.bookings = bookings + def __init__(self, bookings: list, owner_list: list = None): + if isinstance(bookings, dict): + data = bookings + self.bookings = data.get('bookings', []) + self.owner_list = data.get('owner', []) + else: + self.bookings = bookings or [] + self.owner_list = owner_list or [] def to_dict(self): + bookings_response = [] + + for index, booking in enumerate(self.bookings): + booking_dict = { + k: v for k, v in booking.to_dict().items() if k != 'user_id' + } + + if index < len(self.owner_list): + owner = self.owner_list[index] + booking_dict['owner_name'] = owner.get('name') + booking_dict['owner_network_id'] = owner.get('network_id') + + bookings_response.append(booking_dict) + return { - 'bookings': [ - {k: v for k, v in booking.to_dict().items() if k != 'user_id'} - for booking in self.bookings - ], + 'bookings': bookings_response, 'message': 'the bookings were retrieved' } diff --git a/tests/modules/get_bookings/app/test_get_bookings_viewmodel.py b/tests/modules/get_bookings/app/test_get_bookings_viewmodel.py index d5bdb4f..41d9553 100644 --- a/tests/modules/get_bookings/app/test_get_bookings_viewmodel.py +++ b/tests/modules/get_bookings/app/test_get_bookings_viewmodel.py @@ -6,9 +6,9 @@ class Test_GetBookingsViewModel: def test_get_bookings_viewmodel(self): repo = BookingRepositoryMock() usecase = GetBookingsUseCase(repo) - booking = usecase(booking_id=repo.bookings[0].booking_id) + response = usecase(booking_id=repo.bookings[0].booking_id) - viewmodel = GetBookingsViewmodel(bookings=booking).to_dict() + viewmodel = GetBookingsViewmodel(bookings=response['bookings'], owner_list=response['owner']).to_dict() expected = { 'bookings': [{ From 9339aa917b68194d6fde5ef039614e2eb031e954 Mon Sep 17 00:00:00 2001 From: veloy25 Date: Thu, 23 Jul 2026 22:17:44 -0300 Subject: [PATCH 03/18] refactor(get_bookings_presenter): encapsulate controller initialization in a dedicated function --- src/modules/get_bookings/app/get_bookings_presenter.py | 10 ++++++---- .../get_bookings/app/test_get_bookings_presenter.py | 2 ++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/modules/get_bookings/app/get_bookings_presenter.py b/src/modules/get_bookings/app/get_bookings_presenter.py index 1815d57..d215a2d 100644 --- a/src/modules/get_bookings/app/get_bookings_presenter.py +++ b/src/modules/get_bookings/app/get_bookings_presenter.py @@ -2,15 +2,17 @@ from .get_bookings_usecase import GetBookingsUseCase from src.shared.environments import Environments from src.shared.helpers.external_interfaces.http_lambda_requests import LambdaHttpRequest, LambdaHttpResponse -from src.shared.infra.repositories.booking_repository_mock import BookingRepositoryMock -repo = Environments.get_booking_repo()() -usecase = GetBookingsUseCase(repo=repo) -controller = GetBookingsController(usecase=usecase) + +def _build_controller(): + repo = Environments.get_booking_repo()() + usecase = GetBookingsUseCase(repo=repo) + return GetBookingsController(usecase=usecase) def lambda_handler(event, context): httpRequest = LambdaHttpRequest(data=event) + controller = _build_controller() response = controller(request=httpRequest) httpResponse = LambdaHttpResponse(status_code=response.status_code, body=response.body, headers=response.headers) diff --git a/tests/modules/get_bookings/app/test_get_bookings_presenter.py b/tests/modules/get_bookings/app/test_get_bookings_presenter.py index c58e4d7..477e90c 100644 --- a/tests/modules/get_bookings/app/test_get_bookings_presenter.py +++ b/tests/modules/get_bookings/app/test_get_bookings_presenter.py @@ -1,4 +1,6 @@ import json +import os +os.environ['STAGE'] = 'TEST' from src.modules.get_bookings.app.get_bookings_presenter import lambda_handler from src.shared.infra.repositories.booking_repository_mock import BookingRepositoryMock From 9df4ef6ff53f59f8bdcad0d959475fa134ea53d9 Mon Sep 17 00:00:00 2001 From: veloy25 Date: Fri, 24 Jul 2026 17:37:04 -0300 Subject: [PATCH 04/18] fix(get_bookings_presenter): enhance user role handling and JSON parsing in lambda handler --- .../get_bookings/app/get_bookings_controller.py | 6 +++++- .../get_bookings/app/get_bookings_presenter.py | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/modules/get_bookings/app/get_bookings_controller.py b/src/modules/get_bookings/app/get_bookings_controller.py index 10e7781..4b381b5 100644 --- a/src/modules/get_bookings/app/get_bookings_controller.py +++ b/src/modules/get_bookings/app/get_bookings_controller.py @@ -61,6 +61,9 @@ def __call__(self, request: IRequest): fieldTypeExpected='int', fieldTypeReceived=start_date) + user = request.data.get('user_from_authorizer') + requester_role = user.get('role') if isinstance(user, dict) else None + booking = self.usecase( booking_id=booking_id, user_id=user_id, @@ -68,7 +71,8 @@ def __call__(self, request: IRequest): court_number=court_number, end_date=end_date, start_date=start_date, - booking_type=booking_type + booking_type=booking_type, + requester_role=requester_role ) booking_viewmodel = GetBookingsViewmodel(booking) return OK(booking_viewmodel.to_dict()) diff --git a/src/modules/get_bookings/app/get_bookings_presenter.py b/src/modules/get_bookings/app/get_bookings_presenter.py index d215a2d..68b4bb1 100644 --- a/src/modules/get_bookings/app/get_bookings_presenter.py +++ b/src/modules/get_bookings/app/get_bookings_presenter.py @@ -1,3 +1,5 @@ +import json + from .get_bookings_controller import GetBookingsController from .get_bookings_usecase import GetBookingsUseCase from src.shared.environments import Environments @@ -12,6 +14,18 @@ def _build_controller(): def lambda_handler(event, context): httpRequest = LambdaHttpRequest(data=event) + + user_info = event.get('requestContext', {}).get('authorizer', {}).get('user') + if isinstance(user_info, str): + try: + user_info = json.loads(user_info) + except json.JSONDecodeError: + user_info = None + elif not isinstance(user_info, dict): + user_info = None + + httpRequest.data['user_from_authorizer'] = user_info + controller = _build_controller() response = controller(request=httpRequest) httpResponse = LambdaHttpResponse(status_code=response.status_code, body=response.body, headers=response.headers) From b371bad68853efb17b3967d49285a90efbd270b5 Mon Sep 17 00:00:00 2001 From: veloy25 Date: Fri, 24 Jul 2026 17:52:10 -0300 Subject: [PATCH 05/18] fix(get_bookings_usecase): improve user client handling based on environment variable and enhance error handling for owner details --- .../get_bookings/app/get_bookings_usecase.py | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/modules/get_bookings/app/get_bookings_usecase.py b/src/modules/get_bookings/app/get_bookings_usecase.py index b7421f5..96e1d8f 100644 --- a/src/modules/get_bookings/app/get_bookings_usecase.py +++ b/src/modules/get_bookings/app/get_bookings_usecase.py @@ -4,6 +4,7 @@ from src.shared.domain.enums.sport import SPORT from src.shared.domain.enums.type import BOOKING_TYPE from src.shared.domain.repositories.booking_repository_interface import IBookingRepository +import os from src.shared.clients.user_api_client import UserAPIClient from src.shared.helpers.errors.domain_errors import EntityError from src.shared.helpers.errors.usecase_errors import NoItemsFound, DependantFilter @@ -59,22 +60,29 @@ def __call__(self, raise NoItemsFound('booking filters passed') owner_list = [] - + for booking in bookings: if requester_role == 'ADMIN': - client = self.user_client or UserAPIClient() + if os.environ.get('USER_API_URL'): + client = getattr(self, 'user_client', None) or UserAPIClient() + else: + client = None + try: - owner = { - 'name': client.get_user_name(booking.user_id), - 'network_id': client.get_user_network_id(booking.user_id), - } - owner_list.append(owner) + if client: + owner = { + 'name': client.get_user_name(booking.user_id), + 'network_id': client.get_user_network_id(booking.user_id), + } + else: + owner = {'name': None, 'network_id': None} except Exception as e: - print(f"ERRO NA API DE USER: {e}") owner = { 'name': 'Erro de integração', 'network_id': 'Erro de integração', } - + + owner_list.append(owner) + return {'bookings': bookings, 'owner': owner_list} From 7c95dc796cbd3842da996ae69ff3ed59e034f10e Mon Sep 17 00:00:00 2001 From: veloy25 Date: Fri, 24 Jul 2026 19:26:29 -0300 Subject: [PATCH 06/18] fix(get_bookings): enhance user information handling in lambda handler and improve user client initialization for admin role --- .../app/get_bookings_presenter.py | 11 ++- .../get_bookings/app/get_bookings_usecase.py | 7 +- .../app/test_get_bookings_presenter.py | 79 +++++++++++++++++++ .../app/test_get_bookings_usecase.py | 19 +++++ 4 files changed, 108 insertions(+), 8 deletions(-) diff --git a/src/modules/get_bookings/app/get_bookings_presenter.py b/src/modules/get_bookings/app/get_bookings_presenter.py index 68b4bb1..881bd13 100644 --- a/src/modules/get_bookings/app/get_bookings_presenter.py +++ b/src/modules/get_bookings/app/get_bookings_presenter.py @@ -15,13 +15,16 @@ def _build_controller(): def lambda_handler(event, context): httpRequest = LambdaHttpRequest(data=event) - user_info = event.get('requestContext', {}).get('authorizer', {}).get('user') - if isinstance(user_info, str): + user_info_value = event.get('requestContext', {}).get('authorizer', {}).get('user') + if isinstance(user_info_value, str): try: - user_info = json.loads(user_info) + parsed_user = json.loads(user_info_value) + user_info = parsed_user.get('user') if isinstance(parsed_user, dict) else None except json.JSONDecodeError: user_info = None - elif not isinstance(user_info, dict): + elif isinstance(user_info_value, dict): + user_info = user_info_value + else: user_info = None httpRequest.data['user_from_authorizer'] = user_info diff --git a/src/modules/get_bookings/app/get_bookings_usecase.py b/src/modules/get_bookings/app/get_bookings_usecase.py index 96e1d8f..476cf50 100644 --- a/src/modules/get_bookings/app/get_bookings_usecase.py +++ b/src/modules/get_bookings/app/get_bookings_usecase.py @@ -63,10 +63,9 @@ def __call__(self, for booking in bookings: if requester_role == 'ADMIN': - if os.environ.get('USER_API_URL'): - client = getattr(self, 'user_client', None) or UserAPIClient() - else: - client = None + client = getattr(self, 'user_client', None) + if client is None and os.environ.get('USER_API_URL'): + client = UserAPIClient() try: if client: diff --git a/tests/modules/get_bookings/app/test_get_bookings_presenter.py b/tests/modules/get_bookings/app/test_get_bookings_presenter.py index 477e90c..719a8a2 100644 --- a/tests/modules/get_bookings/app/test_get_bookings_presenter.py +++ b/tests/modules/get_bookings/app/test_get_bookings_presenter.py @@ -71,6 +71,85 @@ def test_get_bookings_presenter(self): assert json.loads(response['body'])['bookings'][0]['sport'] == 'Tennis' assert json.loads(response['body'])['bookings'][0]['materials'] == ['Raquete', 'Bola', 'Rede', 'Tenis'] + def test_get_bookings_presenter_admin_owner_fields(self, monkeypatch): + from src.modules.get_bookings.app.get_bookings_usecase import UserAPIClient + + class FakeUserClient: + def __init__(self): + pass + + def get_user_name(self, user_id): + return 'CEAF MAUA' + + def get_user_network_id(self, user_id): + return 'ceaf' + + monkeypatch.setattr('src.modules.get_bookings.app.get_bookings_usecase.UserAPIClient', FakeUserClient) + monkeypatch.setenv('USER_API_URL', 'http://fake-url/') + + 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": { + "booking_id": "b1d3bebf-dc0d-4fc1-861c-506a40cc2925", + "user_id": "", + "sport": "", + "court_number": "", + "end_date": "", + "start_date": "" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "", + "authentication": None, + "authorizer": { + "user": json.dumps({ + "user": { + "id": "1f25448b-3429-4c19-8287-d9e64f17bc3a", + "displayName": "User", + "mail": "lbj@maua.br", + "role": "ADMIN" + } + }) + }, + "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": {}, + "pathParameters": None, + "isBase64Encoded": None, + "stageVariables": None + } + + response = lambda_handler(event, None) + body = json.loads(response['body']) + + assert response['statusCode'] == 200 + assert body['bookings'][0]['owner_name'] == 'CEAF MAUA' + assert body['bookings'][0]['owner_network_id'] == 'ceaf' + def test_get_bookings_presenter_missing_parameters(self): event = { diff --git a/tests/modules/get_bookings/app/test_get_bookings_usecase.py b/tests/modules/get_bookings/app/test_get_bookings_usecase.py index 54bff0f..e8bd44a 100644 --- a/tests/modules/get_bookings/app/test_get_bookings_usecase.py +++ b/tests/modules/get_bookings/app/test_get_bookings_usecase.py @@ -5,6 +5,14 @@ from src.shared.helpers.errors.domain_errors import EntityError from src.shared.helpers.errors.usecase_errors import NoItemsFound +class FakeUserClient: + def get_user_name(self, user_id): + return 'CEAF MAUA' + + def get_user_network_id(self, user_id): + return 'ceaf' + + class TestGetBookingsUseCase: def test_get_bookings_usecase(self): repo = BookingRepositoryMock() @@ -14,6 +22,17 @@ def test_get_bookings_usecase(self): assert response['bookings'][0] == repo.bookings[0] assert response['owner'] == [] + + def test_get_bookings_usecase_admin_returns_owner_metadata(self): + repo = BookingRepositoryMock() + usecase = GetBookingsUseCase(repo=repo) + usecase.user_client = FakeUserClient() + booking_id = repo.bookings[0].booking_id + + response = usecase(booking_id=booking_id, requester_role='ADMIN') + + assert response['bookings'][0] == repo.bookings[0] + assert response['owner'] == [{'name': 'CEAF MAUA', 'network_id': 'ceaf'}] def test_get_bookings_usecase_invalid_id(self): with pytest.raises(EntityError): From 1dff22b00d7d3d4f0610ac86e5fb063bb43e0790 Mon Sep 17 00:00:00 2001 From: veloy25 Date: Fri, 24 Jul 2026 20:13:29 -0300 Subject: [PATCH 07/18] fix(get_bookings_usecase): enhance user client initialization and improve owner data retrieval for admin role --- .../get_bookings/app/get_bookings_usecase.py | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/modules/get_bookings/app/get_bookings_usecase.py b/src/modules/get_bookings/app/get_bookings_usecase.py index 476cf50..e60cebf 100644 --- a/src/modules/get_bookings/app/get_bookings_usecase.py +++ b/src/modules/get_bookings/app/get_bookings_usecase.py @@ -12,8 +12,10 @@ class GetBookingsUseCase: repo: IBookingRepository - def __init__(self, repo: IBookingRepository): + def __init__(self, repo: IBookingRepository, user_client=None): self.repo = repo + self.user_client = user_client + def __call__(self, booking_id: Optional[str] = None, @@ -60,28 +62,22 @@ def __call__(self, raise NoItemsFound('booking filters passed') owner_list = [] - + for booking in bookings: + owner = None if requester_role == 'ADMIN': - client = getattr(self, 'user_client', None) - if client is None and os.environ.get('USER_API_URL'): - client = UserAPIClient() - + client = self.user_client or UserAPIClient() try: - if client: - owner = { - 'name': client.get_user_name(booking.user_id), - 'network_id': client.get_user_network_id(booking.user_id), - } - else: - owner = {'name': None, 'network_id': None} + owner = { + 'name': client.get_user_name(booking.user_id), + 'network_id': client.get_user_network_id(booking.user_id), + } except Exception as e: print(f"ERRO NA API DE USER: {e}") owner = { 'name': 'Erro de integração', 'network_id': 'Erro de integração', } - - owner_list.append(owner) + owner_list.append(owner) return {'bookings': bookings, 'owner': owner_list} From 04eb1485ef093d4c8570a6bbf5d8c204e55dec82 Mon Sep 17 00:00:00 2001 From: veloy25 Date: Fri, 24 Jul 2026 20:13:35 -0300 Subject: [PATCH 08/18] fix(get_bookings_viewmodel): improve owner data retrieval by ensuring owner_list is not None --- src/modules/get_bookings/app/get_bookings_viewmodel.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/modules/get_bookings/app/get_bookings_viewmodel.py b/src/modules/get_bookings/app/get_bookings_viewmodel.py index c8d9e86..8bbd329 100644 --- a/src/modules/get_bookings/app/get_bookings_viewmodel.py +++ b/src/modules/get_bookings/app/get_bookings_viewmodel.py @@ -22,11 +22,10 @@ def to_dict(self): k: v for k, v in booking.to_dict().items() if k != 'user_id' } - if index < len(self.owner_list): + if index < len(self.owner_list) and self.owner_list[index] is not None: owner = self.owner_list[index] booking_dict['owner_name'] = owner.get('name') booking_dict['owner_network_id'] = owner.get('network_id') - bookings_response.append(booking_dict) return { From 2691b61eb9e13f559337e883854dbcdfcb74f708 Mon Sep 17 00:00:00 2001 From: veloy25 Date: Fri, 24 Jul 2026 20:27:34 -0300 Subject: [PATCH 09/18] fix(get_bookings_usecase): correct owner assignment logic in booking retrieval --- src/modules/get_bookings/app/get_bookings_usecase.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/modules/get_bookings/app/get_bookings_usecase.py b/src/modules/get_bookings/app/get_bookings_usecase.py index e60cebf..437acaf 100644 --- a/src/modules/get_bookings/app/get_bookings_usecase.py +++ b/src/modules/get_bookings/app/get_bookings_usecase.py @@ -64,7 +64,6 @@ def __call__(self, owner_list = [] for booking in bookings: - owner = None if requester_role == 'ADMIN': client = self.user_client or UserAPIClient() try: @@ -78,6 +77,6 @@ def __call__(self, 'name': 'Erro de integração', 'network_id': 'Erro de integração', } - owner_list.append(owner) + owner_list.append(owner) return {'bookings': bookings, 'owner': owner_list} From 49c3eecfe141b76742fe511faee45990aa5c3c1b Mon Sep 17 00:00:00 2001 From: veloy25 Date: Fri, 24 Jul 2026 20:39:39 -0300 Subject: [PATCH 10/18] fix(get_bookings_usecase): add debug print for requester_role in booking retrieval --- src/modules/get_bookings/app/get_bookings_usecase.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/get_bookings/app/get_bookings_usecase.py b/src/modules/get_bookings/app/get_bookings_usecase.py index 437acaf..e5d86a2 100644 --- a/src/modules/get_bookings/app/get_bookings_usecase.py +++ b/src/modules/get_bookings/app/get_bookings_usecase.py @@ -62,6 +62,7 @@ def __call__(self, raise NoItemsFound('booking filters passed') owner_list = [] + print(f"[DEBUG] requester_role recebido: {requester_role}") for booking in bookings: if requester_role == 'ADMIN': From 2affcd9de8661c7a603010814621a4a475e48522 Mon Sep 17 00:00:00 2001 From: veloy25 Date: Fri, 24 Jul 2026 20:51:05 -0300 Subject: [PATCH 11/18] fix(lambda_construct): add authorizer to API Gateway integration for secure access --- iac/components/lambda_construct.py | 1 + 1 file changed, 1 insertion(+) diff --git a/iac/components/lambda_construct.py b/iac/components/lambda_construct.py index 53da80b..d6da624 100644 --- a/iac/components/lambda_construct.py +++ b/iac/components/lambda_construct.py @@ -156,6 +156,7 @@ def __init__( method="GET", api_resource=api_gateway_resource, environment_variables=environment_variables, + authorizer=token_authorizer_lambda ) self.delete_booking = self.create_lambda_api_gateway_integration( From 5da5ba749e8a73ece013f15a932b5ac413290099 Mon Sep 17 00:00:00 2001 From: Victor Gasperi Date: Wed, 5 Aug 2026 17:24:16 -0300 Subject: [PATCH 12/18] test(authorizer): adiciona testes de caracterizacao do user_mss_authorizer --- tests/shared/authorizer/__init__.py | 0 .../authorizer/test_user_mss_authorizer.py | 100 ++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 tests/shared/authorizer/__init__.py create mode 100644 tests/shared/authorizer/test_user_mss_authorizer.py diff --git a/tests/shared/authorizer/__init__.py b/tests/shared/authorizer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/shared/authorizer/test_user_mss_authorizer.py b/tests/shared/authorizer/test_user_mss_authorizer.py new file mode 100644 index 0000000..3de6a44 --- /dev/null +++ b/tests/shared/authorizer/test_user_mss_authorizer.py @@ -0,0 +1,100 @@ +import json + +from src.shared.authorizer import user_mss_authorizer +from src.shared.authorizer.user_mss_authorizer import generate_policy, lambda_handler + +METHOD_ARN = 'arn:aws:execute-api:sa-east-1:123456789012:abcdef/DEV/GET/get-bookings' + + +class FakeResponse: + def __init__(self, status, payload): + self.status = status + self.data = json.dumps(payload).encode('utf-8') + + +class FakePoolManager: + """Substitui urllib3.PoolManager. Registra as chamadas feitas em `calls`.""" + + calls = [] + + def __init__(self, status=200, payload=None): + self.status = status + self.payload = payload if payload is not None else { + 'id': '1f25448b-3429-4c19-8287-d9e64f17bc3a', + 'name': 'CEAF MAUA', + 'role': 'ADMIN', + } + + def request(self, method, url, headers=None): + FakePoolManager.calls.append({'method': method, 'url': url, 'headers': headers}) + return FakeResponse(self.status, self.payload) + + +def install_pool_manager(monkeypatch, status=200, payload=None): + FakePoolManager.calls = [] + monkeypatch.setattr( + user_mss_authorizer.urllib3, + 'PoolManager', + lambda *args, **kwargs: FakePoolManager(status=status, payload=payload), + ) + + +class TestGeneratePolicy: + def test_generate_policy_allow_with_context(self): + policy = generate_policy('user-1', 'Allow', METHOD_ARN, {'user': '{"a": 1}'}) + + assert policy['principalId'] == 'user-1' + assert policy['policyDocument']['Statement'][0]['Effect'] == 'Allow' + assert policy['policyDocument']['Statement'][0]['Resource'] == METHOD_ARN + assert policy['context'] == {'user': '{"a": 1}'} + + def test_generate_policy_deny_without_context(self): + policy = generate_policy('user', 'Deny', METHOD_ARN) + + assert policy['principalId'] == 'user' + assert policy['policyDocument']['Statement'][0]['Effect'] == 'Deny' + assert 'context' not in policy + + +class TestLambdaHandler: + def test_lambda_handler_valid_token_returns_allow_with_user_context(self, monkeypatch): + monkeypatch.setenv('USER_API_URL', 'http://fake-user-api/') + install_pool_manager(monkeypatch) + + event = {'authorizationToken': 'Bearer valid-token', 'methodArn': METHOD_ARN} + policy = lambda_handler(event, None) + + assert policy['principalId'] == '1f25448b-3429-4c19-8287-d9e64f17bc3a' + assert policy['policyDocument']['Statement'][0]['Effect'] == 'Allow' + assert json.loads(policy['context']['user'])['role'] == 'ADMIN' + assert FakePoolManager.calls[0]['url'] == 'http://fake-user-api/get-user' + assert FakePoolManager.calls[0]['headers'] == {'Authorization': 'Bearer valid-token'} + + def test_lambda_handler_non_200_returns_deny(self, monkeypatch): + monkeypatch.setenv('USER_API_URL', 'http://fake-user-api/') + install_pool_manager(monkeypatch, status=401) + + event = {'authorizationToken': 'Bearer expired-token', 'methodArn': METHOD_ARN} + policy = lambda_handler(event, None) + + assert policy['principalId'] == 'user' + assert policy['policyDocument']['Statement'][0]['Effect'] == 'Deny' + assert 'context' not in policy + + def test_lambda_handler_missing_env_var_returns_deny(self, monkeypatch): + monkeypatch.delenv('USER_API_URL', raising=False) + install_pool_manager(monkeypatch) + + event = {'authorizationToken': 'Bearer valid-token', 'methodArn': METHOD_ARN} + policy = lambda_handler(event, None) + + assert policy['policyDocument']['Statement'][0]['Effect'] == 'Deny' + + def test_lambda_handler_missing_authorization_token_returns_deny(self, monkeypatch): + monkeypatch.setenv('USER_API_URL', 'http://fake-user-api/') + install_pool_manager(monkeypatch) + + event = {'methodArn': METHOD_ARN} + policy = lambda_handler(event, None) + + assert policy['policyDocument']['Statement'][0]['Effect'] == 'Deny' From 29cdc4677b240c232204caf2072066d3af225ff6 Mon Sep 17 00:00:00 2001 From: Victor Gasperi Date: Wed, 5 Aug 2026 17:28:18 -0300 Subject: [PATCH 13/18] refactor(authorizer): extrai _fetch_user_data e _get_authorization_header --- src/shared/authorizer/user_mss_authorizer.py | 97 ++++++++++++------- .../authorizer/test_user_mss_authorizer.py | 40 ++++++++ 2 files changed, 102 insertions(+), 35 deletions(-) diff --git a/src/shared/authorizer/user_mss_authorizer.py b/src/shared/authorizer/user_mss_authorizer.py index 21da681..2c8ca1a 100644 --- a/src/shared/authorizer/user_mss_authorizer.py +++ b/src/shared/authorizer/user_mss_authorizer.py @@ -5,10 +5,64 @@ from src.shared.environments import Environments +def _get_authorization_header(headers): + ''' + Extracts the Authorization header from a REQUEST authorizer event, case-insensitively. + + Args: + headers (dict): The headers received in the event. + + Returns: + str | None: The raw header value, or None when it is not present. + ''' + + if not headers: + return None + + for key, value in headers.items(): + if key.lower() == "authorization": + return value + + return None + + +def _fetch_user_data(token): + ''' + Fetches the user information from the user mss using the given token. + + Args: + token (str): The bearer token, already stripped of the "Bearer " prefix. + + Returns: + dict: The user data returned by the user mss. + + Raises: + Exception: When USER_API_URL is not set or the user mss does not answer with 200. + ''' + + # Fetch the User Mss enpoint from the environment variables + MSS_USER_API_ENDPOINT = os.environ.get("USER_API_URL") + if not MSS_USER_API_ENDPOINT: + raise Exception("MSS_USER_ENDPOINT environment variable not set") + + # Creating a HTTP client + http = urllib3.PoolManager() + + # Fetching the user information from the user mss + headers = {"Authorization": f"Bearer {token}"} + response = http.request("GET", MSS_USER_API_ENDPOINT + "get-user", headers=headers) + + # Checking if the request was successful + if response.status != 200: + raise Exception("Failed to fetch user information") + + # Parsing the user data + return json.loads(response.data.decode("utf-8")) + + def lambda_handler(event, context): """ - This function is used to authorize the user to access the API Gateway. - It uses the Microsoft Graph API to fetch the user information and check if the user is from Maua. + TOKEN authorizer. Requires a valid Bearer token — used by every protected route. Args: event (dict): The event data passed to the Lambda function. @@ -18,47 +72,20 @@ def lambda_handler(event, context): dict: The response object containing the policy document. """ - try: - - # Fetch the User Mss enpoint from the environment variables - MSS_USER_API_ENDPOINT = os.environ.get("USER_API_URL") - if not MSS_USER_API_ENDPOINT: - raise Exception("MSS_USER_ENDPOINT environment variable not set") - - # Creating a HTTP client - http = urllib3.PoolManager() + method_arn = event["methodArn"] - # Extracting the token from the event data + try: token = event["authorizationToken"].replace("Bearer ", "") + user_data = _fetch_user_data(token) - # Fetching the user information from the user mss - methodArn = event["methodArn"] - headers = {"Authorization": f"Bearer {token}"} - response = http.request("GET", MSS_USER_API_ENDPOINT + "get-user", headers=headers) - - # Checking if the request was successful - if response.status != 200: - raise Exception("Failed to fetch user information") - - # Parsing the user data - user_data = json.loads(response.data.decode("utf-8")) - - print("CHECK BEFORE REGEX") - print(user_data) - - policy = generate_policy( - user_data.get("id", "user"), "Allow", methodArn, {"user": json.dumps(user_data)} + return generate_policy( + user_data.get("id", "user"), "Allow", method_arn, {"user": json.dumps(user_data)} ) - print(policy) - - return policy - # Handling exceptions except Exception as e: print(f"Error: {e}") - methodArn = event["methodArn"] - return generate_policy("user", "Deny", methodArn) + return generate_policy("user", "Deny", method_arn) def generate_policy(principal_id, effect, method_arn, context=None): diff --git a/tests/shared/authorizer/test_user_mss_authorizer.py b/tests/shared/authorizer/test_user_mss_authorizer.py index 3de6a44..1354af5 100644 --- a/tests/shared/authorizer/test_user_mss_authorizer.py +++ b/tests/shared/authorizer/test_user_mss_authorizer.py @@ -1,5 +1,7 @@ import json +import pytest + from src.shared.authorizer import user_mss_authorizer from src.shared.authorizer.user_mss_authorizer import generate_policy, lambda_handler @@ -98,3 +100,41 @@ def test_lambda_handler_missing_authorization_token_returns_deny(self, monkeypat policy = lambda_handler(event, None) assert policy['policyDocument']['Statement'][0]['Effect'] == 'Deny' + + +class TestGetAuthorizationHeader: + def test_get_authorization_header_exact_case(self): + headers = {'Authorization': 'Bearer abc', 'Content-Type': 'application/json'} + assert user_mss_authorizer._get_authorization_header(headers) == 'Bearer abc' + + def test_get_authorization_header_lowercase(self): + headers = {'authorization': 'Bearer abc'} + assert user_mss_authorizer._get_authorization_header(headers) == 'Bearer abc' + + def test_get_authorization_header_absent(self): + assert user_mss_authorizer._get_authorization_header({'Content-Type': 'application/json'}) is None + + def test_get_authorization_header_none_headers(self): + assert user_mss_authorizer._get_authorization_header(None) is None + + +class TestFetchUserData: + def test_fetch_user_data_returns_parsed_json(self, monkeypatch): + monkeypatch.setenv('USER_API_URL', 'http://fake-user-api/') + install_pool_manager(monkeypatch, payload={'id': 'u-1', 'role': 'STUDENT'}) + + assert user_mss_authorizer._fetch_user_data('valid-token') == {'id': 'u-1', 'role': 'STUDENT'} + + def test_fetch_user_data_raises_without_env_var(self, monkeypatch): + monkeypatch.delenv('USER_API_URL', raising=False) + install_pool_manager(monkeypatch) + + with pytest.raises(Exception): + user_mss_authorizer._fetch_user_data('valid-token') + + def test_fetch_user_data_raises_on_non_200(self, monkeypatch): + monkeypatch.setenv('USER_API_URL', 'http://fake-user-api/') + install_pool_manager(monkeypatch, status=500) + + with pytest.raises(Exception): + user_mss_authorizer._fetch_user_data('valid-token') From b462b1baa3af7e2b0fc22a5538d4978e9b6022c1 Mon Sep 17 00:00:00 2001 From: Victor Gasperi Date: Wed, 5 Aug 2026 17:33:41 -0300 Subject: [PATCH 14/18] feat(authorizer): adiciona optional_lambda_handler que permite chamada anonima --- src/shared/authorizer/user_mss_authorizer.py | 42 ++++++++++ .../authorizer/test_user_mss_authorizer.py | 82 +++++++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/src/shared/authorizer/user_mss_authorizer.py b/src/shared/authorizer/user_mss_authorizer.py index 2c8ca1a..cb0baff 100644 --- a/src/shared/authorizer/user_mss_authorizer.py +++ b/src/shared/authorizer/user_mss_authorizer.py @@ -88,6 +88,48 @@ def lambda_handler(event, context): return generate_policy("user", "Deny", method_arn) +def optional_lambda_handler(event, context): + """ + REQUEST authorizer with optional authentication. + + Wired to routes that must stay reachable by unauthenticated clients. Because it is a + REQUEST authorizer registered with no identity sources, API Gateway always invokes it, + even when the Authorization header is absent. + + - No token -> Allow with no user context (the route behaves as if the caller were a STUDENT) + - Valid token -> Allow with the user context, same shape as lambda_handler + - Invalid token -> Deny + + Args: + event (dict): The event data passed to the Lambda function. + context (object): The context object representing the current invocation. + + Returns: + dict: The response object containing the policy document. + """ + + method_arn = event["methodArn"] + + authorization_header = _get_authorization_header(event.get("headers")) + token = authorization_header.replace("Bearer ", "").strip() if authorization_header else "" + + # No token at all: the caller is anonymous, let it through without user context + if not token: + return generate_policy("anonymous", "Allow", method_arn) + + try: + user_data = _fetch_user_data(token) + + return generate_policy( + user_data.get("id", "user"), "Allow", method_arn, {"user": json.dumps(user_data)} + ) + + # A token was sent but it is not valid: this is a real authentication failure + except Exception as e: + print(f"Error: {e}") + return generate_policy("user", "Deny", method_arn) + + def generate_policy(principal_id, effect, method_arn, context=None): ''' This function generates the policy document based on the principal ID, effect, method ARN, and context. diff --git a/tests/shared/authorizer/test_user_mss_authorizer.py b/tests/shared/authorizer/test_user_mss_authorizer.py index 1354af5..05e7739 100644 --- a/tests/shared/authorizer/test_user_mss_authorizer.py +++ b/tests/shared/authorizer/test_user_mss_authorizer.py @@ -138,3 +138,85 @@ def test_fetch_user_data_raises_on_non_200(self, monkeypatch): with pytest.raises(Exception): user_mss_authorizer._fetch_user_data('valid-token') + + +class TestOptionalLambdaHandler: + def test_optional_handler_without_authorization_header_allows_anonymous(self, monkeypatch): + monkeypatch.setenv('USER_API_URL', 'http://fake-user-api/') + install_pool_manager(monkeypatch) + + event = {'type': 'REQUEST', 'methodArn': METHOD_ARN, 'headers': {'Accept': '*/*'}} + policy = user_mss_authorizer.optional_lambda_handler(event, None) + + assert policy['principalId'] == 'anonymous' + assert policy['policyDocument']['Statement'][0]['Effect'] == 'Allow' + assert 'context' not in policy + assert FakePoolManager.calls == [] + + def test_optional_handler_without_headers_key_allows_anonymous(self, monkeypatch): + monkeypatch.setenv('USER_API_URL', 'http://fake-user-api/') + install_pool_manager(monkeypatch) + + event = {'type': 'REQUEST', 'methodArn': METHOD_ARN} + policy = user_mss_authorizer.optional_lambda_handler(event, None) + + assert policy['principalId'] == 'anonymous' + assert policy['policyDocument']['Statement'][0]['Effect'] == 'Allow' + assert 'context' not in policy + + def test_optional_handler_with_empty_bearer_allows_anonymous(self, monkeypatch): + monkeypatch.setenv('USER_API_URL', 'http://fake-user-api/') + install_pool_manager(monkeypatch) + + event = {'type': 'REQUEST', 'methodArn': METHOD_ARN, 'headers': {'Authorization': 'Bearer '}} + policy = user_mss_authorizer.optional_lambda_handler(event, None) + + assert policy['principalId'] == 'anonymous' + assert policy['policyDocument']['Statement'][0]['Effect'] == 'Allow' + assert 'context' not in policy + assert FakePoolManager.calls == [] + + def test_optional_handler_with_valid_token_allows_with_user_context(self, monkeypatch): + monkeypatch.setenv('USER_API_URL', 'http://fake-user-api/') + install_pool_manager(monkeypatch) + + event = { + 'type': 'REQUEST', + 'methodArn': METHOD_ARN, + 'headers': {'Authorization': 'Bearer valid-token'}, + } + policy = user_mss_authorizer.optional_lambda_handler(event, None) + + assert policy['principalId'] == '1f25448b-3429-4c19-8287-d9e64f17bc3a' + assert policy['policyDocument']['Statement'][0]['Effect'] == 'Allow' + assert json.loads(policy['context']['user'])['role'] == 'ADMIN' + assert FakePoolManager.calls[0]['headers'] == {'Authorization': 'Bearer valid-token'} + + def test_optional_handler_with_lowercase_header_allows_with_user_context(self, monkeypatch): + monkeypatch.setenv('USER_API_URL', 'http://fake-user-api/') + install_pool_manager(monkeypatch) + + event = { + 'type': 'REQUEST', + 'methodArn': METHOD_ARN, + 'headers': {'authorization': 'Bearer valid-token'}, + } + policy = user_mss_authorizer.optional_lambda_handler(event, None) + + assert policy['policyDocument']['Statement'][0]['Effect'] == 'Allow' + assert 'context' in policy + + def test_optional_handler_with_invalid_token_denies(self, monkeypatch): + monkeypatch.setenv('USER_API_URL', 'http://fake-user-api/') + install_pool_manager(monkeypatch, status=401) + + event = { + 'type': 'REQUEST', + 'methodArn': METHOD_ARN, + 'headers': {'Authorization': 'Bearer expired-token'}, + } + policy = user_mss_authorizer.optional_lambda_handler(event, None) + + assert policy['principalId'] == 'user' + assert policy['policyDocument']['Statement'][0]['Effect'] == 'Deny' + assert 'context' not in policy From 38b915aa694203f6dc74fe893e2f1057a4078a47 Mon Sep 17 00:00:00 2001 From: Victor Gasperi Date: Wed, 5 Aug 2026 17:38:13 -0300 Subject: [PATCH 15/18] fix(iac): usa request authorizer opcional na rota get_bookings --- iac/components/lambda_construct.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/iac/components/lambda_construct.py b/iac/components/lambda_construct.py index d6da624..5323c32 100644 --- a/iac/components/lambda_construct.py +++ b/iac/components/lambda_construct.py @@ -127,6 +127,32 @@ def __init__( results_cache_ttl=Duration.seconds(0) ) + # Segundo authorizer, para rotas em que o token é opcional. + # Reaproveita o mesmo asset do authorizer obrigatório, mudando só o handler. + optional_authorizer_lambda = lambda_.Function( + self, + id=f"LambdaOptionalUserMssAuthorizer-{self.stack_name}-{self.stage}", + function_name=f"lambda_optional_user_mss_authorizer-{self.stack_name}-{self.stage}"[:63], + code=lambda_.Code.from_asset("../src/shared/authorizer"), + handler="user_mss_authorizer.optional_lambda_handler", + runtime=lambda_.Runtime("python3.13"), + layers=[self.lambda_layer], + environment=environment_variables, + timeout=Duration.seconds(15) + ) + + # identity_sources=[] só é aceito com results_cache_ttl=0, e é justamente essa + # combinação que faz o API Gateway invocar o authorizer mesmo sem header Authorization. + # Com um TokenAuthorizer, a requisição sem header morre em 401 antes de chegar aqui. + optional_request_authorizer = apigw.RequestAuthorizer( + self, + id=f"RequestOptionalUserMssAuthorizer-{self.stack_name}-{self.stage}", + authorizer_name=f"optional_user_mss_authorizer-{self.stack_name}-{self.stage}", + handler=optional_authorizer_lambda, + identity_sources=[], + results_cache_ttl=Duration.seconds(0) + ) + self.create_booking = self.create_lambda_api_gateway_integration( module_name="create_booking", method="POST", @@ -156,7 +182,7 @@ def __init__( method="GET", api_resource=api_gateway_resource, environment_variables=environment_variables, - authorizer=token_authorizer_lambda + authorizer=optional_request_authorizer ) self.delete_booking = self.create_lambda_api_gateway_integration( From f9e8c1708e8410c827c24d34cfe6bf14e979110b Mon Sep 17 00:00:00 2001 From: Victor Gasperi Date: Wed, 5 Aug 2026 17:42:16 -0300 Subject: [PATCH 16/18] test(get_bookings): garante resposta anonima identica a de um STUDENT --- .../app/test_get_bookings_presenter.py | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/tests/modules/get_bookings/app/test_get_bookings_presenter.py b/tests/modules/get_bookings/app/test_get_bookings_presenter.py index 719a8a2..31e3c1e 100644 --- a/tests/modules/get_bookings/app/test_get_bookings_presenter.py +++ b/tests/modules/get_bookings/app/test_get_bookings_presenter.py @@ -367,3 +367,125 @@ def test_get_bookings_presenter_entity_not_found(self): assert response['statusCode'] == 404 assert json.loads(response['body']) == 'No items found for booking filters passed' + + def test_get_bookings_presenter_anonymous_authorizer_context(self): + # Cenário do authorizer opcional devolvendo Allow anônimo: existe `authorizer`, + # mas sem a chave `user`. + event = { + "version": "2.0", + "routeKey": "$default", + "rawPath": "/my/path", + "rawQueryString": "booking_id=b1d3bebf-dc0d-4fc1-861c-506a40cc2925", + "headers": { + "header1": "value1" + }, + "queryStringParameters": { + "booking_id": "b1d3bebf-dc0d-4fc1-861c-506a40cc2925" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "", + "authentication": None, + "authorizer": { + "principalId": "anonymous" + }, + "requestId": "id", + "stage": "$default", + "timeEpoch": 1583348638390 + }, + "body": {}, + "pathParameters": None, + "isBase64Encoded": None, + "stageVariables": None + } + + response = lambda_handler(event, None) + body = json.loads(response['body']) + + assert response['statusCode'] == 200 + assert body['message'] == 'the bookings were retrieved' + assert body['bookings'][0]['booking_id'] == 'b1d3bebf-dc0d-4fc1-861c-506a40cc2925' + assert 'owner_name' not in body['bookings'][0] + assert 'owner_network_id' not in body['bookings'][0] + assert 'user_id' not in body['bookings'][0] + + def test_get_bookings_presenter_without_authorizer_at_all(self): + # Cenário mais defensivo: requestContext sem nenhuma chave `authorizer`. + event = { + "version": "2.0", + "routeKey": "$default", + "rawPath": "/my/path", + "rawQueryString": "booking_id=b1d3bebf-dc0d-4fc1-861c-506a40cc2925", + "headers": { + "header1": "value1" + }, + "queryStringParameters": { + "booking_id": "b1d3bebf-dc0d-4fc1-861c-506a40cc2925" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "", + "requestId": "id", + "stage": "$default", + "timeEpoch": 1583348638390 + }, + "body": {}, + "pathParameters": None, + "isBase64Encoded": None, + "stageVariables": None + } + + response = lambda_handler(event, None) + body = json.loads(response['body']) + + assert response['statusCode'] == 200 + assert 'owner_name' not in body['bookings'][0] + assert 'owner_network_id' not in body['bookings'][0] + + def test_get_bookings_presenter_student_matches_anonymous_response(self): + # O requisito central: sem token a resposta é byte a byte igual à de um STUDENT. + base_event = { + "version": "2.0", + "routeKey": "$default", + "rawPath": "/my/path", + "rawQueryString": "sport=Tennis", + "headers": { + "header1": "value1" + }, + "queryStringParameters": { + "sport": "Tennis" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "", + "requestId": "id", + "stage": "$default", + "timeEpoch": 1583348638390 + }, + "body": {}, + "pathParameters": None, + "isBase64Encoded": None, + "stageVariables": None + } + + anonymous_event = json.loads(json.dumps(base_event)) + anonymous_event['requestContext']['authorizer'] = {"principalId": "anonymous"} + + student_event = json.loads(json.dumps(base_event)) + student_event['requestContext']['authorizer'] = { + "user": json.dumps({ + "user": { + "id": "c8435c66-13a4-4641-9d54-773b4b8ccc98", + "displayName": "User", + "mail": "lbj@maua.br", + "role": "STUDENT" + } + }) + } + + anonymous_response = lambda_handler(anonymous_event, None) + student_response = lambda_handler(student_event, None) + + assert anonymous_response['statusCode'] == 200 + assert student_response['statusCode'] == 200 + assert json.loads(anonymous_response['body']) == json.loads(student_response['body']) From c0dae04b4150a76f1c69f6cc71cbd8f5a5a89faf Mon Sep 17 00:00:00 2001 From: Victor Gasperi Date: Wed, 5 Aug 2026 17:46:08 -0300 Subject: [PATCH 17/18] fix(get_bookings): reaproveita UserAPIClient e remove print de debug --- .../get_bookings/app/get_bookings_usecase.py | 17 +++++++---- .../app/test_get_bookings_usecase.py | 29 ++++++++++++++++++- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/modules/get_bookings/app/get_bookings_usecase.py b/src/modules/get_bookings/app/get_bookings_usecase.py index e5d86a2..6f108dc 100644 --- a/src/modules/get_bookings/app/get_bookings_usecase.py +++ b/src/modules/get_bookings/app/get_bookings_usecase.py @@ -4,7 +4,6 @@ from src.shared.domain.enums.sport import SPORT from src.shared.domain.enums.type import BOOKING_TYPE from src.shared.domain.repositories.booking_repository_interface import IBookingRepository -import os from src.shared.clients.user_api_client import UserAPIClient from src.shared.helpers.errors.domain_errors import EntityError from src.shared.helpers.errors.usecase_errors import NoItemsFound, DependantFilter @@ -62,12 +61,17 @@ def __call__(self, raise NoItemsFound('booking filters passed') owner_list = [] - print(f"[DEBUG] requester_role recebido: {requester_role}") - - for booking in bookings: - if requester_role == 'ADMIN': - client = self.user_client or UserAPIClient() + + if requester_role == 'ADMIN': + client = self.user_client + + for booking in bookings: try: + # Construído sob demanda e reaproveitado: cada UserAPIClient() baixa + # a lista inteira de usuários do user mss. + if client is None: + client = UserAPIClient() + owner = { 'name': client.get_user_name(booking.user_id), 'network_id': client.get_user_network_id(booking.user_id), @@ -78,6 +82,7 @@ def __call__(self, 'name': 'Erro de integração', 'network_id': 'Erro de integração', } + owner_list.append(owner) return {'bookings': bookings, 'owner': owner_list} diff --git a/tests/modules/get_bookings/app/test_get_bookings_usecase.py b/tests/modules/get_bookings/app/test_get_bookings_usecase.py index e8bd44a..e54e3e2 100644 --- a/tests/modules/get_bookings/app/test_get_bookings_usecase.py +++ b/tests/modules/get_bookings/app/test_get_bookings_usecase.py @@ -44,4 +44,31 @@ def test_get_bookings_usecase_no_items_found(self): with pytest.raises(NoItemsFound): repo = BookingRepositoryMock() usecase = GetBookingsUseCase(repo=repo) - usecase(booking_id='b3d3b3b3-dc0d-4fc1-861c-506a40cc2925') \ No newline at end of file + usecase(booking_id='b3d3b3b3-dc0d-4fc1-861c-506a40cc2925') + + def test_get_bookings_usecase_admin_builds_user_client_once(self, monkeypatch): + class CountingUserClient: + instances = 0 + + def __init__(self): + CountingUserClient.instances += 1 + + def get_user_name(self, user_id): + return 'CEAF MAUA' + + def get_user_network_id(self, user_id): + return 'ceaf' + + monkeypatch.setattr( + 'src.modules.get_bookings.app.get_bookings_usecase.UserAPIClient', + CountingUserClient, + ) + + repo = BookingRepositoryMock() + usecase = GetBookingsUseCase(repo=repo) + + response = usecase(user_id='c8435c66-13a4-4641-9d54-773b4b8ccc98', requester_role='ADMIN') + + assert len(response['bookings']) > 1 + assert len(response['owner']) == len(response['bookings']) + assert CountingUserClient.instances == 1 \ No newline at end of file From 6316f3f472de5ef30bceef749949de8fd4c0bcc8 Mon Sep 17 00:00:00 2001 From: Victor Gasperi Date: Wed, 5 Aug 2026 19:04:40 -0300 Subject: [PATCH 18/18] chore: adiciona CLAUDE.md --- CLAUDE.md | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..aec799b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,77 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +```bash +# Activate virtual environment (required before running anything) +source venv/bin/activate + +# Install dependencies +pip install -r requirements-app.txt + +# Run all tests +pytest + +# Run a single test file +pytest tests/modules/create_booking/app/test_create_booking_usecase.py + +# Run a single test by name +pytest tests/modules/create_booking/app/test_create_booking_usecase.py::TestCreateBookingUsecase::test_create_booking_valid + +# Run tests with coverage +pytest --cov=src +``` + +Set `STAGE=TEST` in your `.env` file (or environment) for local development — this switches to mock repositories and local DynamoDB config automatically. + +## Architecture + +This is a **Clean Architecture** Python microservice deployed as AWS Lambda functions behind API Gateway, with DynamoDB as the database. Each Lambda function is a module under `src/modules/`. + +### Layer flow (outer → inner) + +``` +Lambda event → Presenter → Controller → Usecase → Repository Interface + ↑ + Mock (TEST) or DynamoDB (DEV/PROD) +``` + +- **Presenter** (`*_presenter.py`): Lambda entry point. Instantiates repo/usecase/controller from `Environments`, wraps the raw Lambda event into `LambdaHttpRequest`, injects `user_from_authorizer` from the API Gateway authorizer context, and returns `LambdaHttpResponse.toDict()`. +- **Controller** (`*_controller.py`): Validates and extracts parameters from the request, calls the usecase, wraps the result in a Viewmodel, and returns an HTTP code object (`Created`, `BadRequest`, etc.). +- **Usecase** (`*_usecase.py`): Business logic. Receives primitive types, raises domain/usecase errors. +- **Viewmodel** (`*_viewmodel.py`): Serializes domain entities to response dicts. +- **Repository interface** (`src/shared/domain/repositories/`): Abstract base classes (`IBookingRepository`, `IReservationRepository`) that define the data contract. +- **Repository implementations** (`src/shared/infra/repositories/`): `*_mock.py` for tests, `*_dynamo.py` for production. + +### Environment / repo selection + +`Environments.get_envs()` (in `src/shared/environments.py`) reads the `STAGE` env var and returns the correct repository class. When `STAGE=TEST`, mocks are used; otherwise DynamoDB implementations are used. All presenters call this at module load time. + +### Authentication + +A Lambda Authorizer (`src/shared/authorizer/user_mss_authorizer.py`) validates Bearer tokens against an external User MSS API and injects user data into the API Gateway request context. Controllers receive it via `request.data['user_from_authorizer']` (a dict with `user_id`, role, etc.). + +### Key shared paths + +| Path | Purpose | +|------|---------| +| `src/shared/domain/entities/` | `Booking` and `Court` domain entities with validation | +| `src/shared/domain/enums/` | `SPORT`, `BOOKING_TYPE`, `STATUS_ENUM` enums | +| `src/shared/helpers/errors/` | `domain_errors`, `usecase_errors`, `controller_errors` — raised by different layers | +| `src/shared/helpers/external_interfaces/` | `LambdaHttpRequest/Response`, HTTP status code wrappers | +| `src/shared/infra/dto/` | DynamoDB ↔ domain entity conversion (`*_dynamo_dto.py`) | +| `src/shared/clients/` | External HTTP clients (e.g., `user_api_client.py`) | +| `iac/` | AWS CDK infrastructure (API Gateway, Lambda, DynamoDB, S3, SSM constructs) | + +### Naming conventions + +- Files and directories: `snake_case` +- Classes: `PascalCase` with type suffix — `CreateBookingController`, `BookingRepositoryMock`, `IBookingRepository` +- Enums: `UPPER_SNAKE_CASE` with `_ENUM` suffix where applicable +- Tests mirror the `src/` directory structure under `tests/` + +### Infrastructure + +Defined in `iac/` using AWS CDK (Python). The stack provisions API Gateway, Lambda functions, DynamoDB table, S3 bucket, and SSM parameters. The `STAGE` variable controls deployment target (`DEV`, `HOMOLOG`, `PROD`). Local development uses Docker Compose with DynamoDB Local and MinIO (see `iac/local/`).