From 7136fdd32969bdbcb3e3363176ab26525dc60920 Mon Sep 17 00:00:00 2001 From: Hersonls Date: Mon, 17 Feb 2025 11:16:18 -0300 Subject: [PATCH] Add session support to client --- barte/client.py | 97 ++++++++-------- tests/test_client.py | 262 +++++++++++++++++++------------------------ 2 files changed, 167 insertions(+), 192 deletions(-) diff --git a/barte/client.py b/barte/client.py index 99a7f02..8964e22 100644 --- a/barte/client.py +++ b/barte/client.py @@ -43,6 +43,8 @@ def __init__(self, api_key: str, environment: str = "production"): else "https://sandbox-api.barte.com" ) self.headers = {"X-Token-Api": api_key, "Content-Type": "application/json"} + self.session = requests.Session() + self.session.headers.update(self.headers) BarteClient._instance = self @classmethod @@ -53,76 +55,79 @@ def get_instance(cls) -> "BarteClient": ) return cls._instance + def _request( + self, + method: str, + path: str, + params: Optional[Dict[str, Any]] = None, + json: Optional[Dict[str, Any]] = None, + ) -> Dict[str, Any]: + """ + Private method to centralize HTTP requests. + + Args: + method: HTTP method (e.g., 'GET', 'POST', 'DELETE', etc.) + path: API endpoint path (e.g., '/v2/orders') + params: Query parameters for GET requests. + json: JSON body for POST, PATCH requests. + + Returns: + The response JSON as a dictionary. + + Raises: + HTTPError: If the HTTP request returned an unsuccessful status code. + """ + url = f"{self.base_url}{path}" + response = self.session.request(method, url, params=params, json=json) + response.raise_for_status() + return response.json() + def create_order(self, data: Union[Dict[str, Any], OrderPayload]) -> Order: """Create a new order""" - endpoint = f"{self.base_url}/v2/orders" - if isinstance(data, OrderPayload): data = asdict(data) - - response = requests.post(endpoint, headers=self.headers, json=data) - response.raise_for_status() - return from_dict(data_class=Order, data=response.json(), config=DACITE_CONFIG) + json_response = self._request("POST", "/v2/orders", json=data) + return from_dict(data_class=Order, data=json_response, config=DACITE_CONFIG) def get_charge(self, charge_id: str) -> Charge: """Get a specific charge""" - endpoint = f"{self.base_url}/v2/charges/{charge_id}" - response = requests.get(endpoint, headers=self.headers) - response.raise_for_status() - return from_dict(data_class=Charge, data=response.json(), config=DACITE_CONFIG) + json_response = self._request("GET", f"/v2/charges/{charge_id}") + return from_dict(data_class=Charge, data=json_response, config=DACITE_CONFIG) def list_charges(self, params: Optional[Dict[str, Any]] = None) -> ChargeList: """List all charges with optional filters""" - endpoint = f"{self.base_url}/v2/charges" - response = requests.get(endpoint, headers=self.headers, params=params) - response.raise_for_status() + json_response = self._request("GET", "/v2/charges", params=params) return from_dict( - data_class=ChargeList, data=response.json(), config=DACITE_CONFIG + data_class=ChargeList, data=json_response, config=DACITE_CONFIG ) def cancel_charge(self, charge_id: str) -> None: """Cancel a specific charge""" - endpoint = f"{self.base_url}/v2/charges/{charge_id}" - response = requests.delete(endpoint, headers=self.headers) - response.raise_for_status() + self._request("DELETE", f"/v2/charges/{charge_id}") - def create_buyer(self, buyer_data: Dict[str, any]) -> Buyer: - endpoint = f"{self.base_url}/v2/buyers" - response = requests.post(endpoint, headers=self.headers, json=buyer_data) - response.raise_for_status() - return from_dict(data_class=Buyer, data=response.json(), config=DACITE_CONFIG) + def create_buyer(self, buyer_data: Dict[str, Any]) -> Buyer: + """Create a buyer""" + json_response = self._request("POST", "/v2/buyers", json=buyer_data) + return from_dict(data_class=Buyer, data=json_response, config=DACITE_CONFIG) - def get_buyer(self, filters: Dict[str, any]) -> BuyerList: - endpoint = f"{self.base_url}/v2/buyers" - response = requests.get(endpoint, params=filters, headers=self.headers) - response.raise_for_status() - return from_dict( - data_class=BuyerList, data=response.json(), config=DACITE_CONFIG - ) + def get_buyer(self, filters: Dict[str, Any]) -> BuyerList: + """Get buyers based on filters""" + json_response = self._request("GET", "/v2/buyers", params=filters) + return from_dict(data_class=BuyerList, data=json_response, config=DACITE_CONFIG) def create_card_token(self, card_data: Dict[str, Any]) -> CardToken: """Create a token for a credit card""" - endpoint = f"{self.base_url}/v2/cards" - response = requests.post(endpoint, headers=self.headers, json=card_data) - response.raise_for_status() - return from_dict( - data_class=CardToken, data=response.json(), config=DACITE_CONFIG - ) + json_response = self._request("POST", "/v2/cards", json=card_data) + return from_dict(data_class=CardToken, data=json_response, config=DACITE_CONFIG) def get_pix_qrcode(self, charge_id: str) -> PixCharge: """Get PIX QR Code data for a charge""" - endpoint = f"{self.base_url}/v2/charges/{charge_id}" - response = requests.get(endpoint, headers=self.headers) - response.raise_for_status() - return from_dict( - data_class=PixCharge, data=response.json(), config=DACITE_CONFIG - ) + json_response = self._request("GET", f"/v2/charges/{charge_id}") + return from_dict(data_class=PixCharge, data=json_response, config=DACITE_CONFIG) def refund_charge(self, charge_id: str, as_fraud: Optional[bool] = False) -> Refund: """Refund a charge""" - endpoint = f"{self.base_url}/v2/charges/{charge_id}/refund" - response = requests.patch( - endpoint, headers=self.headers, json={"asFraud": as_fraud} + json_response = self._request( + "PATCH", f"/v2/charges/{charge_id}/refund", json={"asFraud": as_fraud} ) - response.raise_for_status() - return from_dict(data_class=Refund, data=response.json(), config=DACITE_CONFIG) + return from_dict(data_class=Refund, data=json_response, config=DACITE_CONFIG) diff --git a/tests/test_client.py b/tests/test_client.py index 3b4f782..8269b16 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -2,20 +2,14 @@ from datetime import datetime from unittest.mock import patch, Mock from dacite import from_dict -from barte import ( - BarteClient, - Charge, - CardToken, - Refund, - PixCharge, -) +from barte import BarteClient, Charge, CardToken, Refund, PixCharge from barte.models import DACITE_CONFIG, Order @pytest.fixture def barte_client(): client = BarteClient(api_key="test_key", environment="sandbox") - BarteClient._instance = client # Set instance for model methods + BarteClient._instance = client # For model instance methods return client @@ -106,8 +100,15 @@ def mock_pix_charge_response(): "phone": "11999999999", "alternativeEmail": "", }, - "pixCode": "000201010211261230014BR.GOV.BCB.PIX01000297BENEFICIƁRIO FINAL: BUSER BRASIL TECNOLOGIA LTDA \n Intermediado pela plataforma Barte Brasil Ltda52040000530398654040.035802BR5920ClienteExterno-sTZ4 600062360532cd5e99706300441787ee6188e4814fa263040CB9", - "pixQRCodeImage": "https://s3.amazonaws.com/sandbox-charge-docs.barte.corp/pix/155e846a-c237-43a3-95a9-b8c88b5d5833.png", + "pixCode": ( + "000201010211261230014BR.GOV.BCB.PIX01000297BENEFICIƁRIO FINAL: " + "BUSER BRASIL TECNOLOGIA LTDA \n Intermediado pela plataforma Barte Brasil Ltda" + "52040000530398654040.035802BR5920ClienteExterno-sTZ4 600062360532cd5e99706300441787ee6188e4814fa263040CB9" + ), + "pixQRCodeImage": ( + "https://s3.amazonaws.com/sandbox-charge-docs.barte.corp/pix/" + "155e846a-c237-43a3-95a9-b8c88b5d5833.png" + ), } @@ -136,27 +137,47 @@ def mock_list_response(): class TestBarteClient: - def test_client_initialization(self): - """Test client initialization with valid environment""" - client = BarteClient(api_key="test_key", environment="sandbox") - assert client.api_key == "test_key" - assert client.base_url == "https://sandbox-api.barte.com" - assert client.headers == { - "X-Token-Api": "test_key", - "Content-Type": "application/json", - } + @patch("barte.client.requests.Session.request") + def test_request_get(self, mock_request, barte_client): + """Test _request method with GET (no JSON data)""" + response_dict = {"key": "value"} + # Configure the mock response: + mock_request.return_value.json.return_value = response_dict + mock_request.return_value.raise_for_status = Mock() + + result = barte_client._request("GET", "/test_endpoint") + assert result == response_dict + + mock_request.assert_called_once_with( + "GET", + f"{barte_client.base_url}/test_endpoint", + params=None, + json=None, + ) - def test_invalid_environment(self): - """Test client initialization with invalid environment""" - with pytest.raises(ValueError) as exc_info: - BarteClient(api_key="test_key", environment="invalid") - assert "Invalid environment" in str(exc_info.value) + @patch("barte.client.requests.Session.request") + def test_request_post(self, mock_request, barte_client): + """Test _request method with POST (with JSON data)""" + request_dict = {"data": "value"} + response_dict = {"key": "value"} + mock_request.return_value.json.return_value = response_dict + mock_request.return_value.raise_for_status = Mock() + + result = barte_client._request("POST", "/test_endpoint", json=request_dict) + assert result == response_dict + + mock_request.assert_called_once_with( + "POST", + f"{barte_client.base_url}/test_endpoint", + params=None, + json=request_dict, + ) - @patch("requests.post") - def test_create_order(self, mock_post, barte_client, mock_order_response): - """Test creating a new order""" - mock_post.return_value.json.return_value = mock_order_response - mock_post.return_value.raise_for_status = Mock() + @patch("barte.client.requests.Session.request") + def test_create_order(self, mock_request, barte_client, mock_order_response): + """Test creating a new order using create_order""" + mock_request.return_value.json.return_value = mock_order_response + mock_request.return_value.raise_for_status = Mock() order_data = { "startDate": "2025-02-07", @@ -192,24 +213,25 @@ def test_create_order(self, mock_post, barte_client, mock_order_response): }, "uuidBuyer": "5929a30b-e68f-4c81-9481-d25adbabafeb", } - order = barte_client.create_order(order_data) + # Verify the returned object (converted to Order) assert isinstance(order, Order) assert order.value == 60 assert order.customer.name == "John Doe" assert order.charges[0].uuid == "35b45f90-11bc-448a-bcb4-969a9697d4d5" assert isinstance(order.startDate, datetime) - mock_post.assert_called_once_with( + mock_request.assert_called_once_with( + "POST", f"{barte_client.base_url}/v2/orders", - headers=barte_client.headers, + params=None, json=order_data, ) - @patch("requests.post") - def test_create_card_token(self, mock_post, barte_client): - """Test creating a card token""" + @patch("barte.client.requests.Session.request") + def test_create_card_token(self, mock_request, barte_client): + """Test creating a card token using create_card_token""" mock_response = { "uuid": "790e8637-c16b-4ed5-a9bf-faec76dbc5aa", "status": "ACTIVE", @@ -225,8 +247,8 @@ def test_create_card_token(self, mock_post, barte_client): "expirationYear": "2025", "cardId": "9dc2ffe0-d588-44b7-b74d-d5ad88a31143", } - mock_post.return_value.json.return_value = mock_response - mock_post.return_value.raise_for_status = Mock() + mock_request.return_value.json.return_value = mock_response + mock_request.return_value.raise_for_status = Mock() card_data = { "number": "5383630891", @@ -235,7 +257,6 @@ def test_create_card_token(self, mock_post, barte_client): "expiration_year": 2025, "cvv": "123", } - token = barte_client.create_card_token(card_data) assert isinstance(token, CardToken) @@ -244,15 +265,16 @@ def test_create_card_token(self, mock_post, barte_client): assert token.cardHolderName == "John Doe" assert isinstance(token.createdAt, datetime) - mock_post.assert_called_once_with( + mock_request.assert_called_once_with( + "POST", f"{barte_client.base_url}/v2/cards", - headers=barte_client.headers, + params=None, json=card_data, ) - @patch("requests.patch") - def test_refund_charge(self, mock_patch, barte_client): - """Test refunding a charge""" + @patch("barte.client.requests.Session.request") + def test_refund_charge(self, mock_request, barte_client): + """Test refunding a charge using refund_charge""" mock_response = { "uuid": "d54f6553-8bcf-4376-a995-aaffb6d29492", "title": "Barte - Postman - BgN", @@ -273,150 +295,101 @@ def test_refund_charge(self, mock_patch, barte_client): "authorizationCode": "3235588", "authorizationNsu": "5555742", } - mock_patch.return_value.json.return_value = mock_response - mock_patch.return_value.raise_for_status = Mock() + mock_response_obj = Mock() + mock_response_obj.json.return_value = mock_response + mock_response_obj.raise_for_status = Mock() + mock_request.return_value = mock_response_obj refund = barte_client.refund_charge( "d54f6553-8bcf-4376-a995-aaffb6d29492", as_fraud=False ) - assert isinstance(refund, Refund) assert refund.uuid == "d54f6553-8bcf-4376-a995-aaffb6d29492" assert refund.value == 23.00 assert refund.status == "REFUND" assert isinstance(refund.paidDate, datetime) - mock_patch.assert_called_once_with( + mock_request.assert_called_once_with( + "PATCH", f"{barte_client.base_url}/v2/charges/d54f6553-8bcf-4376-a995-aaffb6d29492/refund", - headers=barte_client.headers, + params=None, json={"asFraud": False}, ) - @patch("requests.get") - def test_get_charge(self, mock_get, barte_client, mock_charge_response): - """Test getting a specific charge""" - mock_get.return_value.json.return_value = mock_charge_response - mock_get.return_value.raise_for_status = Mock() + @patch("barte.client.requests.Session.request") + def test_get_charge(self, mock_request, barte_client, mock_charge_response): + """Test getting a specific charge using get_charge""" + mock_request.return_value.json.return_value = mock_charge_response + mock_request.return_value.raise_for_status = Mock() charge = barte_client.get_charge("8b6b2ddc-7ccb-4d1f-8832-ef0adc62ed31") - assert isinstance(charge, Charge) assert charge.uuid == "8b6b2ddc-7ccb-4d1f-8832-ef0adc62ed31" assert charge.value == 1000.00 assert charge.customer.name == "John Doe" assert isinstance(charge.paidDate, datetime) - mock_get.assert_called_once_with( + mock_request.assert_called_once_with( + "GET", f"{barte_client.base_url}/v2/charges/8b6b2ddc-7ccb-4d1f-8832-ef0adc62ed31", - headers=barte_client.headers, + params=None, + json=None, ) - @patch("requests.get") + @patch("barte.client.requests.Session.request") def test_list_charges( - self, mock_get, barte_client, mock_charge_response, mock_list_response + self, mock_request, barte_client, mock_charge_response, mock_list_response ): - """Test listing all charges""" - mock_response = { + """Test listing all charges using list_charges""" + combined_response = { **mock_list_response, - "content": [ - mock_charge_response, - ], + "content": [mock_charge_response], "has_more": False, } - mock_get.return_value.json.return_value = mock_response - mock_get.return_value.raise_for_status = Mock() + mock_request.return_value.json.return_value = combined_response + mock_request.return_value.raise_for_status = Mock() params = {"customerDocument": "19340911032"} - charges = barte_client.list_charges(params).content - - assert len(charges) == 1 - assert all(isinstance(charge, Charge) for charge in charges) - assert charges[0].uuid == "8b6b2ddc-7ccb-4d1f-8832-ef0adc62ed31" - assert all(isinstance(charge.paidDate, datetime) for charge in charges) - - mock_get.assert_called_once_with( + result = barte_client.list_charges(params) + content = result.content + assert len(content) == 1 + assert all(isinstance(c, Charge) for c in content) + assert content[0].uuid == "8b6b2ddc-7ccb-4d1f-8832-ef0adc62ed31" + assert all(isinstance(c.paidDate, datetime) for c in content) + + mock_request.assert_called_once_with( + "GET", f"{barte_client.base_url}/v2/charges", - headers=barte_client.headers, params=params, + json=None, ) - @patch("requests.delete") - @patch("requests.patch") - def test_charge_methods(self, mock_patch, mock_delete, mock_charge_response): - """Test charge instance methods""" - # Mock for refund - refund_response = { - "uuid": "d54f6553-8bcf-4376-a995-aaffb6d29492", - "title": "Barte - Postman - BgN", - "expirationDate": "2025-02-12", - "paidDate": "2025-02-12", - "value": 23.00, - "paymentMethod": "CREDIT_CARD_EARLY_SELLER", - "status": "REFUND", - "customer": { - "uuid": "", - "document": "19340911032", - "type": "CPF", - "name": "ClienteExterno-sTZ4 ", - "email": "ClienteExterno-sTZ4@email.com", - "phone": "11999999999", - "alternativeEmail": "", - }, - "authorizationCode": "3235588", - "authorizationNsu": "5555742", - } - - mock_patch.return_value.json.return_value = refund_response - mock_patch.return_value.raise_for_status = Mock() - - charge = from_dict( - data_class=Charge, data=mock_charge_response, config=DACITE_CONFIG - ) - - # Test refund method - refund = charge.refund(as_fraud=False) - assert isinstance(refund, Refund) - assert refund.value == 23.00 - mock_patch.assert_called_with( - f"https://sandbox-api.barte.com/v2/charges/{charge.uuid}/refund", - headers={"X-Token-Api": "test_key", "Content-Type": "application/json"}, - json={"asFraud": False}, - ) - - # Mock for cancel - use the original mock response with updated status - mock_delete.return_value.json.return_value = None - mock_delete.status_code = 204 - - # Test cancel method - charge.cancel() - mock_delete.assert_called_with( - f"https://sandbox-api.barte.com/v2/charges/{charge.uuid}", - headers={"X-Token-Api": "test_key", "Content-Type": "application/json"}, - ) + @patch("barte.client.requests.Session.request") + def test_pix_charge_get_qrcode( + self, mock_request, barte_client, mock_pix_charge_response + ): + """Test PIX charge QR code method using get_qr_code""" + # For get_qr_code, the client calls its _request to fetch updated charge info. + # Set the response and ensure a 200 status. + mock_request.return_value.json.return_value = mock_pix_charge_response + mock_request.return_value.raise_for_status = Mock() + mock_request.return_value.status_code = 200 - @patch("requests.get") - def test_pix_charge_get_qrcode(self, mock_get, mock_pix_charge_response): - """Test PIX charge QR code method""" - # Create a PIX charge pix_charge = from_dict( data_class=PixCharge, data=mock_pix_charge_response.copy(), config=DACITE_CONFIG, ) + qr_data = pix_charge.get_qr_code() - # Mock QR code response - mock_get.return_value.json.return_value = mock_pix_charge_response - mock_get.return_value.raise_for_status = Mock() - - # Get QR code - pix_charge_qr_code = pix_charge.get_qr_code() - - assert pix_charge_qr_code.qr_code == pix_charge.pixCode - assert pix_charge_qr_code.qr_code_image == pix_charge.pixQRCodeImage + assert qr_data.qr_code == pix_charge.pixCode + assert qr_data.qr_code_image == pix_charge.pixQRCodeImage - mock_get.assert_called_once_with( - f"https://sandbox-api.barte.com/v2/charges/{pix_charge.uuid}", - headers={"X-Token-Api": "test_key", "Content-Type": "application/json"}, + mock_request.assert_called_once_with( + "GET", + f"{barte_client.base_url}/v2/charges/{pix_charge.uuid}", + params=None, + json=None, ) def test_client_singleton(self): @@ -424,16 +397,13 @@ def test_client_singleton(self): # Reset singleton for initial state BarteClient._instance = None - # Test uninitialized state with pytest.raises(RuntimeError) as exc_info: BarteClient.get_instance() assert "BarteClient not initialized" in str(exc_info.value) - # First initialization client1 = BarteClient(api_key="test_key", environment="sandbox") assert BarteClient.get_instance() == client1 - # Second initialization client2 = BarteClient(api_key="another_key", environment="sandbox") assert BarteClient.get_instance() == client2 assert client2.api_key == "another_key"