From 13d4cd67f1085bbc5db65161fe9d81e2e4ea7e14 Mon Sep 17 00:00:00 2001 From: SherylNyawira Date: Wed, 17 Sep 2025 11:47:36 +0300 Subject: [PATCH 1/7] Added the async on the mpesa_http_client.py --- .../http_client/test_mpesa_http_client.py | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/tests/unit/http_client/test_mpesa_http_client.py b/tests/unit/http_client/test_mpesa_http_client.py index c2a38e4..4951d58 100644 --- a/tests/unit/http_client/test_mpesa_http_client.py +++ b/tests/unit/http_client/test_mpesa_http_client.py @@ -182,3 +182,136 @@ def test_get_connection_error(client): with pytest.raises(MpesaApiException) as exc: client.get("/conn") assert exc.value.error.error_code == "CONNECTION_ERROR" + + + + + +# Test async POST success scenario. +@pytest.mark.asyncio +async def test_async_post_success(client): + # Patch aiohttp ClientSession.post to mock async POST request. + with patch("mpesakit.http_client.mpesa_http_client.aiohttp.ClientSession.post") as mock_post: + mock_response = Mock() + mock_response.status = 200 # Simulate HTTP 200 OK. + mock_response.json = Mock(return_value={"foo": "bar"}) # Mock JSON response. + mock_post.return_value.__aenter__.return_value = mock_response + + # Call async_post and assert the result. + result = await client.async_post("/test", json={"a": 1}, headers={"h": "v"}) + assert result == {"foo": "bar"} + mock_post.assert_called_once() + + +# Test async POST HTTP error scenario. +@pytest.mark.asyncio +async def test_async_post_http_error(client): + # Patch aiohttp ClientSession.post to simulate HTTP 400 error. + with patch("mpesakit.http_client.mpesa_http_client.aiohttp.ClientSession.post") as mock_post: + mock_response = Mock() + mock_response.status = 400 # Simulate HTTP 400 error. + mock_response.json = Mock(return_value={"errorMessage": "Bad Request"}) + mock_post.return_value.__aenter__.return_value = mock_response + + # Assert MpesaApiException is raised with correct error code and message. + with pytest.raises(MpesaApiException) as exc: + await client.async_post("/fail", json={}, headers={}) + assert exc.value.error.error_code == "HTTP_400" + assert "Bad Request" in exc.value.error.error_message + + +# Test async POST JSON decode error scenario. +@pytest.mark.asyncio +async def test_async_post_json_decode_error(client): + # Patch aiohttp ClientSession.post to simulate JSON decode error. + with patch("mpesakit.http_client.mpesa_http_client.aiohttp.ClientSession.post") as mock_post: + mock_response = Mock() + mock_response.status = 500 # Simulate HTTP 500 error. + mock_response.json = Mock(side_effect=ValueError()) # Simulate JSON decode error. + mock_response.text = "Internal Server Error" + mock_post.return_value.__aenter__.return_value = mock_response + + # Assert MpesaApiException is raised with correct error code and message. + with pytest.raises(MpesaApiException) as exc: + await client.async_post("/fail", json={}, headers={}) + assert exc.value.error.error_code == "HTTP_500" + assert "Internal Server Error" in exc.value.error.error_message + + +# Test async POST generic exception scenario. +@pytest.mark.asyncio +async def test_async_post_exception(client): + # Patch aiohttp ClientSession.post to raise a generic exception. + with patch( + "mpesakit.http_client.mpesa_http_client.aiohttp.ClientSession.post", + side_effect=Exception("boom"), + ): + # Assert MpesaApiException is raised with REQUEST_FAILED code. + with pytest.raises(MpesaApiException) as exc: + await client.async_post("/fail", json={}, headers={}) + assert exc.value.error.error_code == "REQUEST_FAILED" + + +# Test async GET success scenario. +@pytest.mark.asyncio +async def test_async_get_success(client): + # Patch aiohttp ClientSession.get to mock async GET request. + with patch("mpesakit.http_client.mpesa_http_client.aiohttp.ClientSession.get") as mock_get: + mock_response = Mock() + mock_response.status = 200 # Simulate HTTP 200 OK. + mock_response.json = Mock(return_value={"foo": "bar"}) # Mock JSON response. + mock_get.return_value.__aenter__.return_value = mock_response + + # Call async_get and assert the result. + result = await client.async_get("/test", params={"a": 1}, headers={"h": "v"}) + assert result == {"foo": "bar"} + mock_get.assert_called_once() + + +# Test async GET HTTP error scenario. +@pytest.mark.asyncio +async def test_async_get_http_error(client): + # Patch aiohttp ClientSession.get to simulate HTTP 404 error. + with patch("mpesakit.http_client.mpesa_http_client.aiohttp.ClientSession.get") as mock_get: + mock_response = Mock() + mock_response.status = 404 # Simulate HTTP 404 error. + mock_response.json = Mock(return_value={"errorMessage": "Not Found"}) + mock_get.return_value.__aenter__.return_value = mock_response + + # Assert MpesaApiException is raised with correct error code and message. + with pytest.raises(MpesaApiException) as exc: + await client.async_get("/fail") + assert exc.value.error.error_code == "HTTP_404" + assert "Not Found" in exc.value.error.error_message + + +# Test async GET JSON decode error scenario. +@pytest.mark.asyncio +async def test_async_get_json_decode_error(client): + # Patch aiohttp ClientSession.get to simulate JSON decode error. + with patch("mpesakit.http_client.mpesa_http_client.aiohttp.ClientSession.get") as mock_get: + mock_response = Mock() + mock_response.status = 500 # Simulate HTTP 500 error. + mock_response.json = Mock(side_effect=ValueError()) # Simulate JSON decode error. + mock_response.text = "Internal Server Error" + mock_get.return_value.__aenter__.return_value = mock_response + + # Assert MpesaApiException is raised with correct error code and message. + with pytest.raises(MpesaApiException) as exc: + await client.async_get("/fail") + assert exc.value.error.error_code == "HTTP_500" + assert "Internal Server Error" in exc.value.error.error_message + + +# Test async GET generic exception scenario. +@pytest.mark.asyncio +async def test_async_get_exception(client): + # Patch aiohttp ClientSession.get to raise a generic exception. + with patch( + "mpesakit.http_client.mpesa_http_client.aiohttp.ClientSession.get", + side_effect=Exception("boom"), + ): + # Assert MpesaApiException is raised with REQUEST_FAILED code. + with pytest.raises(MpesaApiException) as exc: + await client.async_get("/fail") + assert exc.value.error.error_code == "REQUEST_FAILED" \ No newline at end of file From 6021cf8c78bcbfe0eaa6b9437c134e40fce21f6b Mon Sep 17 00:00:00 2001 From: SherylNyawira Date: Thu, 18 Sep 2025 10:35:14 +0300 Subject: [PATCH 2/7] tests: removed async tests from test_mpesa_http_client from the previous commit --- .../http_client/test_mpesa_http_client.py | 132 ------------------ 1 file changed, 132 deletions(-) diff --git a/tests/unit/http_client/test_mpesa_http_client.py b/tests/unit/http_client/test_mpesa_http_client.py index 4951d58..0733986 100644 --- a/tests/unit/http_client/test_mpesa_http_client.py +++ b/tests/unit/http_client/test_mpesa_http_client.py @@ -183,135 +183,3 @@ def test_get_connection_error(client): client.get("/conn") assert exc.value.error.error_code == "CONNECTION_ERROR" - - - - -# Test async POST success scenario. -@pytest.mark.asyncio -async def test_async_post_success(client): - # Patch aiohttp ClientSession.post to mock async POST request. - with patch("mpesakit.http_client.mpesa_http_client.aiohttp.ClientSession.post") as mock_post: - mock_response = Mock() - mock_response.status = 200 # Simulate HTTP 200 OK. - mock_response.json = Mock(return_value={"foo": "bar"}) # Mock JSON response. - mock_post.return_value.__aenter__.return_value = mock_response - - # Call async_post and assert the result. - result = await client.async_post("/test", json={"a": 1}, headers={"h": "v"}) - assert result == {"foo": "bar"} - mock_post.assert_called_once() - - -# Test async POST HTTP error scenario. -@pytest.mark.asyncio -async def test_async_post_http_error(client): - # Patch aiohttp ClientSession.post to simulate HTTP 400 error. - with patch("mpesakit.http_client.mpesa_http_client.aiohttp.ClientSession.post") as mock_post: - mock_response = Mock() - mock_response.status = 400 # Simulate HTTP 400 error. - mock_response.json = Mock(return_value={"errorMessage": "Bad Request"}) - mock_post.return_value.__aenter__.return_value = mock_response - - # Assert MpesaApiException is raised with correct error code and message. - with pytest.raises(MpesaApiException) as exc: - await client.async_post("/fail", json={}, headers={}) - assert exc.value.error.error_code == "HTTP_400" - assert "Bad Request" in exc.value.error.error_message - - -# Test async POST JSON decode error scenario. -@pytest.mark.asyncio -async def test_async_post_json_decode_error(client): - # Patch aiohttp ClientSession.post to simulate JSON decode error. - with patch("mpesakit.http_client.mpesa_http_client.aiohttp.ClientSession.post") as mock_post: - mock_response = Mock() - mock_response.status = 500 # Simulate HTTP 500 error. - mock_response.json = Mock(side_effect=ValueError()) # Simulate JSON decode error. - mock_response.text = "Internal Server Error" - mock_post.return_value.__aenter__.return_value = mock_response - - # Assert MpesaApiException is raised with correct error code and message. - with pytest.raises(MpesaApiException) as exc: - await client.async_post("/fail", json={}, headers={}) - assert exc.value.error.error_code == "HTTP_500" - assert "Internal Server Error" in exc.value.error.error_message - - -# Test async POST generic exception scenario. -@pytest.mark.asyncio -async def test_async_post_exception(client): - # Patch aiohttp ClientSession.post to raise a generic exception. - with patch( - "mpesakit.http_client.mpesa_http_client.aiohttp.ClientSession.post", - side_effect=Exception("boom"), - ): - # Assert MpesaApiException is raised with REQUEST_FAILED code. - with pytest.raises(MpesaApiException) as exc: - await client.async_post("/fail", json={}, headers={}) - assert exc.value.error.error_code == "REQUEST_FAILED" - - -# Test async GET success scenario. -@pytest.mark.asyncio -async def test_async_get_success(client): - # Patch aiohttp ClientSession.get to mock async GET request. - with patch("mpesakit.http_client.mpesa_http_client.aiohttp.ClientSession.get") as mock_get: - mock_response = Mock() - mock_response.status = 200 # Simulate HTTP 200 OK. - mock_response.json = Mock(return_value={"foo": "bar"}) # Mock JSON response. - mock_get.return_value.__aenter__.return_value = mock_response - - # Call async_get and assert the result. - result = await client.async_get("/test", params={"a": 1}, headers={"h": "v"}) - assert result == {"foo": "bar"} - mock_get.assert_called_once() - - -# Test async GET HTTP error scenario. -@pytest.mark.asyncio -async def test_async_get_http_error(client): - # Patch aiohttp ClientSession.get to simulate HTTP 404 error. - with patch("mpesakit.http_client.mpesa_http_client.aiohttp.ClientSession.get") as mock_get: - mock_response = Mock() - mock_response.status = 404 # Simulate HTTP 404 error. - mock_response.json = Mock(return_value={"errorMessage": "Not Found"}) - mock_get.return_value.__aenter__.return_value = mock_response - - # Assert MpesaApiException is raised with correct error code and message. - with pytest.raises(MpesaApiException) as exc: - await client.async_get("/fail") - assert exc.value.error.error_code == "HTTP_404" - assert "Not Found" in exc.value.error.error_message - - -# Test async GET JSON decode error scenario. -@pytest.mark.asyncio -async def test_async_get_json_decode_error(client): - # Patch aiohttp ClientSession.get to simulate JSON decode error. - with patch("mpesakit.http_client.mpesa_http_client.aiohttp.ClientSession.get") as mock_get: - mock_response = Mock() - mock_response.status = 500 # Simulate HTTP 500 error. - mock_response.json = Mock(side_effect=ValueError()) # Simulate JSON decode error. - mock_response.text = "Internal Server Error" - mock_get.return_value.__aenter__.return_value = mock_response - - # Assert MpesaApiException is raised with correct error code and message. - with pytest.raises(MpesaApiException) as exc: - await client.async_get("/fail") - assert exc.value.error.error_code == "HTTP_500" - assert "Internal Server Error" in exc.value.error.error_message - - -# Test async GET generic exception scenario. -@pytest.mark.asyncio -async def test_async_get_exception(client): - # Patch aiohttp ClientSession.get to raise a generic exception. - with patch( - "mpesakit.http_client.mpesa_http_client.aiohttp.ClientSession.get", - side_effect=Exception("boom"), - ): - # Assert MpesaApiException is raised with REQUEST_FAILED code. - with pytest.raises(MpesaApiException) as exc: - await client.async_get("/fail") - assert exc.value.error.error_code == "REQUEST_FAILED" \ No newline at end of file From 311e9b083401eadf12d3544e01e4b07270d80f9f Mon Sep 17 00:00:00 2001 From: SherylNyawira Date: Thu, 18 Sep 2025 11:26:24 +0300 Subject: [PATCH 3/7] refactor: use of requests lib to use httpx \n\n To support asynchronous in the future we need to use httpx as it is more suitable in handling async requests. --- mpesakit/http_client/mpesa_http_client.py | 38 +++++----------- .../mpesa_express/test_stk_push_e2e.py | 30 +++++++------ .../http_client/test_mpesa_http_client.py | 45 +++++++++---------- 3 files changed, 47 insertions(+), 66 deletions(-) diff --git a/mpesakit/http_client/mpesa_http_client.py b/mpesakit/http_client/mpesa_http_client.py index a5d755b..5f7393c 100644 --- a/mpesakit/http_client/mpesa_http_client.py +++ b/mpesakit/http_client/mpesa_http_client.py @@ -4,7 +4,7 @@ """ from typing import Dict, Any, Optional -import requests +import httpx from mpesakit.errors import MpesaError, MpesaApiException from .http_client import HttpClient @@ -54,14 +54,15 @@ def post( """ try: full_url = f"{self.base_url}{url}" - response = requests.post(full_url, json=json, headers=headers, timeout=10) + with httpx.Client(timeout=10) as client: + response = client.post(full_url, json=json, headers=headers) try: response_data = response.json() except ValueError: response_data = {"errorMessage": response.text.strip() or ""} - if not response.ok: + if not response.is_success: error_message = response_data.get("errorMessage", "") raise MpesaApiException( MpesaError( @@ -74,7 +75,7 @@ def post( return response_data - except requests.Timeout: + except httpx.TimeoutException: raise MpesaApiException( MpesaError( error_code="REQUEST_TIMEOUT", @@ -82,15 +83,7 @@ def post( status_code=None, ) ) - except requests.ConnectionError: - raise MpesaApiException( - MpesaError( - error_code="CONNECTION_ERROR", - error_message="Failed to connect to Mpesa API. Check network or URL.", - status_code=None, - ) - ) - except requests.RequestException as e: + except httpx.RequestError as e: raise MpesaApiException( MpesaError( error_code="REQUEST_FAILED", @@ -124,16 +117,15 @@ def get( headers = {} full_url = f"{self.base_url}{url}" - response = requests.get( - full_url, params=params, headers=headers, timeout=10 - ) # Add timeout + with httpx.Client(timeout=10) as client: + response = client.get(full_url, params=params, headers=headers) try: response_data = response.json() except ValueError: response_data = {"errorMessage": response.text.strip() or ""} - if not response.ok: + if not response.is_success: error_message = response_data.get("errorMessage", "") raise MpesaApiException( MpesaError( @@ -146,7 +138,7 @@ def get( return response_data - except requests.Timeout: + except httpx.TimeoutException: raise MpesaApiException( MpesaError( error_code="REQUEST_TIMEOUT", @@ -154,15 +146,7 @@ def get( status_code=None, ) ) - except requests.ConnectionError: - raise MpesaApiException( - MpesaError( - error_code="CONNECTION_ERROR", - error_message="Failed to connect to Mpesa API. Check network or URL.", - status_code=None, - ) - ) - except requests.RequestException as e: + except httpx.RequestError as e: raise MpesaApiException( MpesaError( error_code="REQUEST_FAILED", diff --git a/tests/integration/mpesa_express/test_stk_push_e2e.py b/tests/integration/mpesa_express/test_stk_push_e2e.py index 70531c1..6a65869 100644 --- a/tests/integration/mpesa_express/test_stk_push_e2e.py +++ b/tests/integration/mpesa_express/test_stk_push_e2e.py @@ -7,7 +7,7 @@ import os import time import pytest -import requests +import httpx from threading import Thread from dotenv import load_dotenv @@ -82,7 +82,8 @@ def test_stk_push_full_e2e_with_query(stk_service, fastapi_server, ngrok_tunnel) print("🔗 Starting E2E Test: STK Push, Callback, and Query") # 1. Clear previous callbacks callback_base_url = f"{ngrok_tunnel}/mpesa/callback" - requests.post(f"{callback_base_url}/clear") + with httpx.Client() as client: + client.post(f"{callback_base_url}/clear") callback_url = f"{ngrok_tunnel}/mpesa/callback" print(f"📨 Using callback URL: {callback_url}") @@ -111,18 +112,19 @@ def test_stk_push_full_e2e_with_query(stk_service, fastapi_server, ngrok_tunnel) print("⏳ Waiting up to 30 seconds for callback...") callback_received = False callback = None - for _ in range(30): - time.sleep(1) - r = requests.get(f"{callback_base_url}/latest", timeout=45) - if r.status_code == 200: - callback_received = True - callback_json = r.json()["parsed"] - callback = StkPushSimulateCallback.model_validate(callback_json) - body = callback.Body.stkCallback - print( - f"🎉 Callback received: ResultCode={body.ResultCode}, Desc={body.ResultDesc}" - ) - break + with httpx.Client() as client: + for _ in range(30): + time.sleep(1) + r = client.get(f"{callback_base_url}/latest", timeout=45) + if r.status_code == 200: + callback_received = True + callback_json = r.json()["parsed"] + callback = StkPushSimulateCallback.model_validate(callback_json) + body = callback.Body.stkCallback + print( + f"🎉 Callback received: ResultCode={body.ResultCode}, Desc={body.ResultDesc}" + ) + break if not callback_received: print( diff --git a/tests/unit/http_client/test_mpesa_http_client.py b/tests/unit/http_client/test_mpesa_http_client.py index 0733986..cb026e4 100644 --- a/tests/unit/http_client/test_mpesa_http_client.py +++ b/tests/unit/http_client/test_mpesa_http_client.py @@ -4,7 +4,7 @@ HTTP POST and GET request handling, and error handling for various scenarios. """ -import requests +import httpx import pytest from unittest.mock import Mock, patch from mpesakit.http_client.mpesa_http_client import MpesaHttpClient @@ -32,9 +32,9 @@ def test_base_url_production(): def test_post_success(client): """Test successful POST request returns expected JSON.""" - with patch("mpesakit.http_client.mpesa_http_client.requests.post") as mock_post: + with patch("mpesakit.http_client.mpesa_http_client.httpx.post") as mock_post: mock_response = Mock() - mock_response.ok = True + mock_response.status_code = 200 mock_response.json.return_value = {"foo": "bar"} mock_post.return_value = mock_response @@ -45,9 +45,8 @@ def test_post_success(client): def test_post_http_error(client): """Test POST request returns MpesaApiException on HTTP error.""" - with patch("mpesakit.http_client.mpesa_http_client.requests.post") as mock_post: + with patch("mpesakit.http_client.mpesa_http_client.httpx.post") as mock_post: mock_response = Mock() - mock_response.ok = False mock_response.status_code = 400 mock_response.json.return_value = {"errorMessage": "Bad Request"} mock_post.return_value = mock_response @@ -60,9 +59,8 @@ def test_post_http_error(client): def test_post_json_decode_error(client): """Test POST request handles JSON decode error gracefully.""" - with patch("mpesakit.http_client.mpesa_http_client.requests.post") as mock_post: + with patch("mpesakit.http_client.mpesa_http_client.httpx.post") as mock_post: mock_response = Mock() - mock_response.ok = False mock_response.status_code = 500 mock_response.json.side_effect = ValueError() mock_response.text = "Internal Server Error" @@ -77,8 +75,8 @@ def test_post_json_decode_error(client): def test_post_request_exception(client): """Test POST request raises MpesaApiException on generic exception.""" with patch( - "mpesakit.http_client.mpesa_http_client.requests.post", - side_effect=requests.RequestException("boom"), + "mpesakit.http_client.mpesa_http_client.httpx.post", + side_effect=httpx.RequestError("boom"), ): with pytest.raises(MpesaApiException) as exc: client.post("/fail", json={}, headers={}) @@ -88,8 +86,8 @@ def test_post_request_exception(client): def test_post_timeout(client): """Test POST request raises MpesaApiException on timeout.""" with patch( - "mpesakit.http_client.mpesa_http_client.requests.post", - side_effect=requests.Timeout, + "mpesakit.http_client.mpesa_http_client.httpx.post", + side_effect=httpx.TimeoutException, ): with pytest.raises(MpesaApiException) as exc: client.post("/timeout", json={}, headers={}) @@ -99,8 +97,8 @@ def test_post_timeout(client): def test_post_connection_error(client): """Test POST request raises MpesaApiException on connection error.""" with patch( - "mpesakit.http_client.mpesa_http_client.requests.post", - side_effect=requests.ConnectionError, + "mpesakit.http_client.mpesa_http_client.httpx.post", + side_effect=httpx.ConnectError, ): with pytest.raises(MpesaApiException) as exc: client.post("/conn", json={}, headers={}) @@ -109,7 +107,7 @@ def test_post_connection_error(client): def test_get_success(client): """Test successful GET request returns expected JSON.""" - with patch("mpesakit.http_client.mpesa_http_client.requests.get") as mock_get: + with patch("mpesakit.http_client.mpesa_http_client.httpx.get") as mock_get: mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = {"foo": "bar"} @@ -122,9 +120,8 @@ def test_get_success(client): def test_get_http_error(client): """Test GET request returns MpesaApiException on HTTP error.""" - with patch("mpesakit.http_client.mpesa_http_client.requests.get") as mock_get: + with patch("mpesakit.http_client.mpesa_http_client.httpx.get") as mock_get: mock_response = Mock() - mock_response.ok = False mock_response.status_code = 404 mock_response.json.return_value = {"errorMessage": "Not Found"} mock_get.return_value = mock_response @@ -137,9 +134,8 @@ def test_get_http_error(client): def test_get_json_decode_error(client): """Test GET request handles JSON decode error gracefully.""" - with patch("mpesakit.http_client.mpesa_http_client.requests.get") as mock_get: + with patch("mpesakit.http_client.mpesa_http_client.httpx.get") as mock_get: mock_response = Mock() - mock_response.ok = False mock_response.status_code = 500 mock_response.json.side_effect = ValueError() mock_response.text = "Internal Server Error" @@ -154,8 +150,8 @@ def test_get_json_decode_error(client): def test_get_request_exception(client): """Test GET request raises MpesaApiException on generic exception.""" with patch( - "mpesakit.http_client.mpesa_http_client.requests.get", - side_effect=requests.RequestException("boom"), + "mpesakit.http_client.mpesa_http_client.httpx.get", + side_effect=httpx.RequestError("boom"), ): with pytest.raises(MpesaApiException) as exc: client.get("/fail") @@ -165,8 +161,8 @@ def test_get_request_exception(client): def test_get_timeout(client): """Test GET request raises MpesaApiException on timeout.""" with patch( - "mpesakit.http_client.mpesa_http_client.requests.get", - side_effect=requests.Timeout, + "mpesakit.http_client.mpesa_http_client.httpx.get", + side_effect=httpx.TimeoutException, ): with pytest.raises(MpesaApiException) as exc: client.get("/timeout") @@ -176,10 +172,9 @@ def test_get_timeout(client): def test_get_connection_error(client): """Test GET request raises MpesaApiException on connection error.""" with patch( - "mpesakit.http_client.mpesa_http_client.requests.get", - side_effect=requests.ConnectionError, + "mpesakit.http_client.mpesa_http_client.httpx.get", + side_effect=httpx.ConnectError, ): with pytest.raises(MpesaApiException) as exc: client.get("/conn") assert exc.value.error.error_code == "CONNECTION_ERROR" - From e7b8ddb51b9a24d0f1c1f8fc21d46a27a79f25f6 Mon Sep 17 00:00:00 2001 From: SherylNyawira Date: Thu, 18 Sep 2025 11:36:32 +0300 Subject: [PATCH 4/7] fix: tests mocked httpx.Client.post instead of httpx.post --- tests/unit/http_client/test_mpesa_http_client.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/unit/http_client/test_mpesa_http_client.py b/tests/unit/http_client/test_mpesa_http_client.py index cb026e4..9c036b4 100644 --- a/tests/unit/http_client/test_mpesa_http_client.py +++ b/tests/unit/http_client/test_mpesa_http_client.py @@ -32,7 +32,7 @@ def test_base_url_production(): def test_post_success(client): """Test successful POST request returns expected JSON.""" - with patch("mpesakit.http_client.mpesa_http_client.httpx.post") as mock_post: + with patch("mpesakit.http_client.mpesa_http_client.httpx.Client.post") as mock_post: mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = {"foo": "bar"} @@ -45,7 +45,7 @@ def test_post_success(client): def test_post_http_error(client): """Test POST request returns MpesaApiException on HTTP error.""" - with patch("mpesakit.http_client.mpesa_http_client.httpx.post") as mock_post: + with patch("mpesakit.http_client.mpesa_http_client.httpx.Client.post") as mock_post: mock_response = Mock() mock_response.status_code = 400 mock_response.json.return_value = {"errorMessage": "Bad Request"} @@ -59,7 +59,7 @@ def test_post_http_error(client): def test_post_json_decode_error(client): """Test POST request handles JSON decode error gracefully.""" - with patch("mpesakit.http_client.mpesa_http_client.httpx.post") as mock_post: + with patch("mpesakit.http_client.mpesa_http_client.httpx.Client.post") as mock_post: mock_response = Mock() mock_response.status_code = 500 mock_response.json.side_effect = ValueError() @@ -75,7 +75,7 @@ def test_post_json_decode_error(client): def test_post_request_exception(client): """Test POST request raises MpesaApiException on generic exception.""" with patch( - "mpesakit.http_client.mpesa_http_client.httpx.post", + "mpesakit.http_client.mpesa_http_client.httpx.Client.post", side_effect=httpx.RequestError("boom"), ): with pytest.raises(MpesaApiException) as exc: @@ -86,7 +86,7 @@ def test_post_request_exception(client): def test_post_timeout(client): """Test POST request raises MpesaApiException on timeout.""" with patch( - "mpesakit.http_client.mpesa_http_client.httpx.post", + "mpesakit.http_client.mpesa_http_client.httpx.Client.post", side_effect=httpx.TimeoutException, ): with pytest.raises(MpesaApiException) as exc: @@ -97,7 +97,7 @@ def test_post_timeout(client): def test_post_connection_error(client): """Test POST request raises MpesaApiException on connection error.""" with patch( - "mpesakit.http_client.mpesa_http_client.httpx.post", + "mpesakit.http_client.mpesa_http_client.httpx.Client.post", side_effect=httpx.ConnectError, ): with pytest.raises(MpesaApiException) as exc: From 2fdefcab0afd664764f5399a79c002eeba7f2f92 Mon Sep 17 00:00:00 2001 From: SherylNyawira Date: Thu, 18 Sep 2025 11:40:53 +0300 Subject: [PATCH 5/7] fix: tests mocked httpx.Client.get instead of httpx.get --- tests/unit/http_client/test_mpesa_http_client.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/unit/http_client/test_mpesa_http_client.py b/tests/unit/http_client/test_mpesa_http_client.py index 9c036b4..308e952 100644 --- a/tests/unit/http_client/test_mpesa_http_client.py +++ b/tests/unit/http_client/test_mpesa_http_client.py @@ -107,7 +107,7 @@ def test_post_connection_error(client): def test_get_success(client): """Test successful GET request returns expected JSON.""" - with patch("mpesakit.http_client.mpesa_http_client.httpx.get") as mock_get: + with patch("mpesakit.http_client.mpesa_http_client.httpx.Client.get") as mock_get: mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = {"foo": "bar"} @@ -120,7 +120,7 @@ def test_get_success(client): def test_get_http_error(client): """Test GET request returns MpesaApiException on HTTP error.""" - with patch("mpesakit.http_client.mpesa_http_client.httpx.get") as mock_get: + with patch("mpesakit.http_client.mpesa_http_client.httpx.Client.get") as mock_get: mock_response = Mock() mock_response.status_code = 404 mock_response.json.return_value = {"errorMessage": "Not Found"} @@ -134,7 +134,7 @@ def test_get_http_error(client): def test_get_json_decode_error(client): """Test GET request handles JSON decode error gracefully.""" - with patch("mpesakit.http_client.mpesa_http_client.httpx.get") as mock_get: + with patch("mpesakit.http_client.mpesa_http_client.httpx.Client.get") as mock_get: mock_response = Mock() mock_response.status_code = 500 mock_response.json.side_effect = ValueError() @@ -150,7 +150,7 @@ def test_get_json_decode_error(client): def test_get_request_exception(client): """Test GET request raises MpesaApiException on generic exception.""" with patch( - "mpesakit.http_client.mpesa_http_client.httpx.get", + "mpesakit.http_client.mpesa_http_client.httpx.Client.get", side_effect=httpx.RequestError("boom"), ): with pytest.raises(MpesaApiException) as exc: @@ -161,7 +161,7 @@ def test_get_request_exception(client): def test_get_timeout(client): """Test GET request raises MpesaApiException on timeout.""" with patch( - "mpesakit.http_client.mpesa_http_client.httpx.get", + "mpesakit.http_client.mpesa_http_client.httpx.Client.get", side_effect=httpx.TimeoutException, ): with pytest.raises(MpesaApiException) as exc: @@ -172,7 +172,7 @@ def test_get_timeout(client): def test_get_connection_error(client): """Test GET request raises MpesaApiException on connection error.""" with patch( - "mpesakit.http_client.mpesa_http_client.httpx.get", + "mpesakit.http_client.mpesa_http_client.httpx.Client.get", side_effect=httpx.ConnectError, ): with pytest.raises(MpesaApiException) as exc: From 392bee92e6ea7177981d6dfa727ff035aed2f593 Mon Sep 17 00:00:00 2001 From: SherylNyawira Date: Thu, 18 Sep 2025 11:54:47 +0300 Subject: [PATCH 6/7] fix: use status code instead of method that was initially in requests lib --- mpesakit/http_client/mpesa_http_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mpesakit/http_client/mpesa_http_client.py b/mpesakit/http_client/mpesa_http_client.py index 5f7393c..f99b389 100644 --- a/mpesakit/http_client/mpesa_http_client.py +++ b/mpesakit/http_client/mpesa_http_client.py @@ -62,7 +62,7 @@ def post( except ValueError: response_data = {"errorMessage": response.text.strip() or ""} - if not response.is_success: + if response.status_code >= 400: error_message = response_data.get("errorMessage", "") raise MpesaApiException( MpesaError( From a75690901a84e537b697269d6346e70a1342464f Mon Sep 17 00:00:00 2001 From: SherylNyawira Date: Wed, 5 Nov 2025 14:12:59 +0300 Subject: [PATCH 7/7] =?UTF-8?q?=1B[200~fix(light-theme):=20improve=20visib?= =?UTF-8?q?ility=20and=20contrast=20for=20MpesaKit=20components?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/src/components/MpesaKit.module.css | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/docs/src/components/MpesaKit.module.css b/docs/src/components/MpesaKit.module.css index 9748464..f71cb65 100644 --- a/docs/src/components/MpesaKit.module.css +++ b/docs/src/components/MpesaKit.module.css @@ -4,9 +4,8 @@ --mpesa-green: #00D13A; --mpesa-dark-green: #00B032; --mpesa-light-green: #4AE668; - --dark-bg: #0a0a0a; - --card-bg: rgba(255, 255, 255, 0.05); - --text-primary: #ffffff; + --card-bg: rgba(0, 0, 0, 0.05); + --text-primary: #1a1a1a; --text-secondary: #a0a0a0; --gradient-primary: linear-gradient(135deg, var(--mpesa-green) 0%, var(--mpesa-light-green) 100%); --gradient-dark: linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 100%); @@ -140,10 +139,10 @@ justify-content: center; flex-direction: column; } - .btn { padding: 0.875rem 1.5rem; - border: none; + border: 1px solid rgba(0, 0, 0, 0.1); + border-radius: 50px; border-radius: 50px; font-weight: 600; text-decoration: none; @@ -259,6 +258,7 @@ overflow-x: auto; -webkit-overflow-scrolling: touch; white-space: nowrap; + color: #f8f8f8; } .codeLine { @@ -610,11 +610,11 @@ grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 2rem; } - .featureCard { background: var(--card-bg); border-radius: 20px; padding: 2.5rem; + border: 1px solid rgba(0, 0, 0, 0.1); border: 1px solid rgba(255, 255, 255, 0.1); transition: all 0.3s ease; position: relative; @@ -738,15 +738,13 @@ font-size: 1.2rem; flex-shrink: 0; } - -.securityVisual { - /* Styles handled inline in component */ -} +/* Security visual styles are handled inline in the component */ /* API Status Section */ .apiStatus { padding: 6rem 5%; - background: rgba(0, 0, 0, 0.3); + background: rgba(0, 209, 58, 0.02); + color: var(--text-primary); } .statusGrid {