Skip to content

A followup to pr #127#128

Open
watersRand wants to merge 4 commits intoByte-Barn:masterfrom
watersRand:test/stk-push-limit
Open

A followup to pr #127#128
watersRand wants to merge 4 commits intoByte-Barn:masterfrom
watersRand:test/stk-push-limit

Conversation

@watersRand
Copy link
Copy Markdown
Contributor

@watersRand watersRand commented Feb 25, 2026

Description

This PR is a follow-up on PR #127.
It aligns the MpesaAsyncHttpClient with the synchronous client to support unified retry functionality using the tenacity logic.

Key Changes:

Type of Change

  • Refactor (aligned async retry logic with sync client)
  • Tests (added 100 STK push simulation and retry path tests)

How Has This Been Tested?

  • Unit Tests: All existing tests in tests/unit/http_client/ pass.
  • Simulation: Verified 100 successful STK pushes using a persistent httpx.AsyncClient session.

Checklist

  • My code follows the project's coding style guidelines
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Summary by CodeRabbit

  • New Features

    • Automatic retry with exponential backoff for idempotent POST requests; idempotency key injected when enabled.
    • GET/POST accept optional headers and configurable timeouts; responses returned as parsed JSON.
  • Bug Fixes

    • No retries on client (4xx) errors; improved mapping of connection/timeout errors to clearer failure messages.
  • Tests

    • Stress tests for repeated STK Push usage.
    • New tests for idempotency persistence, retry limits, and updated error-message expectations.
  • Documentation

    • Example payloads annotated to suppress static-security scanner warnings.

Copy link
Copy Markdown
Member

@RafaelJohn9 RafaelJohn9 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @watersRand can we make the Commit message more informative of what we want to be achieved ?

The PR was never merged thus is hard to understand what this commit does without referencing a GitHub PR

And we also have some failing checks

@RafaelJohn9
Copy link
Copy Markdown
Member

@coderabbitai

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 7, 2026

Tip

For best results, initiate chat on the files or code changes.

@RafaelJohn9 It looks like your message may have been sent without the full content. How can I assist you? Here are a few things I can help with:

  • 🔍 Review the PR — I can perform a full review of the changes.
  • 🐛 Investigate a specific concern — Share the details and I'll dig in.
  • 📋 Summarize the changes — I can regenerate the PR summary.
  • 🛠️ Help with code changes — Describe what you'd like updated.

Just let me know! 🐇

@RafaelJohn9
Copy link
Copy Markdown
Member

@coderabbitai Review the PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 8, 2026

@RafaelJohn9 Sure! I'll perform a full review of PR #128 now. 🐇

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 8, 2026

📝 Walkthrough

Walkthrough

Added retry and idempotency handling to the async HTTP client, expanded post/get signatures (timeout/header/idempotent options), introduced internal raw/retryable helpers using Tenacity, updated exception mapping, and added/updated tests and small schema example tweaks.

Changes

Cohort / File(s) Summary
Async HTTP client
mpesakit/http_client/mpesa_async_http_client.py
Added Tenacity retry logic, mpesa_retry_condition, _raw_post/_raw_get helpers, _retryable_post wrapper, idempotency header injection, expanded post/get signatures (timeout, optional headers, idempotent flag) and consolidated exception handling.
Unit tests: async client
tests/unit/http_client/test_mpesa_async_http_client.py
Added tests for idempotency key persistence and retry behavior; updated expectations for timeout/connection/generic error messages; minor formatting adjustments.
Integration tests: STK push
tests/unit/mpesa_express/test_stk_push.py
Added synchronous and asynchronous repeated STK push tests (100 iterations) validating repeated calls and client reuse behavior.
Schemas (examples only)
mpesakit/mpesa_express/schemas.py
Appended #nosec B105 comment to Password example strings in schema examples (no runtime logic changes).

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant AsyncClient as MpesaAsyncHttpClient
  participant Retry as Tenacity (retry wrapper)
  participant HTTPX as httpx.AsyncClient
  participant MPESA as Mpesa API

  Client->>AsyncClient: post(url, json, headers?, timeout, idempotent?)
  alt idempotent == true
    AsyncClient->>Retry: _retryable_post(...)
    Retry->>HTTPX: _raw_post (X-Idempotency-Key ensured)
    HTTPX->>MPESA: HTTP POST
    MPESA-->>HTTPX: response
    HTTPX-->>Retry: response
    Retry-->>AsyncClient: response
  else
    AsyncClient->>HTTPX: _raw_post(...)
    HTTPX->>MPESA: HTTP POST
    MPESA-->>HTTPX: response
    HTTPX-->>AsyncClient: response
  end
  AsyncClient->>AsyncClient: handle_request_error(response)
  AsyncClient-->>Client: response.json() or raise MpesaApiException
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Feat/async #122: Related changes to async HTTP client behavior and retry/idempotency logic affecting the same module and method signatures.
  • Develop #99: Prior modifications to mpesa_async_http_client.py touching async post/get behavior and exception handling.

"I nibble logs and hop with glee,
A UUID carrot for idempotency.
Retries bounce like springtime hops,
Tests applaud my fluffy props.
— 🐰"

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is vague and does not clearly communicate the main change; it only references a prior PR without describing what this PR actually does. Revise the title to clearly describe the primary change, such as 'Align MpesaAsyncHttpClient with sync client retry logic using tenacity' or 'Add retry support and idempotency to async HTTP client'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description check ✅ Passed The description covers key changes and includes most required sections; however, the 'How Has This Been Tested?' section is minimal and lacks specific test coverage details.
Docstring Coverage ✅ Passed Docstring coverage is 90.48% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/unit/http_client/test_mpesa_async_http_client.py (1)

162-174: Assert the retry attempt count in these GET failure tests.

These assertions only pin the final translated exception. They still pass if _raw_get() stops retrying but preserves the last error mapping. Add an await_count == 3 assertion to match stop_after_attempt(3) so the new retry contract is actually covered.

Also applies to: 178-190, 194-206

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/http_client/test_mpesa_async_http_client.py` around lines 162 -
174, The test only checks the final mapped MpesaApiException but not that
retries occurred; update test_get_timeout (and the other similar tests at the
referenced ranges) to assert the mocked request was awaited the expected number
of times by adding an assertion like async_client._client.get.await_count == 3
after the existing exception assertions to match stop_after_attempt(3); apply
the same await_count == 3 check in the two other GET failure tests mentioned.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mpesakit/http_client/mpesa_async_http_client.py`:
- Around line 50-57: The POST retry policy risks duplicate transactions; update
the retry behavior in _raw_post (and its decorator/usage) so POST requests do
not automatically retry on network timeouts unless the caller explicitly opts
into idempotency/deduplication; specifically, change the retry predicate used on
_raw_post to only retry for safe/idempotent methods or when an idempotency
token/header is provided, and leave _raw_get unchanged; ensure callers like the
STK Push flow (stk_push.py processrequest call) either disable retries or pass
an idempotency key before allowing retries.

In `@tests/unit/mpesa_express/test_stk_push.py`:
- Around line 164-183: The test is calling MpesaHttpClient.post("/test")
directly which bypasses the StkPush flow; instead construct a StkPush (and
AsyncStkPush for the other test) using a mocked token manager and the real
client, patch the underlying httpx.Client.post used by StkPush to return the
mocked response, then loop 100 times calling StkPush.push() so the token lookup,
Authorization header construction, request serialization and service-layer reuse
are exercised; ensure you reference StkPush.push (and AsyncStkPush.push) and the
token manager mock when updating the test and assert both the push success count
and the mocked httpx post call_count == 100.

---

Nitpick comments:
In `@tests/unit/http_client/test_mpesa_async_http_client.py`:
- Around line 162-174: The test only checks the final mapped MpesaApiException
but not that retries occurred; update test_get_timeout (and the other similar
tests at the referenced ranges) to assert the mocked request was awaited the
expected number of times by adding an assertion like
async_client._client.get.await_count == 3 after the existing exception
assertions to match stop_after_attempt(3); apply the same await_count == 3 check
in the two other GET failure tests mentioned.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: acaf4488-58de-4e2b-9858-ce73371b628f

📥 Commits

Reviewing files that changed from the base of the PR and between 40042d9 and 22e5468.

📒 Files selected for processing (3)
  • mpesakit/http_client/mpesa_async_http_client.py
  • tests/unit/http_client/test_mpesa_async_http_client.py
  • tests/unit/mpesa_express/test_stk_push.py

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
mpesakit/http_client/mpesa_async_http_client.py (1)

79-91: Remove duplicated POST transport logic in _retryable_post.

_retryable_post() duplicates _raw_post() and can drift over time. Delegate to _raw_post() so retry behavior wraps a single transport implementation.

♻️ Proposed refactor
     async def _retryable_post(
         self,
         url: str,
         json: Dict[str, Any],
         headers: Dict[str, str],
         timeout: int = 10,
     ) -> httpx.Response:
-        return await self._client.post(
-            url,
-            json=json,
-            headers=headers,
-            timeout=timeout,
-        )
+        return await self._raw_post(url=url, json=json, headers=headers, timeout=timeout)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mpesakit/http_client/mpesa_async_http_client.py` around lines 79 - 91,
_refiable_post currently duplicates the HTTP transport by directly calling
self._client.post; change it to delegate to the single transport implementation
_raw_post so retries wrap one place. Replace the body of _retryable_post to call
and return await self._raw_post(url=url, json=json, headers=headers,
timeout=timeout) (preserve the same parameters and return type httpx.Response)
so all POST logic lives in _raw_post and retry behavior uses that single
implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mpesakit/http_client/mpesa_async_http_client.py`:
- Around line 113-117: The code mutates the caller-provided headers dict when
adding an idempotency key, which can leak the key into subsequent requests; fix
by making a shallow copy of headers before any modification (e.g., assign
headers = dict(headers) when headers is not None or headers = {} otherwise) and
then add the "X-Idempotency-Key" (using uuid.uuid4()) to that copy so the
original dict passed into the method remains unchanged; update the logic around
the idempotent check and "X-Idempotency-Key" assignment (referencing the headers
variable, the idempotent flag, and uuid.uuid4()) to use the copied headers.

In `@tests/unit/mpesa_express/test_stk_push.py`:
- Around line 164-210: The test test_stk_push_multiple_times should ensure
MpesaHttpClient is closed even on early failures: wrap the loop/body that
constructs MpesaHttpClient(env="sandbox", use_session=use_session) and uses stk
= StkPush(http_client=client, token_manager=mock_token_manager) in a try/finally
(or use a context manager) and call client.close() (and if use_session=True
ensure any underlying session is closed) in the finally block so that the httpx
client/session is cleaned up regardless of assertion errors; apply the same
pattern to the other similar test at lines 341-386.

---

Nitpick comments:
In `@mpesakit/http_client/mpesa_async_http_client.py`:
- Around line 79-91: _refiable_post currently duplicates the HTTP transport by
directly calling self._client.post; change it to delegate to the single
transport implementation _raw_post so retries wrap one place. Replace the body
of _retryable_post to call and return await self._raw_post(url=url, json=json,
headers=headers, timeout=timeout) (preserve the same parameters and return type
httpx.Response) so all POST logic lives in _raw_post and retry behavior uses
that single implementation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 67c4458d-11a3-4571-b9b8-d15414df130c

📥 Commits

Reviewing files that changed from the base of the PR and between 22e5468 and 37157b4.

📒 Files selected for processing (3)
  • mpesakit/http_client/mpesa_async_http_client.py
  • tests/unit/http_client/test_mpesa_async_http_client.py
  • tests/unit/mpesa_express/test_stk_push.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/unit/http_client/test_mpesa_async_http_client.py

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
mpesakit/http_client/mpesa_async_http_client.py (1)

79-91: Have _retryable_post() delegate to _raw_post().

The retryable and non-retryable POST paths currently duplicate the same low-level request construction. Reusing _raw_post() keeps that behavior in one place and avoids the two paths drifting.

Suggested refactor
 async def _retryable_post(
     self,
     url: str,
     json: Dict[str, Any],
     headers: Dict[str, str],
     timeout: int = 10,
 ) -> httpx.Response:
-    return await self._client.post(
-        url,
-        json=json,
-        headers=headers,
-        timeout=timeout,
-    )
+    return await self._raw_post(url, json, headers, timeout)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mpesakit/http_client/mpesa_async_http_client.py` around lines 79 - 91,
_refactor:_retryable_post currently duplicates the low-level request logic;
change it to call and return self._raw_post(...) instead of calling
self._client.post directly. Pass the same parameters (url, json, headers,
timeout) through to _raw_post so the retryable path reuses the centralized
request construction in _raw_post (ensure the return type remains
httpx.Response).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mpesakit/http_client/mpesa_async_http_client.py`:
- Around line 115-116: The branch that sets an idempotency key can add a
duplicate header if the caller provided a differently-cased key; change the
check in the idempotent handling (where `idempotent` and `headers` are used in
mpesa_async_http_client.py) to look for any header whose lowercased name equals
"x-idempotency-key" (e.g., any(k.lower() == "x-idempotency-key" for k in
headers)) and only generate and insert a new `X-Idempotency-Key` when no such
key exists; keep the canonical "X-Idempotency-Key" casing when inserting so
downstream servers see a consistent header name.

In `@mpesakit/mpesa_express/schemas.py`:
- Line 92: The schema contains a literal Base64-looking password
"bXlwYXNzd29yZA==" with a "#nosec B105" suppression; remove the suppression and
replace the value for the "Password" field with a clearly fake placeholder (e.g.
"BASE64_PASSWORD_PLACEHOLDER" or "<PASSWORD_PLACEHOLDER>") so it no longer
resembles credentials, and make the same replacement for the other occurrence
referenced (the second "Password" example). Ensure you do not reintroduce any
secret-suppression comments.

---

Nitpick comments:
In `@mpesakit/http_client/mpesa_async_http_client.py`:
- Around line 79-91: _refactor:_retryable_post currently duplicates the
low-level request logic; change it to call and return self._raw_post(...)
instead of calling self._client.post directly. Pass the same parameters (url,
json, headers, timeout) through to _raw_post so the retryable path reuses the
centralized request construction in _raw_post (ensure the return type remains
httpx.Response).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f0433efa-38e7-494e-8986-09d0ba850e8a

📥 Commits

Reviewing files that changed from the base of the PR and between 37157b4 and 97eb1d0.

📒 Files selected for processing (3)
  • mpesakit/http_client/mpesa_async_http_client.py
  • mpesakit/mpesa_express/schemas.py
  • tests/unit/mpesa_express/test_stk_push.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/unit/mpesa_express/test_stk_push.py

"example": {
"BusinessShortCode": 654321,
"Password": "bXlwYXNzd29yZA==",
"Password": "bXlwYXNzd29yZA==", #nosec B105
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Replace the secret-looking sample instead of suppressing the warning.

These literals still look like credentials to secret scanners, so #nosec B105 only hides the Bandit finding while Betterleaks keeps flagging both examples. Use a clearly fake placeholder here instead so the schema docs stay useful without normalizing secret-leak suppressions.

Suggested change
-                "Password": "bXlwYXNzd29yZA==", `#nosec` B105
+                "Password": "<generated-from-shortcode-passkey-timestamp>",

Also applies to: 502-502

🧰 Tools
🪛 Betterleaks (1.1.1)

[high] 92-92: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mpesakit/mpesa_express/schemas.py` at line 92, The schema contains a literal
Base64-looking password "bXlwYXNzd29yZA==" with a "#nosec B105" suppression;
remove the suppression and replace the value for the "Password" field with a
clearly fake placeholder (e.g. "BASE64_PASSWORD_PLACEHOLDER" or
"<PASSWORD_PLACEHOLDER>") so it no longer resembles credentials, and make the
same replacement for the other occurrence referenced (the second "Password"
example). Ensure you do not reintroduce any secret-suppression comments.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
mpesakit/http_client/mpesa_async_http_client.py (2)

79-91: Inconsistent indentation in method signature and body.

The method parameters and return statement have non-standard indentation that differs from the rest of the file.

♻️ Suggested formatting fix
     async def _retryable_post(
-    self,
-    url: str,
-    json: Dict[str, Any],
-    headers: MutableMapping[str, str],
-    timeout: int = 10,
-) -> httpx.Response:
-     return await self._raw_post(
+        self,
+        url: str,
+        json: Dict[str, Any],
+        headers: MutableMapping[str, str],
+        timeout: int = 10,
+    ) -> httpx.Response:
+        return await self._raw_post(
             url,
             json,
             headers,
             timeout,
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mpesakit/http_client/mpesa_async_http_client.py` around lines 79 - 91, The
_retryable_post method has inconsistent indentation for its signature and the
return statement; adjust the function signature and body to match the project's
style (align parameters under the opening parenthesis or use a single-line
signature) and indent the return statement one level inside the method body,
making the call to _raw_post consistently formatted (refer to _retryable_post
and the call to _raw_post) so the parameters and closing parenthesis line up
with the rest of the file.

3-24: Redundant exception types in retry condition.

httpx.ConnectTimeout and httpx.ReadTimeout are subclasses of httpx.TimeoutException, so listing them explicitly is redundant. Including only httpx.TimeoutException and httpx.ConnectError would be sufficient and clearer.

♻️ Suggested simplification
 mpesa_retry_condition = retry_if_exception_type(
-    (httpx.ConnectError, httpx.ConnectTimeout,httpx.TimeoutException ,httpx.ReadTimeout)
+    (httpx.ConnectError, httpx.TimeoutException)
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mpesakit/http_client/mpesa_async_http_client.py` around lines 3 - 24, The
retry condition mpesa_retry_condition currently lists httpx.ConnectTimeout and
httpx.ReadTimeout along with httpx.TimeoutException, which is redundant because
those classes subclass httpx.TimeoutException; simplify the
retry_if_exception_type call used to build mpesa_retry_condition to include only
httpx.ConnectError and httpx.TimeoutException (remove explicit ConnectTimeout
and ReadTimeout) so the intent is clearer and behavior remains the same.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/unit/mpesa_express/test_stk_push.py`:
- Around line 344-386: The test redundantly calls await client.aclose() after
using the context manager; remove the explicit await client.aclose() call at the
end of the test so the MpesaAsyncHttpClient context manager (its __aexit__)
handles closing automatically—locate the test block that creates
MpesaAsyncHttpClient(), AsyncStkPush, and the loop (references:
MpesaAsyncHttpClient, AsyncStkPush, mock_async_token_manager) and delete the
final await client.aclose() statement.

---

Nitpick comments:
In `@mpesakit/http_client/mpesa_async_http_client.py`:
- Around line 79-91: The _retryable_post method has inconsistent indentation for
its signature and the return statement; adjust the function signature and body
to match the project's style (align parameters under the opening parenthesis or
use a single-line signature) and indent the return statement one level inside
the method body, making the call to _raw_post consistently formatted (refer to
_retryable_post and the call to _raw_post) so the parameters and closing
parenthesis line up with the rest of the file.
- Around line 3-24: The retry condition mpesa_retry_condition currently lists
httpx.ConnectTimeout and httpx.ReadTimeout along with httpx.TimeoutException,
which is redundant because those classes subclass httpx.TimeoutException;
simplify the retry_if_exception_type call used to build mpesa_retry_condition to
include only httpx.ConnectError and httpx.TimeoutException (remove explicit
ConnectTimeout and ReadTimeout) so the intent is clearer and behavior remains
the same.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0c3a8dac-3760-47f6-97db-b076d9ba3188

📥 Commits

Reviewing files that changed from the base of the PR and between 97eb1d0 and 35a463e.

📒 Files selected for processing (2)
  • mpesakit/http_client/mpesa_async_http_client.py
  • tests/unit/mpesa_express/test_stk_push.py

Comment on lines +344 to +386
async with MpesaAsyncHttpClient() as client:
stk = AsyncStkPush(http_client =client,token_manager=mock_async_token_manager)

request_data = StkPushSimulateRequest(
BusinessShortCode=174379,
Amount=1,
PhoneNumber="254700000000",
CallBackURL="https://example.com/callback",
AccountReference="TestRef",
TransactionDesc="TestDesc",
TransactionType="CustomerPayBillOnline",
PartyA="254700000000",
PartyB="174379",
Timestamp="20231010120000",
Password="base64_encoded_password"
)


mock_response = MagicMock(spec=httpx.Response)
mock_response.is_success = True
mock_response.status_code = 200
mock_response.json.return_value = {
"MerchantRequestID": "12345",
"CheckoutRequestID":"ws_CO_260520211133524545",
"ResponseDescription":"Test Description",
"CustomerMessage": "Success",
"ResponseCode": "0"}

with patch.object(mock_async_token_manager, "get_token", AsyncMock(return_value="mock_token")):
with patch.object(httpx.AsyncClient, "send", AsyncMock(return_value=mock_response)) as mock_send:
success_count = 0

for _ in range(100):
result = await stk.push(
request=request_data
)
if str(result.ResponseCode) == "0":
success_count += 1

assert success_count == 100
assert mock_send.call_count == 100

await client.aclose()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove redundant aclose() call inside async with block.

The async with MpesaAsyncHttpClient() context manager already calls aclose() in __aexit__. The explicit call on line 386 is redundant and results in double-closing the client.

🐛 Proposed fix
             assert success_count == 100
             assert mock_send.call_count == 100

-        await client.aclose()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/mpesa_express/test_stk_push.py` around lines 344 - 386, The test
redundantly calls await client.aclose() after using the context manager; remove
the explicit await client.aclose() call at the end of the test so the
MpesaAsyncHttpClient context manager (its __aexit__) handles closing
automatically—locate the test block that creates MpesaAsyncHttpClient(),
AsyncStkPush, and the loop (references: MpesaAsyncHttpClient, AsyncStkPush,
mock_async_token_manager) and delete the final await client.aclose() statement.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants